Skip to content

Optimize azimuthal_mean with one sparse ring operator - #1847

Draft
cmdupuis3 wants to merge 3 commits into
UXARRAY:mainfrom
cmdupuis3:cmd/lazy-azimuthal
Draft

cmdupuis3 wants to merge 3 commits into
UXARRAY:mainfrom
cmdupuis3:cmd/lazy-azimuthal

Conversation

@cmdupuis3

@cmdupuis3 cmdupuis3 commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #1848

Overview

Simple speedup for azimuthal means, and an incidental accuracy fix.

azimuthal_mean built a full subgrid for every ring (isel without ignore_grid), recomputed face_areas on it, and forced a compute per ring. The rings are disjoint, so the per-ring area-weighted means now form one sparse (n_radius, n_ring_faces) operator over the ring faces, gathered once with ignore_grid=True and weighted by the parent grid's areas. It is applied through a single apply_ufunc, so dask-backed input stays lazy.

Ring membership, hit counts, output dims/coords/attrs and NaN handling (NaN faces add nothing to the sum but still count toward the ring area) are unchanged. Accumulation is now float64: on float32 grids the old float32 sums were off by up to ~3e-7 relative against a float64 reference; the new result matches it to ~3e-16.

Adds AzimuthalAverage to the oQU benchmarks (21 rings, out to 20 degrees): 480km 43.9 ms -> 0.7 ms, 120km 50.0 ms -> 1.2 ms.

PR Checklist

General

  • An issue is created and linked
  • Added appropriate labels (if your uxarray repo permissions allow it)
  • Filled out Overview and Expected Usage (if applicable) sections

Testing & Benchmarking

  • There is adequate test coverage of changes from this PR (add new tests if needed)
  • If this PR could affect performance, ran ASV benchmarks and confirmed they show expected behavior (add a new benchmark if necessary)

Documentation and Examples

  • Docstrings updated with any function changes, and included in all new functions
  • User (public) functions added to docs/api.rst; internal (private) function names start with an underscore (_)

AI Disclosure

AI Usage: Claude Opus 5.5

  • I have tested and take responsibility for all AI-generated content in my PR.

azimuthal_mean built a full subgrid for every ring (isel without
ignore_grid), recomputed face_areas on it, and forced a compute per ring.
The rings are disjoint, so the per-ring area-weighted means now form one
sparse (n_radius, n_ring_faces) operator over the ring faces, gathered
once with ignore_grid=True and weighted by the parent grid's areas. It is
applied through a single apply_ufunc, so dask-backed input stays lazy.

Ring membership, hit counts, output dims/coords/attrs and NaN handling
(NaN faces add nothing to the sum but still count toward the ring area)
are unchanged. Accumulation is now float64: on float32 grids the old
float32 sums were off by up to ~3e-7 relative against a float64
reference; the new result matches it to ~3e-16.

Adds AzimuthalAverage to the oQU benchmarks (21 rings, out to 20 degrees):
480km 43.9 ms -> 0.7 ms, 120km 50.0 ms -> 1.2 ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cmdupuis3 cmdupuis3 added scalability Related to scalability & performance efforts run-benchmark Run ASV benchmark workflow accuracy Related to mathematical accuracy of operations, such as numerical precision or rounding errors labels Oct 23, 2026
@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

ASV Benchmarking

Benchmark Comparison Results

Benchmarks that have improved:

Change Before [b3896f4] After [a3c427e] Ratio Benchmark (Parameter)
- 393±0.6ms 3.24±0.02ms 0.01 mpas_ocean.AzimuthalAverage.time_azimuthal_average('120km')
- 353±4ms 1.38±0.04ms 0 mpas_ocean.AzimuthalAverage.time_azimuthal_average('480km')

Benchmarks that have stayed the same:

Change Before [b3896f4] After [a3c427e] Ratio Benchmark (Parameter)
997±20μs 969±20μs 0.97 connectivity.Connectivity.time_edge_face('120km')
485±20μs 485±20μs 1 connectivity.Connectivity.time_edge_face('480km')
3.98±0.1ms 4.00±0.04ms 1.01 connectivity.Connectivity.time_edge_node('120km')
1.32±0.04ms 1.31±0.02ms 0.99 connectivity.Connectivity.time_edge_node('480km')
73.7±9μs 70.7±10μs 0.96 connectivity.Connectivity.time_face_edge('120km')
69.6±7μs 68.2±5μs 0.98 connectivity.Connectivity.time_face_edge('480km')
994±10μs 985±10μs 0.99 connectivity.Connectivity.time_face_face('120km')
421±20μs 419±20μs 1 connectivity.Connectivity.time_face_face('480km')
66.0±10μs 62.3±2μs 0.94 connectivity.Connectivity.time_face_node('120km')
67.1±4μs 67.7±8μs 1.01 connectivity.Connectivity.time_face_node('480km')
495±40μs 484±20μs 0.98 connectivity.Connectivity.time_n_nodes_per_face('120km')
419±20μs 418±30μs 1 connectivity.Connectivity.time_n_nodes_per_face('480km')
1.37±0.03ms 1.36±0.02ms 0.99 connectivity.Connectivity.time_node_edge('120km')
483±10μs 490±10μs 1.02 connectivity.Connectivity.time_node_edge('480km')
84.0±4ms 79.9±2ms 0.95 connectivity.Connectivity.time_node_face('120km')
5.03±0.2ms 5.04±0.05ms 1 connectivity.Connectivity.time_node_face('480km')
1.42M 1.42M 1 connectivity.ConnectivityTracemalloc.track_peakmem_edge_face('120km')
106k 106k 1 connectivity.ConnectivityTracemalloc.track_peakmem_edge_face('480km')
6.48M 6.48M 1 connectivity.ConnectivityTracemalloc.track_peakmem_edge_node('120km')
420k 420k 1 connectivity.ConnectivityTracemalloc.track_peakmem_edge_node('480km')
2.42k 2.42k 1 connectivity.ConnectivityTracemalloc.track_peakmem_face_edge('120km')
2.42k 2.42k 1 connectivity.ConnectivityTracemalloc.track_peakmem_face_edge('480km')
1.6M 1.6M 1 connectivity.ConnectivityTracemalloc.track_peakmem_face_face('120km')
101k 101k 1 connectivity.ConnectivityTracemalloc.track_peakmem_face_face('480km')
2.54k 2.54k 1 connectivity.ConnectivityTracemalloc.track_peakmem_face_node('120km')
2.54k 2.54k 1 connectivity.ConnectivityTracemalloc.track_peakmem_face_node('480km')
240k 240k 1 connectivity.ConnectivityTracemalloc.track_peakmem_n_nodes_per_face('120km')
26.3k 26.3k 1 connectivity.ConnectivityTracemalloc.track_peakmem_n_nodes_per_face('480km')
1.9M 1.9M 1 connectivity.ConnectivityTracemalloc.track_peakmem_node_edge('120km')
127k 127k 1 connectivity.ConnectivityTracemalloc.track_peakmem_node_edge('480km')
11.9M 11.9M 1 connectivity.ConnectivityTracemalloc.track_peakmem_node_face('120km')
747k 747k 1 connectivity.ConnectivityTracemalloc.track_peakmem_node_face('480km')
8.53±0.7ms 8.48±0.04ms 0.99 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
2.71±0.05ms 2.73±0.05ms 1.01 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
10.2±0.07ms 10.1±9ms 0.99 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
1.57±0.02ms 1.56±0.03ms 0.99 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
57.3k 57.3k 1 face_bounds.FaceBounds.track_nbytes_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
12.3k 12.3k 1 face_bounds.FaceBounds.track_nbytes_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
123k 123k 1 face_bounds.FaceBounds.track_nbytes_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
128 128 1 face_bounds.FaceBounds.track_nbytes_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
1.27M 1.27M 1 face_bounds.FaceBounds.track_nbytes_grid_with_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
50.1k 50.1k 1 face_bounds.FaceBounds.track_nbytes_grid_with_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
1.48M 1.48M 1 face_bounds.FaceBounds.track_nbytes_grid_with_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
712 712 1 face_bounds.FaceBounds.track_nbytes_grid_with_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
1.85M 1.85M 1 face_bounds.FaceBounds.track_peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
1.85M 1.85M 1 face_bounds.FaceBounds.track_peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
2.01M 2.01M 1 face_bounds.FaceBounds.track_peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
35.6k 35.6k 1 face_bounds.FaceBounds.track_peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
891±20ns 901±20ns 1.01 geometry_kernels.AccucrossKernels.time_accucross
1.96±0.02μs 1.95±0.04μs 1 geometry_kernels.AccucrossKernels.time_accucross_pair
446±4ns 462±30ns 1.03 geometry_kernels.EFTPrimitives.time_acc_sqrt_re
419±10ns 406±20ns 0.97 geometry_kernels.EFTPrimitives.time_diff_of_products
401±10ns 368±20ns 0.92 geometry_kernels.EFTPrimitives.time_two_prod
384±30ns 373±20ns 0.97 geometry_kernels.EFTPrimitives.time_two_sum
451±9ns 465±9ns 1.03 geometry_kernels.GCAConstLatIntersection.time_accux_constlat_kernel
530±10ns 511±5ns 0.96 geometry_kernels.GCAConstLatIntersection.time_gca_const_lat_intersection
571±20ns 521±10ns 0.91 geometry_kernels.GCAConstLatIntersection.time_try_gca_const_lat_intersection
802±10ns 812±5ns 1.01 geometry_kernels.GCAGCAIntersection.time_accux_gca_kernel
907±30ns 912±30ns 1.01 geometry_kernels.GCAGCAIntersection.time_gca_gca_intersection
1.08±0.02μs 1.03±0.01μs 0.95 geometry_kernels.GCAGCAIntersection.time_try_gca_gca_intersection
28.8±0.2μs 29.4±0.2μs 1.02 geometry_kernels.OrientPredicates.time_on_minor_arc
28.4±0.1μs 28.9±0.2μs 1.02 geometry_kernels.OrientPredicates.time_orient3d_on_sphere
3.07±0.02ms 3.06±0ms 1 geometry_samebody.SameBodyConstLat.time_accux_dispatch
1.17±0.01ms 1.16±0.01ms 1 geometry_samebody.SameBodyConstLat.time_accux_kernel
2.30±0ms 2.28±0ms 0.99 geometry_samebody.SameBodyConstLat.time_fp64_dispatch
148±0.7μs 146±0.09μs 0.99 geometry_samebody.SameBodyConstLat.time_fp64_kernel
22.8±0.07ms 23.4±0.03ms 1.03 geometry_samebody_gcagca.SameBodyGcaGca.time_accux_dispatch
5.54±0.01ms 5.53±0.01ms 1 geometry_samebody_gcagca.SameBodyGcaGca.time_accux_kernel
18.0±0.07ms 18.3±0.04ms 1.01 geometry_samebody_gcagca.SameBodyGcaGca.time_fp64_dispatch
697±3μs 698±2μs 1 geometry_samebody_gcagca.SameBodyGcaGca.time_fp64_kernel
707±8ms 711±6ms 1.01 import.Imports.timeraw_import_uxarray
2.87±0.03ms 2.89±0.03ms 1.01 mpas_ocean.CheckNorm.time_check_norm('120km')
1.86±0.01ms 1.83±0.01ms 0.98 mpas_ocean.CheckNorm.time_check_norm('480km')
1.18±0.05ms 1.17±0.05ms 0.99 mpas_ocean.ConnectivityConstruction.time_face_face_connectivity('120km')
544±7μs 523±7μs 0.96 mpas_ocean.ConnectivityConstruction.time_face_face_connectivity('480km')
682±10μs 687±5μs 1.01 mpas_ocean.ConnectivityConstruction.time_n_nodes_per_face('120km')
590±10μs 589±10μs 1 mpas_ocean.ConnectivityConstruction.time_n_nodes_per_face('480km')
5.26±0.03ms 5.27±0.03ms 1 mpas_ocean.ConstructFaceLatLon.time_cartesian_averaging('120km')
3.90±0.07ms 3.73±0.04ms 0.96 mpas_ocean.ConstructFaceLatLon.time_cartesian_averaging('480km')
96.5±0.3ms 96.8±0.8ms 1 mpas_ocean.ConstructFaceLatLon.time_welzl('120km')
9.45±0.03ms 10.0±0.3ms 1.06 mpas_ocean.ConstructFaceLatLon.time_welzl('480km')
16.3±0.02ms 16.1±0.04ms 0.99 mpas_ocean.ConstructTreeStructures.time_ball_tree('120km')
808±20μs 811±10μs 1 mpas_ocean.ConstructTreeStructures.time_ball_tree('480km')
8.18±0.04ms 8.18±0.03ms 1 mpas_ocean.ConstructTreeStructures.time_kd_tree('120km')
488±20μs 523±40μs 1.07 mpas_ocean.ConstructTreeStructures.time_kd_tree('480km')
428±2ms 429±4ms 1 mpas_ocean.CrossSections.time_const_lat('120km', 1)
216±2ms 217±6ms 1 mpas_ocean.CrossSections.time_const_lat('120km', 2)
111±0.3ms 114±0.6ms 1.03 mpas_ocean.CrossSections.time_const_lat('120km', 4)
377±4ms 379±4ms 1 mpas_ocean.CrossSections.time_const_lat('480km', 1)
190±2ms 187±1ms 0.98 mpas_ocean.CrossSections.time_const_lat('480km', 2)
97.5±2ms 94.7±0.6ms 0.97 mpas_ocean.CrossSections.time_const_lat('480km', 4)
342M 368M 1.08 mpas_ocean.CrossSectionsPeakMem.track_peakmem_const_lat('120km', 1)
342M 368M 1.08 mpas_ocean.CrossSectionsPeakMem.track_peakmem_const_lat('120km', 2)
342M 368M 1.08 mpas_ocean.CrossSectionsPeakMem.track_peakmem_const_lat('120km', 4)
26.2±0.5ms 26.0±0.06ms 0.99 mpas_ocean.DualMesh.time_dual_mesh_construction('120km')
2.94±0.04ms 2.94±0.04ms 1 mpas_ocean.DualMesh.time_dual_mesh_construction('480km')
14.6±0.4ms 14.1±0.5ms 0.97 mpas_ocean.FaceAreas.time_face_areas('120km')
4.30±0.06ms 4.53±0.1ms 1.05 mpas_ocean.FaceAreas.time_face_areas('480km')
229k 229k 1 mpas_ocean.FaceAreas.track_nbytes_face_areas('120km')
14.3k 14.3k 1 mpas_ocean.FaceAreas.track_nbytes_face_areas('480km')
2.12M 2.12M 1 mpas_ocean.FaceAreas.track_peakmem_face_areas('120km')
719k 718k 1 mpas_ocean.FaceAreas.track_peakmem_face_areas('480km')
329±5ms 325±4ms 0.99 mpas_ocean.GeoDataFrame.time_to_geodataframe('120km', False)
51.4±0.7ms 52.4±1ms 1.02 mpas_ocean.GeoDataFrame.time_to_geodataframe('120km', True)
42.7±0.2ms 41.1±0.2ms 0.96 mpas_ocean.GeoDataFrame.time_to_geodataframe('480km', False)
5.20±0.3ms 5.22±0.2ms 1 mpas_ocean.GeoDataFrame.time_to_geodataframe('480km', True)
13.0±0.05ms 13.1±0.04ms 1.01 mpas_ocean.Gradient.time_gradient('120km')
1.81±0.01ms 1.82±0.01ms 1 mpas_ocean.Gradient.time_gradient('480km')
457k 457k 1 mpas_ocean.Gradient.track_nbytes_gradient('120km')
28.7k 28.7k 1 mpas_ocean.Gradient.track_nbytes_gradient('480km')
3.2M 3.2M 1 mpas_ocean.Gradient.track_peakmem_gradient('120km')
204k 204k 1 mpas_ocean.Gradient.track_peakmem_gradient('480km')
338M 366M 1.08 mpas_ocean.GradientColdStartRss.track_peakmem_gradient('120km')
275±10μs 276±2μs 1 mpas_ocean.HoleEdgeIndices.time_construct_hole_edge_indices('120km')
149±10μs 147±6μs 0.99 mpas_ocean.HoleEdgeIndices.time_construct_hole_edge_indices('480km')
203±10μs 202±4μs 1 mpas_ocean.Integrate.time_integrate('120km')
184±10μs 186±1μs 1.01 mpas_ocean.Integrate.time_integrate('480km')
18.4M 18.4M 1 mpas_ocean.Integrate.track_nbytes_integrate('120km')
1.2M 1.2M 1 mpas_ocean.Integrate.track_nbytes_integrate('480km')
143±1ms 143±0.9ms 1 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'exclude')
144±0.4ms 143±1ms 1 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'include')
143±0.9ms 143±0.5ms 1 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'split')
10.1±0.1ms 10.1±0.3ms 1.01 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'exclude')
10.2±0.06ms 10.4±0.1ms 1.01 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'include')
10.2±0.1ms 10.6±0.2ms 1.03 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'split')
246±0.7ms 247±1ms 1 mpas_ocean.NeighborhoodBuild.time_build('120km', 1.0)
506±2ms 505±2ms 1 mpas_ocean.NeighborhoodBuild.time_build('120km', 5.0)
13.4±0.1ms 13.2±0.01ms 0.98 mpas_ocean.NeighborhoodBuild.time_build('480km', 1.0)
16.6±0.2ms 16.5±0.1ms 0.99 mpas_ocean.NeighborhoodBuild.time_build('480km', 5.0)
241±0.6ms 241±0.7ms 1 mpas_ocean.NeighborhoodBuild.time_query_radius('120km', 1.0)
502±2ms 498±0.7ms 0.99 mpas_ocean.NeighborhoodBuild.time_query_radius('120km', 5.0)
13.0±0.07ms 12.9±0.05ms 0.99 mpas_ocean.NeighborhoodBuild.time_query_radius('480km', 1.0)
16.1±0.07ms 16.1±0.07ms 1 mpas_ocean.NeighborhoodBuild.time_query_radius('480km', 5.0)
612.76 612.76 1 mpas_ocean.NeighborhoodBuild.track_mean_neighbors('120km', 15.0)
74.17 74.17 1 mpas_ocean.NeighborhoodBuild.track_mean_neighbors('120km', 5.0)
37.29 37.29 1 mpas_ocean.NeighborhoodBuild.track_mean_neighbors('480km', 15.0)
6.57 6.57 1 mpas_ocean.NeighborhoodBuild.track_mean_neighbors('480km', 5.0)
141M 141M 1 mpas_ocean.NeighborhoodBuild.track_nbytes_neighbors('120km', 15.0)
17.4M 17.4M 1 mpas_ocean.NeighborhoodBuild.track_nbytes_neighbors('120km', 5.0)
563k 563k 1 mpas_ocean.NeighborhoodBuild.track_nbytes_neighbors('480km', 15.0)
123k 123k 1 mpas_ocean.NeighborhoodBuild.track_nbytes_neighbors('480km', 5.0)
145M 145M 1 mpas_ocean.NeighborhoodBuild.track_peakmem_build('120km', 15.0)
21.5M 21.5M 1 mpas_ocean.NeighborhoodBuild.track_peakmem_build('120km', 5.0)
825k 825k 1 mpas_ocean.NeighborhoodBuild.track_peakmem_build('480km', 15.0)
384k 384k 1 mpas_ocean.NeighborhoodBuild.track_peakmem_build('480km', 5.0)
32.9±0.2ms 32.6±0.2ms 0.99 mpas_ocean.NeighborhoodDask.time_mean('120km', 'grid_chunks')
14.7±0.07ms 14.6±0.03ms 0.99 mpas_ocean.NeighborhoodDask.time_mean('120km', 'numpy')
30.3±0.7ms 29.8±0.3ms 0.98 mpas_ocean.NeighborhoodDask.time_mean('120km', 'time_chunks')
7.86±0.01ms 7.68±0.07ms 0.98 mpas_ocean.NeighborhoodDask.time_mean('480km', 'grid_chunks')
505±4μs 496±5μs 0.98 mpas_ocean.NeighborhoodDask.time_mean('480km', 'numpy')
5.45±0.1ms 5.13±0.03ms 0.94 mpas_ocean.NeighborhoodDask.time_mean('480km', 'time_chunks')
5.83M 5.77M 0.99 mpas_ocean.NeighborhoodDask.track_peakmem_mean('120km', 'grid_chunks')
2.75M 2.75M 1 mpas_ocean.NeighborhoodDask.track_peakmem_mean('120km', 'numpy')
5.68M 5.68M 1 mpas_ocean.NeighborhoodDask.track_peakmem_mean('120km', 'time_chunks')
758k 757k 1 mpas_ocean.NeighborhoodDask.track_peakmem_mean('480km', 'grid_chunks')
177k 177k 1 mpas_ocean.NeighborhoodDask.track_peakmem_mean('480km', 'numpy')
538k 540k 1 mpas_ocean.NeighborhoodDask.track_peakmem_mean('480km', 'time_chunks')
4.52±0s 4.51±0.01s 1 mpas_ocean.NeighborhoodReduce.time_dataset_reduce('120km', 'mean')
4.63±0s 4.64±0.01s 1 mpas_ocean.NeighborhoodReduce.time_dataset_reduce('120km', 'median')
123±1ms 123±0.9ms 1 mpas_ocean.NeighborhoodReduce.time_dataset_reduce('480km', 'mean')
123±0.1ms 124±0.6ms 1 mpas_ocean.NeighborhoodReduce.time_dataset_reduce('480km', 'median')
509±0.7ms 509±1ms 1 mpas_ocean.NeighborhoodReduce.time_neighborhood_reduce('120km', 'mean')
550±4ms 542±2ms 0.98 mpas_ocean.NeighborhoodReduce.time_neighborhood_reduce('120km', 'median')
16.7±0.03ms 16.7±0.01ms 0.99 mpas_ocean.NeighborhoodReduce.time_neighborhood_reduce('480km', 'mean')
17.0±0.06ms 17.0±0.02ms 1 mpas_ocean.NeighborhoodReduce.time_neighborhood_reduce('480km', 'median')
4.55±0.02ms 4.50±0.01ms 0.99 mpas_ocean.NeighborhoodReduce.time_reduce('120km', 'mean')
37.1±0.2ms 37.2±0.3ms 1 mpas_ocean.NeighborhoodReduce.time_reduce('120km', 'median')
242±7μs 240±10μs 0.99 mpas_ocean.NeighborhoodReduce.time_reduce('480km', 'mean')
537±4μs 535±10μs 1 mpas_ocean.NeighborhoodReduce.time_reduce('480km', 'median')
234k 234k 1 mpas_ocean.NeighborhoodReduce.track_peakmem_reduce('120km', 'mean')
235k 235k 1 mpas_ocean.NeighborhoodReduce.track_peakmem_reduce('120km', 'median')
19.5k 19.5k 1 mpas_ocean.NeighborhoodReduce.track_peakmem_reduce('480km', 'mean')
19.5k 19.5k 1 mpas_ocean.NeighborhoodReduce.track_peakmem_reduce('480km', 'median')
315±20μs 328±7μs 1.04 mpas_ocean.PointInPolygon.time_face_search_lonlat('120km')
276±20μs 290±10μs 1.05 mpas_ocean.PointInPolygon.time_face_search_lonlat('480km')
292±8μs 268±20μs 0.92 mpas_ocean.PointInPolygon.time_face_search_xyz('120km')
266±10μs 257±20μs 0.97 mpas_ocean.PointInPolygon.time_face_search_xyz('480km')
111±0.7ms 111±0.9ms 1 mpas_ocean.RemapDownsample.time_bilinear_remapping
17.4±0.5ms 17.2±0.06ms 0.99 mpas_ocean.RemapDownsample.time_inverse_distance_weighted_remapping
15.9±0.2ms 16.0±0.2ms 1.01 mpas_ocean.RemapDownsample.time_nearest_neighbor_remapping
1.10±0s 1.12±0s 1.02 mpas_ocean.RemapUpsample.time_bilinear_remapping
27.2±0.3ms 26.0±0.2ms 0.96 mpas_ocean.RemapUpsample.time_inverse_distance_weighted_remapping
11.9±0.09ms 11.5±0.06ms 0.97 mpas_ocean.RemapUpsample.time_nearest_neighbor_remapping
8.75±0.2ms 8.54±0.2ms 0.98 mpas_ocean.ZonalAverage.time_zonal_average('120km')
5.40±0.1ms 5.37±0.05ms 0.99 mpas_ocean.ZonalAverage.time_zonal_average('480km')
344M 368M 1.07 mpas_ocean.ZonalAveragePeakMem.track_peakmem_zonal_average('120km')
1.0298443723640172 1.031732183402152 1 nogil_scaling.GILScaling.track_gil_scaling
7.75±0.2ms 7.50±0.06ms 0.97 quad_hexagon.QuadHexagon.time_open_dataset
6.32±0.2ms 6.40±0.08ms 1.01 quad_hexagon.QuadHexagon.time_open_grid
408 408 1 quad_hexagon.QuadHexagon.track_nbytes_open_dataset
392 392 1 quad_hexagon.QuadHexagon.track_nbytes_open_grid
73.5k 72.5k 0.99 quad_hexagon.QuadHexagon.track_peakmem_open_dataset
72.7k 71.6k 0.99 quad_hexagon.QuadHexagon.track_peakmem_open_grid
1.04±0.01s 1.04±0.01s 1.01 to_raster.ToRaster.time_to_raster((10000.0, 10.0))
2.61±0s 2.61±0.01s 1 to_raster.ToRaster.time_to_raster((10000.0, 100.0))
2.98±0.01s 2.98±0.01s 1 to_raster.ToRaster.time_to_raster((100000.0, 100.0))
271M 271M 1 to_raster.ToRaster.track_peakmem((10000.0, 10.0))
684M 684M 1 to_raster.ToRaster.track_peakmem((10000.0, 100.0))
768M 768M 1 to_raster.ToRaster.track_peakmem((100000.0, 100.0))

Benchmarks that have got worse:

Change Before [b3896f4] After [a3c427e] Ratio Benchmark (Parameter)
+ 321M 366M 1.14 face_bounds.FaceBoundsColdStartRss.track_peakmem_open_and_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
+ 323M 366M 1.14 face_bounds.FaceBoundsColdStartRss.track_peakmem_open_and_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
+ 323M 366M 1.13 face_bounds.FaceBoundsColdStartRss.track_peakmem_open_and_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
+ 323M 366M 1.13 face_bounds.FaceBoundsColdStartRss.track_peakmem_open_and_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
+ 280M 368M 1.31 import.Imports.track_peakmem_import_uxarray
+ 326M 368M 1.13 mpas_ocean.CrossSectionsPeakMem.track_peakmem_const_lat('480km', 1)
+ 326M 368M 1.13 mpas_ocean.CrossSectionsPeakMem.track_peakmem_const_lat('480km', 2)
+ 326M 368M 1.13 mpas_ocean.CrossSectionsPeakMem.track_peakmem_const_lat('480km', 4)
+ 317M 366M 1.15 mpas_ocean.GradientColdStartRss.track_peakmem_gradient('480km')
+ 327M 368M 1.12 mpas_ocean.ZonalAveragePeakMem.track_peakmem_zonal_average('480km')

The ring loop queried the kd-tree once per radius for the whole disc,
then sorted it and ran setdiff1d against the previous disc, so its cost
grew with rings x faces inside. At 60 degrees on oQU120 it was 93% of
the runtime. One query at the outermost circle now finds every candidate
face, and each is binned by its squared chord distance, summed in the
kd-tree's own order.

The tree accepts or rejects whole nodes on distance bounds that carry
their own rounding, so within an ulp or so of a circle it can disagree
with d**2 <= r**2, in either direction. Faces within 1e-12 of a ring's
edge are settled by querying the tree at that radius, as the per-radius
loop did, which keeps ring membership identical. Checked against the
old loop on oQU480, oQU120 and the dyamond subset over 4050 cases,
15,000 of them exact ties on a ring's edge; results are bit-identical
to the previous commit.

The AzimuthalAverage benchmark now runs out to 60 degrees (61 rings),
where the ring faces rather than per-call overhead set the cost:

  480km  141 ms (before 833840d) -> 1.8 ms (833840d) -> 0.5 ms
  120km  169 ms                   -> 10.1 ms           -> 1.6 ms

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cmdupuis3 cmdupuis3 self-assigned this Oct 23, 2026
@cmdupuis3 cmdupuis3 removed the run-benchmark Run ASV benchmark workflow label Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

accuracy Related to mathematical accuracy of operations, such as numerical precision or rounding errors scalability Related to scalability & performance efforts

Projects

None yet

Development

Successfully merging this pull request may close these issues.

azimuthal_mean builds a full subgrid for every ring

1 participant