Skip to content

Avoid tiny numpy arrays in numba: optimize intersections.py (and refactor: "un-scalarize") - #1727

Open
Sevans711 wants to merge 9 commits into
mainfrom
sevans/tinynumpynumba-optimize-intersections.py
Open

Avoid tiny numpy arrays in numba: optimize intersections.py (and refactor: "un-scalarize")#1727
Sevans711 wants to merge 9 commits into
mainfrom
sevans/tinynumpynumba-optimize-intersections.py

Conversation

@Sevans711

@Sevans711 Sevans711 commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Closes #1726 (sub-issue of #1648)

Overview

Optimizes numba routines in uxarray/grid/intersections.py to avoid constructing many tiny numpy arrays inside numba routines, as discussed in #1648. ASV benchmarks seem to show roughly 2x speedup of intersections algorithms, and no performance degradations. Hard to know for sure how gca_gca intersection performance has been affected because those benchmarks are failing on main right now (see discussion on #1688 for details), but the results here at least seem to be the right order of magnitude (roughly 1 μs; latest successes of these benchmarks on main were roughly 1 to 1.5μs).

Additionally, refactors the intersections.py routines to restore prior behavior (from before #1688 merged) of using 3-vector inputs and outputs where possible. To maintain performance improvements, just use tuples instead of allocating tiny numpy arrays. The discussion in PR 1688 clarified the intent was to restore to the pre-scalarized function interfaces where possible without sacrificing performance.

A few changes are within scope of the original issue but the reasoning is not necessarily obvious directly from inspecting the code diff; clarifying here:

  • Previously, gca_gca_intersection output (len(result) == number of intersections) did not match its docstring, which promised shape (2,3) result but filling unused rows with NaNs. This PR updates the implementation to fill unused rows with NaNs, and always return a tuple of 2 length-3 tuples, like ((x1,y1,z1),(x2,y2,z2)), filling with NaNs to represent nonexistent intersection points. (The consistent shape is necessary for the performance optimization here. Numba complains that it "Can't unify return type" if the output length depends on the number of intersections.) The test suite has been updated accordingly, to check for nans instead of len(result).
    • Also updated the docstring of get_number_of_intersections to clarify that it could now be applied to results from gca_gca_intersection too.
  • Removes scalarized versions of methods where non-scalarized versions now suffice. I.e., removed _accux_constlat_scalar (use _accux_constlat instead), and _snap_const_lat_endpoint_xy (use _snap_const_lat_endpoint instead). By using tuples instead of tiny numpy arrays, the "non-scalar" versions also avoid allocation costs, so there is no performance-related need to continue maintaining the scalarized versions.
  • Similarly, updated arcs.py, removing _on_minor_arc_xyz by moving its logic into on_minor_arc, since the scalarized version wasn't being used anywhere anymore (except in benchmarking suite, which has been updated appropriately to use on_minor_arc). Updated on_minor_arc docstring to clarify inputs don't need to be numpy arrays; tuples of length 3 also work just fine.
  • Improved various docstrings, updating docstrings where needed to explain new behavior, but also adding to existing docstrings to clarify meanings and expectations about inputs and outputs, and removing now-obsolete notes about preferring scalarized versions.

EDIT: discussions below revealed the need to incorporate the following changes, too:

  • update the geometry_samebody.py and geometry_samebody_gcagca.py benchmarks files to keep the fp64 function performance in line with the optimizations to the corresponding functions from uxarray.
    • While doing that change, I also noticed and fixed: _fp64_try_gca_gca_intersection was using "and" instead of multiplying together 0s and 1s, so fp64 vs accux wasn't actually the only difference between it and intersections.py's _try_gca_gca_intersection.

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 (_)
  • [N/A] If touched any notebook files, cleared the output of all cells before committing
  • [N/A] If added new notebook files, put into appropriate directories and referenced in appropriate files

AI Disclosure

AI Usage: GitHub Copilot's inline code suggestions, plus some Claude for numba debugging questions

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

undo many changes from #1688 because tuples are easier to read than scalars, and maintain all performance improvements. (There was some intent there to refactor back to vectors, but it got merged before that happened, with the intent to refactor being pushed as something to be handled later.)

Also optimizes to avoid creating numpy array of shape (2,3) for every gca_gca_intersection() call.

And, updates gca_gca_intersection() output to always be a tuple of 2 length-3 tuples, filling with NaNs, as promised in the docstring, instead of a numpy array with shape depending on number of intersections.
and other relevant intersections methods. Removes scalarized versions of related methods: _accux_constlat_scalar, _snap_const_lat_endpoint_xy, _on_minor_arc_xyz,  (if you really want to provide a bunch of scalars, just convert to tuples at call site; using tuples has the same performance as a scalarized implementation but it means there is no longer any need to maintain two versions of each method.)
@Sevans711 Sevans711 self-assigned this Sep 1, 2026
@Sevans711 Sevans711 added scalability Related to scalability & performance efforts run-benchmark Run ASV benchmark workflow labels Sep 1, 2026
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

ASV Benchmarking

Benchmark Comparison Results

Benchmarks that have improved:

Change Before [f856f6d] After [015ac44] Ratio Benchmark (Parameter)
- 1.40±0.03μs 596±10ns 0.43 geometry_kernels.GCAConstLatIntersection.time_accux_constlat_kernel
- 1.03±0.01μs 656±10ns 0.64 geometry_kernels.GCAConstLatIntersection.time_gca_const_lat_intersection
- 1.73±0.02μs 727±30ns 0.42 geometry_kernels.GCAConstLatIntersection.time_try_gca_const_lat_intersection
* failed 771±40ns n/a geometry_kernels.GCAGCAIntersection.time_accux_gca_kernel
* failed 837±30ns n/a geometry_kernels.GCAGCAIntersection.time_gca_gca_intersection
* failed 922±30ns n/a geometry_kernels.GCAGCAIntersection.time_try_gca_gca_intersection
* failed 3.12±0ms n/a geometry_samebody.SameBodyConstLat.time_accux_dispatch
* failed 1.34±0.01ms n/a geometry_samebody.SameBodyConstLat.time_accux_kernel
* failed 2.43±0.01ms n/a geometry_samebody.SameBodyConstLat.time_fp64_dispatch
* failed 671±7μs n/a geometry_samebody.SameBodyConstLat.time_fp64_kernel
* failed 28.0±0.05ms n/a geometry_samebody_gcagca.SameBodyGcaGca.time_accux_dispatch
* failed 6.81±0ms n/a geometry_samebody_gcagca.SameBodyGcaGca.time_accux_kernel
* failed 23.8±0.4ms n/a geometry_samebody_gcagca.SameBodyGcaGca.time_fp64_dispatch
* failed 885±3μs n/a geometry_samebody_gcagca.SameBodyGcaGca.time_fp64_kernel

Benchmarks that have stayed the same:

Change Before [f856f6d] After [015ac44] Ratio Benchmark (Parameter)
201±1ms 210±10ms 1.05 bench_connectivity.Connectivity.time_edge_face('120km')
12.3±0.1ms 12.1±0.07ms 0.98 bench_connectivity.Connectivity.time_edge_face('480km')
203±4ms 197±0.8ms 0.97 bench_connectivity.Connectivity.time_edge_node('120km')
11.8±0.5ms 11.3±0.08ms 0.96 bench_connectivity.Connectivity.time_edge_node('480km')
197±2ms 198±1ms 1.01 bench_connectivity.Connectivity.time_face_edge('120km')
11.5±0.1ms 11.3±0.07ms 0.99 bench_connectivity.Connectivity.time_face_edge('480km')
792±7ms 777±3ms 0.98 bench_connectivity.Connectivity.time_face_face('120km')
49.9±1ms 48.5±0.2ms 0.97 bench_connectivity.Connectivity.time_face_face('480km')
66.1±3μs 63.0±3μs 0.95 bench_connectivity.Connectivity.time_face_node('120km')
62.2±3μs 57.9±2μs 0.93 bench_connectivity.Connectivity.time_face_node('480km')
385±10μs 374±7μs 0.97 bench_connectivity.Connectivity.time_n_nodes_per_face('120km')
308±4μs 301±7μs 0.98 bench_connectivity.Connectivity.time_n_nodes_per_face('480km')
200±2ms 200±2ms 1.00 bench_connectivity.Connectivity.time_node_edge('120km')
11.5±0.08ms 11.5±0.08ms 1.00 bench_connectivity.Connectivity.time_node_edge('480km')
86.0±9ms 80.2±1ms 0.93 bench_connectivity.Connectivity.time_node_face('120km')
4.76±0.02ms 4.77±0.02ms 1.00 bench_connectivity.Connectivity.time_node_face('480km')
6.72±0.04ms 7.41±0.06ms ~1.10 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
2.37±0.04ms 2.36±0.09ms 1.00 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
10.4±10s 9.44±10ms ~0.00 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
1.99±0.02ms 1.95±0.01ms 0.98 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
57.3k 57.3k 1.00 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.00 face_bounds.FaceBounds.track_nbytes_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
123k 123k 1.00 face_bounds.FaceBounds.track_nbytes_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
128 128 1.00 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.00 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.00 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.00 face_bounds.FaceBounds.track_nbytes_grid_with_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
712 712 1.00 face_bounds.FaceBounds.track_nbytes_grid_with_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
1.97M 1.99M 1.01 face_bounds.FaceBounds.track_peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
1.97M 2M 1.01 face_bounds.FaceBounds.track_peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
2.14M 2.16M 1.01 face_bounds.FaceBounds.track_peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
38.3k 38.3k 1.00 face_bounds.FaceBounds.track_peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
336M 336M 1.00 face_bounds.FaceBoundsColdStartRss.peakmem_open_and_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
366M 368M 1.01 face_bounds.FaceBoundsColdStartRss.peakmem_open_and_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
338M 338M 1.00 face_bounds.FaceBoundsColdStartRss.peakmem_open_and_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
337M 338M 1.00 face_bounds.FaceBoundsColdStartRss.peakmem_open_and_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
1.14±0.04μs 1.14±0.03μs 1.00 geometry_kernels.AccucrossKernels.time_accucross
2.55±0.02μs 2.56±0.03μs 1.00 geometry_kernels.AccucrossKernels.time_accucross_pair
411±20ns 391±20ns 0.95 geometry_kernels.EFTPrimitives.time_acc_sqrt_re
390±10ns 385±20ns 0.99 geometry_kernels.EFTPrimitives.time_diff_of_products
381±9ns 350±9ns 0.92 geometry_kernels.EFTPrimitives.time_two_prod
340±0.5ns 376±30ns ~1.10 geometry_kernels.EFTPrimitives.time_two_sum
37.0±0.3μs 36.7±0.6μs 0.99 geometry_kernels.OrientPredicates.time_on_minor_arc
681±10ns 711±30ns 1.04 geometry_kernels.OrientPredicates.time_orient3d_on_sphere
790±6ms 794±5ms 1.01 import.Imports.timeraw_import_uxarray
293M 295M 1.01 import.Imports.track_peakmem_import_uxarray
2.26±0.01ms 2.24±0.01ms 0.99 mpas_ocean.CheckNorm.time_check_norm('120km')
1.80±0.01ms 1.80±0.01ms 1.00 mpas_ocean.CheckNorm.time_check_norm('480km')
694±5ms 696±5ms 1.00 mpas_ocean.ConnectivityConstruction.time_face_face_connectivity('120km')
43.7±0.2ms 43.7±0.2ms 1.00 mpas_ocean.ConnectivityConstruction.time_face_face_connectivity('480km')
615±10μs 617±20μs 1.00 mpas_ocean.ConnectivityConstruction.time_n_nodes_per_face('120km')
517±10μs 551±10μs 1.07 mpas_ocean.ConnectivityConstruction.time_n_nodes_per_face('480km')
5.02±0.02ms 5.03±0.03ms 1.00 mpas_ocean.ConstructFaceLatLon.time_cartesian_averaging('120km')
3.43±0.03ms 3.47±0.06ms 1.01 mpas_ocean.ConstructFaceLatLon.time_cartesian_averaging('480km')
102±0.5ms 103±1ms 1.01 mpas_ocean.ConstructFaceLatLon.time_welzl('120km')
10.2±0.04ms 9.97±0.2ms 0.98 mpas_ocean.ConstructFaceLatLon.time_welzl('480km')
19.7±0.04ms 19.7±0.03ms 1.00 mpas_ocean.ConstructTreeStructures.time_ball_tree('120km')
1.12±0.01ms 1.11±0.04ms 1.00 mpas_ocean.ConstructTreeStructures.time_ball_tree('480km')
10.6±0.02ms 10.6±0.03ms 1.00 mpas_ocean.ConstructTreeStructures.time_kd_tree('120km')
755±10μs 756±40μs 1.00 mpas_ocean.ConstructTreeStructures.time_kd_tree('480km')
521±2ms 530±7ms 1.02 mpas_ocean.CrossSections.time_const_lat('120km', 1)
264±0.6ms 269±2ms 1.02 mpas_ocean.CrossSections.time_const_lat('120km', 2)
137±0.4ms 140±1ms 1.02 mpas_ocean.CrossSections.time_const_lat('120km', 4)
469±3ms 466±2ms 0.99 mpas_ocean.CrossSections.time_const_lat('480km', 1)
236±0.7ms 236±0.7ms 1.00 mpas_ocean.CrossSections.time_const_lat('480km', 2)
122±0.5ms 121±2ms 1.00 mpas_ocean.CrossSections.time_const_lat('480km', 4)
357M 357M 1.00 mpas_ocean.CrossSectionsPeakMem.peakmem_const_lat('120km', 1)
357M 357M 1.00 mpas_ocean.CrossSectionsPeakMem.peakmem_const_lat('120km', 2)
357M 357M 1.00 mpas_ocean.CrossSectionsPeakMem.peakmem_const_lat('120km', 4)
340M 342M 1.01 mpas_ocean.CrossSectionsPeakMem.peakmem_const_lat('480km', 1)
340M 340M 1.00 mpas_ocean.CrossSectionsPeakMem.peakmem_const_lat('480km', 2)
339M 340M 1.00 mpas_ocean.CrossSectionsPeakMem.peakmem_const_lat('480km', 4)
22.9±0.1ms 23.0±0.3ms 1.00 mpas_ocean.DualMesh.time_dual_mesh_construction('120km')
2.64±0.05ms 2.60±0.01ms 0.98 mpas_ocean.DualMesh.time_dual_mesh_construction('480km')
13.7±0.09ms 14.1±0.07ms 1.03 mpas_ocean.FaceAreas.time_face_areas('120km')
3.72±0.09ms 3.75±0.04ms 1.01 mpas_ocean.FaceAreas.time_face_areas('480km')
229k 229k 1.00 mpas_ocean.FaceAreas.track_nbytes_face_areas('120km')
14.3k 14.3k 1.00 mpas_ocean.FaceAreas.track_nbytes_face_areas('480km')
2.12M 2.12M 1.00 mpas_ocean.FaceAreas.track_peakmem_face_areas('120km')
743k 743k 1.00 mpas_ocean.FaceAreas.track_peakmem_face_areas('480km')
837±9ms 835±4ms 1.00 mpas_ocean.GeoDataFrame.time_to_geodataframe('120km', False)
49.9±1ms 52.7±1ms 1.06 mpas_ocean.GeoDataFrame.time_to_geodataframe('120km', True)
71.7±0.6ms 73.6±0.2ms 1.03 mpas_ocean.GeoDataFrame.time_to_geodataframe('480km', False)
5.55±0.03ms 5.85±0.1ms 1.05 mpas_ocean.GeoDataFrame.time_to_geodataframe('480km', True)
14.8±0.09ms 15.0±0.04ms 1.02 mpas_ocean.Gradient.time_gradient('120km')
2.02±0.03ms 2.03±0.03ms 1.00 mpas_ocean.Gradient.time_gradient('480km')
457k 457k 1.00 mpas_ocean.Gradient.track_nbytes_gradient('120km')
28.7k 28.7k 1.00 mpas_ocean.Gradient.track_nbytes_gradient('480km')
3.43M 3.43M 1.00 mpas_ocean.Gradient.track_peakmem_gradient('120km')
218k 218k 1.00 mpas_ocean.Gradient.track_peakmem_gradient('480km')
352M 352M 1.00 mpas_ocean.GradientColdStartRss.peakmem_gradient('120km')
331M 333M 1.01 mpas_ocean.GradientColdStartRss.peakmem_gradient('480km')
365±5μs 377±20μs 1.03 mpas_ocean.HoleEdgeIndices.time_construct_hole_edge_indices('120km')
222±20μs 203±10μs 0.92 mpas_ocean.HoleEdgeIndices.time_construct_hole_edge_indices('480km')
594±30μs 583±7μs 0.98 mpas_ocean.Integrate.time_integrate('120km')
493±20μs 491±10μs 1.00 mpas_ocean.Integrate.time_integrate('480km')
18.4M 18.4M 1.00 mpas_ocean.Integrate.track_nbytes_integrate('120km')
1.2M 1.2M 1.00 mpas_ocean.Integrate.track_nbytes_integrate('480km')
189±1ms 188±1ms 0.99 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'exclude')
187±1ms 196±3ms 1.05 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'include')
185±1ms 189±2ms 1.02 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'split')
14.1±0.09ms 14.2±0.1ms 1.01 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'exclude')
14.0±0.06ms 14.0±0.3ms 1.00 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'include')
14.4±0.08ms 14.0±0.4ms 0.97 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'split')
256±0.2ms 257±0.5ms 1.00 mpas_ocean.NeighborhoodBuild.time_build('120km', 1.0)
1.37±0s 1.38±0s 1.00 mpas_ocean.NeighborhoodBuild.time_build('120km', 15.0)
537±5ms 535±2ms 1.00 mpas_ocean.NeighborhoodBuild.time_build('120km', 5.0)
14.0±0.02ms 14.0±0.04ms 1.00 mpas_ocean.NeighborhoodBuild.time_build('480km', 1.0)
27.0±0.07ms 27.3±0.1ms 1.01 mpas_ocean.NeighborhoodBuild.time_build('480km', 15.0)
17.4±0.03ms 17.4±0.03ms 1.00 mpas_ocean.NeighborhoodBuild.time_build('480km', 5.0)
252±0.3ms 253±0.2ms 1.00 mpas_ocean.NeighborhoodBuild.time_query_radius('120km', 1.0)
1.36±0s 1.37±0.01s 1.01 mpas_ocean.NeighborhoodBuild.time_query_radius('120km', 15.0)
528±1ms 529±1ms 1.00 mpas_ocean.NeighborhoodBuild.time_query_radius('120km', 5.0)
13.7±0.02ms 13.7±0.02ms 1.00 mpas_ocean.NeighborhoodBuild.time_query_radius('480km', 1.0)
26.3±0.09ms 26.3±0.09ms 1.00 mpas_ocean.NeighborhoodBuild.time_query_radius('480km', 15.0)
17.0±0.05ms 17.0±0.04ms 1.00 mpas_ocean.NeighborhoodBuild.time_query_radius('480km', 5.0)
1.19 1.19 1.00 mpas_ocean.NeighborhoodBuild.track_mean_neighbors('120km', 1.0)
612.76 612.76 1.00 mpas_ocean.NeighborhoodBuild.track_mean_neighbors('120km', 15.0)
74.17 74.17 1.00 mpas_ocean.NeighborhoodBuild.track_mean_neighbors('120km', 5.0)
1.0 1.0 1.00 mpas_ocean.NeighborhoodBuild.track_mean_neighbors('480km', 1.0)
37.29 37.29 1.00 mpas_ocean.NeighborhoodBuild.track_mean_neighbors('480km', 15.0)
6.57 6.57 1.00 mpas_ocean.NeighborhoodBuild.track_mean_neighbors('480km', 5.0)
728k 728k 1.00 mpas_ocean.NeighborhoodBuild.track_nbytes_neighbors('120km', 1.0)
141M 141M 1.00 mpas_ocean.NeighborhoodBuild.track_nbytes_neighbors('120km', 15.0)
17.4M 17.4M 1.00 mpas_ocean.NeighborhoodBuild.track_nbytes_neighbors('120km', 5.0)
43k 43k 1.00 mpas_ocean.NeighborhoodBuild.track_nbytes_neighbors('480km', 1.0)
563k 563k 1.00 mpas_ocean.NeighborhoodBuild.track_nbytes_neighbors('480km', 15.0)
123k 123k 1.00 mpas_ocean.NeighborhoodBuild.track_nbytes_neighbors('480km', 5.0)
5.72M 5.72M 1.00 mpas_ocean.NeighborhoodBuild.track_peakmem_build('120km', 1.0)
145M 145M 1.00 mpas_ocean.NeighborhoodBuild.track_peakmem_build('120km', 15.0)
21.5M 21.5M 1.00 mpas_ocean.NeighborhoodBuild.track_peakmem_build('120km', 5.0)
362k 362k 1.00 mpas_ocean.NeighborhoodBuild.track_peakmem_build('480km', 1.0)
825k 825k 1.00 mpas_ocean.NeighborhoodBuild.track_peakmem_build('480km', 15.0)
384k 384k 1.00 mpas_ocean.NeighborhoodBuild.track_peakmem_build('480km', 5.0)
48.0±0.6ms 48.4±0.4ms 1.01 mpas_ocean.NeighborhoodDask.time_mean('120km', 'grid_chunks')
24.9±0.08ms 25.0±0.05ms 1.00 mpas_ocean.NeighborhoodDask.time_mean('120km', 'numpy')
43.8±0.5ms 44.8±0.4ms 1.02 mpas_ocean.NeighborhoodDask.time_mean('120km', 'time_chunks')
10.9±0.2ms 11.5±0.04ms 1.06 mpas_ocean.NeighborhoodDask.time_mean('480km', 'grid_chunks')
553±9μs 543±8μs 0.98 mpas_ocean.NeighborhoodDask.time_mean('480km', 'numpy')
7.70±0.07ms 8.06±0.2ms 1.05 mpas_ocean.NeighborhoodDask.time_mean('480km', 'time_chunks')
5.84M 5.83M 1.00 mpas_ocean.NeighborhoodDask.track_peakmem_mean('120km', 'grid_chunks')
2.75M 2.75M 1.00 mpas_ocean.NeighborhoodDask.track_peakmem_mean('120km', 'numpy')
5.68M 5.68M 1.00 mpas_ocean.NeighborhoodDask.track_peakmem_mean('120km', 'time_chunks')
702k 764k 1.09 mpas_ocean.NeighborhoodDask.track_peakmem_mean('480km', 'grid_chunks')
177k 177k 1.00 mpas_ocean.NeighborhoodDask.track_peakmem_mean('480km', 'numpy')
532k 529k 1.00 mpas_ocean.NeighborhoodDask.track_peakmem_mean('480km', 'time_chunks')
13.3±0s 13.4±0.02s 1.01 mpas_ocean.NeighborhoodReduce.time_dataset_reduce('120km', 'mean')
14.7±0.3s 14.1±0.05s 0.96 mpas_ocean.NeighborhoodReduce.time_dataset_reduce('120km', 'median')
244±0.6ms 245±0.7ms 1.01 mpas_ocean.NeighborhoodReduce.time_dataset_reduce('480km', 'mean')
251±0.3ms 253±0.9ms 1.01 mpas_ocean.NeighborhoodReduce.time_dataset_reduce('480km', 'median')
1.42±0s 1.42±0s 1.00 mpas_ocean.NeighborhoodReduce.time_neighborhood_reduce('120km', 'mean')
1.63±0s 1.63±0s 1.00 mpas_ocean.NeighborhoodReduce.time_neighborhood_reduce('120km', 'median')
27.4±0.1ms 27.5±0.1ms 1.00 mpas_ocean.NeighborhoodReduce.time_neighborhood_reduce('480km', 'mean')
28.9±0.04ms 29.3±0.2ms 1.01 mpas_ocean.NeighborhoodReduce.time_neighborhood_reduce('480km', 'median')
44.9±0.07ms 44.9±0.02ms 1.00 mpas_ocean.NeighborhoodReduce.time_reduce('120km', 'mean')
260±0.2ms 260±0.3ms 1.00 mpas_ocean.NeighborhoodReduce.time_reduce('120km', 'median')
616±7μs 697±90μs ~1.13 mpas_ocean.NeighborhoodReduce.time_reduce('480km', 'mean')
2.11±0.02ms 2.13±0.07ms 1.01 mpas_ocean.NeighborhoodReduce.time_reduce('480km', 'median')
239k 239k 1.00 mpas_ocean.NeighborhoodReduce.track_peakmem_reduce('120km', 'mean')
245k 245k 1.00 mpas_ocean.NeighborhoodReduce.track_peakmem_reduce('120km', 'median')
19.4k 19.4k 1.00 mpas_ocean.NeighborhoodReduce.track_peakmem_reduce('480km', 'mean')
19.9k 19.9k 1.00 mpas_ocean.NeighborhoodReduce.track_peakmem_reduce('480km', 'median')
358±6μs 374±6μs 1.04 mpas_ocean.PointInPolygon.time_face_search_lonlat('120km')
348±10μs 348±5μs 1.00 mpas_ocean.PointInPolygon.time_face_search_lonlat('480km')
330±10μs 321±6μs 0.97 mpas_ocean.PointInPolygon.time_face_search_xyz('120km')
313±9μs 317±8μs 1.02 mpas_ocean.PointInPolygon.time_face_search_xyz('480km')
207±2ms 208±1ms 1.00 mpas_ocean.RemapDownsample.time_bilinear_remapping
229±2ms 229±0.9ms 1.00 mpas_ocean.RemapDownsample.time_inverse_distance_weighted_remapping
15.7±0.06ms 15.6±0.04ms 1.00 mpas_ocean.RemapDownsample.time_nearest_neighbor_remapping
1.11±0s 1.11±0s 1.00 mpas_ocean.RemapUpsample.time_bilinear_remapping
36.6±0.2ms 35.9±0.5ms 0.98 mpas_ocean.RemapUpsample.time_inverse_distance_weighted_remapping
11.2±0.1ms 11.3±0.07ms 1.01 mpas_ocean.RemapUpsample.time_nearest_neighbor_remapping
9.60±0.3ms 9.53±0.3ms 0.99 mpas_ocean.ZonalAverage.time_zonal_average('120km')
5.02±0.08ms 5.12±0.2ms 1.02 mpas_ocean.ZonalAverage.time_zonal_average('480km')
358M 358M 1.00 mpas_ocean.ZonalAveragePeakMem.peakmem_zonal_average('120km')
341M 341M 1.00 mpas_ocean.ZonalAveragePeakMem.peakmem_zonal_average('480km')
6.22±0.02ms 6.72±0.2ms 1.08 quad_hexagon.QuadHexagon.time_open_dataset
5.20±0.03ms 5.22±0.01ms 1.00 quad_hexagon.QuadHexagon.time_open_grid
408 408 1.00 quad_hexagon.QuadHexagon.track_nbytes_open_dataset
392 392 1.00 quad_hexagon.QuadHexagon.track_nbytes_open_grid
73.5k 73.8k 1.00 quad_hexagon.QuadHexagon.track_peakmem_open_dataset
72.8k 73k 1.00 quad_hexagon.QuadHexagon.track_peakmem_open_grid

@Sevans711
Sevans711 marked this pull request as ready for review September 2, 2026 18:37
@Sevans711

Copy link
Copy Markdown
Collaborator Author

Marking as ready for review because tests passed and ASV benchmarks look good. Pinging @cmdupuis3 regarding the gca_gca non-constant-latitude benchmarks, since they are failing on main so it is hard to compare. Do you think another intersections-related benchmark needs to be added as part of this PR or is it okay to move forward as-is?

@erogluorhan
erogluorhan removed the request for review from rajeeja September 2, 2026 20:44
@cmdupuis3

Copy link
Copy Markdown
Collaborator

@Sevans711 We should be able to compare it to main commits prior to #1688, the latest of which is 7eff90d. I can do that and report the results here if you'd like.

@Sevans711

Copy link
Copy Markdown
Collaborator Author

@cmdupuis3 Ah, yes if you are able to do that comparison and share results here that would be great!

Comment thread uxarray/grid/intersections.py Outdated

@dylannelson dylannelson left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking around here, one small comment

@dylannelson dylannelson left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everything I ran came through clean. One minor line was fixed. I didn't see any blockers. Hoping tests done with Chris turn out well. Looks good on my end! Thanks!

@cmdupuis3

cmdupuis3 commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

The timings are pretty small, so some wide variability is expected, but this is what I see:

Change Before [7eff90d] <v2026.08.1~2> After [4e34008] <sevans/tinynumpynumba-optimize-intersections.py> Ratio Benchmark (Parameter)
- 667±0.5ns 417±0ns 0.63 geometry_kernels.GCAGCAIntersection.time_gca_gca_intersection
- 1.08±0.06μs 500±20ns 0.46 geometry_kernels.GCAGCAIntersection.time_try_gca_gca_intersection
- 833±40ns 375±0ns 0.45 geometry_kernels.GCAGCAIntersection.time_accux_gca_kernel

@cmdupuis3 cmdupuis3 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All tests pass, and the benchmarks show improvements. I had Claude scan for accuracy divergences and none were found, although LLMs aren't great for that. Considering that this is a sort-of reversion of #1688, that should be expected.

A few benchmarks still need to be migrated though:

  • geometry_samebody.py:270 — import renamed _accux_constlat_scalar → _accux_constlat, but the px, py, nxo, nyo = ... 4-way unpack was kept; the new kernel returns two 3-tuples. TypingError, which takes down the asv class SameBodyConstLat.

  • geometry_samebody.py:292 — res[0, 0] 2D-indexes the now-tuple return of gca_const_lat_intersection.

  • geometry_samebody.py:345 — same 2D indexing in main()'s accuracy cross-check.

Comment thread benchmarks/geometry_samebody_gcagca.py Outdated
@Sevans711

Copy link
Copy Markdown
Collaborator Author

A few benchmarks still need to be migrated though: …

@cmdupuis3 Thank you for reviewing and pointing these out! I now see that they were originally reported as "failed" in the ASV benchmarking suite results comment above. I believe I have fixed all of these with the latest commit.

@Sevans711
Sevans711 requested a review from cmdupuis3 September 4, 2026 20:48

@cmdupuis3 cmdupuis3 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two issues:

  1. geometry_samebody_gcagca.py:274 uses np.max; the const-lat sibling correctly uses np.nanmax at geometry_samebody.py:296. Since a 1-intersection result is now (point, (nan,nan,nan)), np.max returns NaN — and max(0.0, nan) returns 0.0, because NaN loses every comparison.

  2. geometry_samebody_gcagca.py:61 _fp64_try_gca_gca_intersection is plain @njit(cache=True), but production _try_gca_gca_intersection is @njit(cache=True, inline="always", ...). This means the FP64 version will show different behavior in benchmarks, so the FP64 version should have inline="always" as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run-benchmark Run ASV benchmark workflow scalability Related to scalability & performance efforts

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Avoid tiny numpy arrays in numba: optimize intersections.py

3 participants