Skip to content

Make longitude range lazy for lazy Grid constructors - #1791

Open
cmdupuis3 wants to merge 6 commits into
UXARRAY:mainfrom
cmdupuis3:cmd/lazy-lon-range
Open

cmdupuis3 wants to merge 6 commits into
UXARRAY:mainfrom
cmdupuis3:cmd/lazy-lon-range

Conversation

@cmdupuis3

@cmdupuis3 cmdupuis3 commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #1792

Overview

_set_desired_longitude_range does if da.max() > 180 on a lazy reduction, three times (node_lon, edge_lon, face_lon). Re-fires on every Grid construction, so every isel and every copy() pays it again.

The basic idea is to use a memoized xr.where so that we don't need to materialize the whole dimension to view the values that are available per chunk.

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.

cmdupuis3 and others added 3 commits September 25, 2026 18:46
_set_desired_longitude_range decided whether to wrap by asking
lon.max() > 180. On a dask-backed coordinate that reduction is a compute,
and Grid.__init__ calls it -- so opening a chunked grid read and reduced
every longitude array before the caller had asked for anything, and did it
again on every isel and every copy(), each of which builds a new Grid.

The wrap is now xr.where((lon > 180) | (lon < -180), (lon + 180) % 360 - 180,
lon): elementwise, lazy, chunk-parallel, no reduction.

Measured on a synthetic 4M-node UGRID file, chunked at 500k nodes, best of 5:

                       dask computes    wall      tracemalloc peak
    open_grid (before)             3    23.3ms          12.2 MB
    open_grid (after)              2    16.2ms           1.0 MB
    isel      (before)             2
    isel      (after)              1

The computes that remain are a separate site on connectivity rather than
coordinates: _standardize_connectivity's conn.isnull().any() in io/_ugrid.py,
reached twice on the read path, and _slice_face_indices in grid/slice.py
materializing the connectivity it slices by. Neither is touched here.

Three behavioural differences, all from doing this per element rather than
per array.

  * In-range longitudes are now left exactly alone. (lon + 180) - 180 does
    not round-trip, so wrapping the whole array perturbed values that were
    already in range by up to 3e-14 degrees -- in outCSne30, 2.1182935e-14
    became 2.8421709e-14. On the elements that do need wrapping the two forms
    are bit-identical.

  * Longitudes below -180 are normalized. The old test was on the maximum
    alone, so it reached the negative tail only when the same array also held
    a value above 180.

  * Both endpoints are kept, so the interval is the closed [-180, 180].
    Folding 180.0 to -180.0 would match _xyz_to_lonlat_deg, which wraps
    unconditionally into the half-open interval, but it breaks
    antimeridian_face_indices: that reads a face as crossing from the span of
    its longitudes, and a face with one vertex at 180 and the rest near -170
    goes from a span of 350 to a span of 10 and disappears. Caught by
    test_antimeridian_point_on and
    test_to_geodataframe_preserves_antimeridian_faces.

Each variable is wrapped at most once, keyed on the xr.Variable object.
Without that, edge_lat -- which calls this on every property access,
outside its populate guard -- would stack a where layer onto the graph per
access. Keying on the Variable makes the memo
self-invalidating: assigning into _ds replaces that object, so a repopulated
or user-assigned coordinate is wrapped again.

The two Exodus round-trip tests compared a grid against its own
lon -> xyz -> lon reload with assert_allclose(rtol=1e-8), and passed only
because the old whole-array wrap applied to the original the identical
perturbation the reload applies. With the original left alone, the reload's
own error is exposed, and rtol is the wrong instrument for it twice over: a
longitude near zero has no magnitude for a relative tolerance to measure
against (outCSne30 nodes 4372, 4749, 7e-15 degrees apart), and longitude is
periodic, so a node on or one ulp short of the antimeridian reads 180.0 on
the original and -180.0 on the reload -- the same meridian, scored as a
360-degree error (179 nodes of outRLL1deg; outCSne30 nodes 3966, 5155).
Those two assertions now compare the difference modulo 360 to an absolute
tolerance, still ERROR_TOLERANCE. The helper still catches a 1e-7 shift and
still rejects an antipode.

Tier 0.2 of the chunked refactor plan.

Test suite: 962 passed, 1 skipped. test_plot_with_features fails identically
before and after (matplotlib figure size, unrelated).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The elementwise wrap is the right shape for a dask-backed coordinate and the
wrong one for an array already in memory. There was never a compute to defer
on that path, only a scan, and elementwise costs a pass plus ~18 bytes per
node of temporaries -- two bool masks, the arithmetic, the result -- where a
reduction costs a pass and allocates nothing.

That matters because every open_grid/open_dataset call in the benchmark suite
is eager; none pass chunks=. So the previous commit, measured there, was a
regression and nothing else.

Measured on a synthetic 4M-node UGRID file, best of 5. The wrap in isolation:

                              in range         0..360
    old reduction               4.6ms  0MB    36.8ms  64MB
    elementwise only           20.9ms 72MB    38.1ms  72MB
    elementwise + guard         9.3ms  0MB    42.7ms  72MB

and through eager open_grid, where the file read dominates and the peak does
not move at all (180.0 MB in every arm):

                              in range         0..360
    base (cmd/nogil)           145.5ms         181.9ms
    elementwise only           164.4ms         186.1ms
    elementwise + guard        147.9ms         185.9ms

_lon_within_range is a guard, not a decision: when it is true the wrap is the
identity on every element, so skipping it cannot change a value.
test_eager_fast_path_agrees_with_the_where_element_for_element asserts that
directly against the unguarded expression rather than assuming it, over five
inputs including both endpoints, the negative tail and NaN.

The reductions are the thing that made the old code compute, so they are
allowed only where there is nothing to defer -- da.chunks is None. A
dask-backed array skips the guard entirely, which
test_guard_is_skipped_for_dask_backed_arrays pins by asserting the guard does
compute when handed one. The chunked numbers are unchanged: 2 graph
executions, 16.0ms, 1.0 MB peak.

`and` short-circuits, so an array that does need wrapping usually pays a
single max -- the same reduction the old code paid -- before falling through.
The 0..360 column above is that extra max: ~4.6ms on a 190ms open_grid.

Test suite: 973 passed, 1 skipped. test_plot_with_features fails identically
before and after (matplotlib figure size, unrelated).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every other open_grid in the suite is eager, so none of it can see what
Grid construction does to a dask-backed grid -- which is the path the lazy
longitude wrap changed, and the path the rest of the chunked refactor will
keep changing. OpenGridChunked adds time_open_grid and
track_peakmem_open_grid, parametrized over the oQU 480km and 120km meshes
already registered in helpers/_fixtures.py.

Chunks are held at N_CHUNKS=4 per grid dimension, so the graph is the same
shape at both resolutions and only the data under it grows. The file is read
directly rather than through CachedFixtures, because reading it is the
subject.

On MPAS this is where the lazy wrap matters most. node_lon, edge_lon and
face_lon all exist at construction, so the old max() > 180 check ran three
computes per open; the branch runs none. Measured with this benchmark's own
setup, best of 15, against HEAD's tree with coordinates.py taken from
cmd/nogil:

                      time               tracemalloc peak
                  base    branch        base    branch
    480km       60.5ms    56.5ms      3.27MB    3.27MB
    120km       42.3ms    38.0ms      3.53MB    4.02MB

The 120km peak reads higher on the branch, and it is not data. With gc
disabled, building the where graph allocates ~1.9 MB of transient objects,
nearly all in inspect.signature via dask/xarray op dispatch -- the same at
both resolutions and with a single chunk, so it does not scale with the
grid. At 480km it sits under the HDF5 read's own high-water mark; at 120km
the netCDF3 read is cheap enough that it becomes the peak. After a
gc.collect() the branch retains ~30 kB more than base, which is the extra
graph layers. Worth knowing for later steps: this benchmark sees
graph-construction overhead, not only bytes read.

Three things the numbers above depend on:

  * Compare each resolution to its own history, not to the other. The two
    files are different formats -- oQU480.grid.nc is netCDF4/HDF5,
    oQU120.grid.nc is netCDF3 -- and the HDF5 open costs more, so 480km reads
    slower than 120km despite a sixteenth of the data.

  * Each open gets a fresh copy of the chunks dict. match_chunks_to_ugrid
    (core/utils.py) writes the source-format dimension names into the
    dict it is handed, so a reused one gives every sample after the first a
    different argument.

  * The warning "The specified chunks separate the stored chunks" is
    filtered. oQU480 stores layerThickness, ssh and zMid as one chunk of all
    1791 cells, so any n_face chunking splits them and xarray warns once per
    open -- about data variables the grid reader drops.

Checked by calling the class the way asv does (setup(param), then the
time_/track_ methods) under -W error::UserWarning. Not run through asv
itself: its discovery subprocess cannot import uxarray from the uxarray
conda env, where the package is not installed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cmdupuis3 cmdupuis3 self-assigned this Sep 25, 2026
@cmdupuis3 cmdupuis3 added scalability Related to scalability & performance efforts run-benchmark Run ASV benchmark workflow labels Sep 25, 2026
@github-actions

github-actions Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

ASV Benchmarking

Benchmark Comparison Results

Benchmarks that have improved:

Change Before [379c895] <v2026.09.1> After [7b25aeb] Ratio Benchmark (Parameter)
- 348M 275M 0.79 import.Imports.track_peakmem_import_uxarray
- 2 1 0.5 lazy_grid_construction.LazyGridConstruction.track_computes_isel
- 3 2 0.67 lazy_grid_construction.LazyGridConstruction.track_computes_open_grid_chunked
- 75.8±0.8ms 66.5±2ms 0.88 lazy_grid_construction.OpenGridChunked.time_open_grid('120km')
- 2.81M 2.11M 0.75 lazy_grid_construction.OpenGridChunked.track_peakmem_open_grid('120km')
- 348M 313M 0.9 mpas_ocean.GradientColdStartRss.track_peakmem_gradient('480km')

Benchmarks that have stayed the same:

Change Before [379c895] <v2026.09.1> After [7b25aeb] Ratio Benchmark (Parameter)
3.96±0.2ms 3.94±0.1ms 1.00 bench_connectivity.Connectivity.time_edge_face('120km')
1.37±0.01ms 1.34±0.01ms 0.98 bench_connectivity.Connectivity.time_edge_face('480km')
3.28±0.2ms 3.12±0.06ms 0.95 bench_connectivity.Connectivity.time_edge_node('120km')
1.04±0.01ms 1.04±0.01ms 1.00 bench_connectivity.Connectivity.time_edge_node('480km')
3.10±0.07ms 3.16±0.1ms 1.02 bench_connectivity.Connectivity.time_face_edge('120km')
1.05±0.01ms 1.03±0.01ms 0.98 bench_connectivity.Connectivity.time_face_edge('480km')
4.45±0.06ms 4.47±0.09ms 1.01 bench_connectivity.Connectivity.time_face_face('120km')
1.65±0.01ms 1.62±0.01ms 0.98 bench_connectivity.Connectivity.time_face_face('480km')
34.9±1μs 36.9±0.7μs 1.06 bench_connectivity.Connectivity.time_face_node('120km')
33.9±0.7μs 35.8±0.2μs 1.06 bench_connectivity.Connectivity.time_face_node('480km')
355±6μs 353±8μs 0.99 bench_connectivity.Connectivity.time_n_nodes_per_face('120km')
252±4μs 240±2μs 0.95 bench_connectivity.Connectivity.time_n_nodes_per_face('480km')
4.32±0.05ms 4.41±0.03ms 1.02 bench_connectivity.Connectivity.time_node_edge('120km')
1.38±0.02ms 1.36±0.01ms 0.98 bench_connectivity.Connectivity.time_node_edge('480km')
67.5±4ms 68.2±2ms 1.01 bench_connectivity.Connectivity.time_node_face('120km')
3.73±0.06ms 3.75±0.09ms 1.00 bench_connectivity.Connectivity.time_node_face('480km')
5.98±0.09ms 5.93±0.1ms 0.99 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
1.92±0.02ms 1.86±0.03ms 0.97 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
5.77±6s 7.32±9ms ~0.00 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
1.05±0.01ms 1.02±0.02ms 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'))
2.03M 2.03M 1.00 face_bounds.FaceBounds.track_peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
2.03M 2.03M 1.00 face_bounds.FaceBounds.track_peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
2.19M 2.19M 1.00 face_bounds.FaceBounds.track_peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
35.5k 35.5k 1.00 face_bounds.FaceBounds.track_peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
348M 318M 0.91 face_bounds.FaceBoundsColdStartRss.track_peakmem_open_and_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
348M 318M 0.91 face_bounds.FaceBoundsColdStartRss.track_peakmem_open_and_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
348M 319M 0.92 face_bounds.FaceBoundsColdStartRss.track_peakmem_open_and_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
348M 319M 0.92 face_bounds.FaceBoundsColdStartRss.track_peakmem_open_and_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
852±10ns 841±10ns 0.99 geometry_kernels.AccucrossKernels.time_accucross
1.91±0.01μs 1.92±0.01μs 1.01 geometry_kernels.AccucrossKernels.time_accucross_pair
316±5ns 315±9ns 1.00 geometry_kernels.EFTPrimitives.time_acc_sqrt_re
316±9ns 315±5ns 1.00 geometry_kernels.EFTPrimitives.time_diff_of_products
280±20ns 281±7ns 1.00 geometry_kernels.EFTPrimitives.time_two_prod
270±10ns 265±5ns 0.98 geometry_kernels.EFTPrimitives.time_two_sum
466±10ns 466±9ns 1.00 geometry_kernels.GCAConstLatIntersection.time_accux_constlat_kernel
516±10ns 516±10ns 1.00 geometry_kernels.GCAConstLatIntersection.time_gca_const_lat_intersection
551±7ns 531±5ns 0.96 geometry_kernels.GCAConstLatIntersection.time_try_gca_const_lat_intersection
546±10ns 566±10ns 1.04 geometry_kernels.GCAGCAIntersection.time_accux_gca_kernel
676±10ns 641±40ns 0.95 geometry_kernels.GCAGCAIntersection.time_gca_gca_intersection
736±30ns 731±20ns 0.99 geometry_kernels.GCAGCAIntersection.time_try_gca_gca_intersection
28.9±0.08μs 30.8±0.7μs 1.07 geometry_kernels.OrientPredicates.time_on_minor_arc
28.9±0.2μs 29.9±0.8μs 1.04 geometry_kernels.OrientPredicates.time_orient3d_on_sphere
2.41±0ms 2.41±0.01ms 1.00 geometry_samebody.SameBodyConstLat.time_accux_dispatch
1.02±0ms 1.02±0ms 1.00 geometry_samebody.SameBodyConstLat.time_accux_kernel
1.64±0.01ms 1.64±0.01ms 1.00 geometry_samebody.SameBodyConstLat.time_fp64_dispatch
121±0.8μs 121±0.2μs 1.00 geometry_samebody.SameBodyConstLat.time_fp64_kernel
22.8±0.02ms 23.3±0.2ms 1.02 geometry_samebody_gcagca.SameBodyGcaGca.time_accux_dispatch
5.54±0.01ms 5.54±0.01ms 1.00 geometry_samebody_gcagca.SameBodyGcaGca.time_accux_kernel
18.0±0.2ms 18.3±0.5ms 1.02 geometry_samebody_gcagca.SameBodyGcaGca.time_fp64_dispatch
698±3μs 700±5μs 1.00 geometry_samebody_gcagca.SameBodyGcaGca.time_fp64_kernel
717±7ms 729±7ms 1.02 import.Imports.timeraw_import_uxarray
98.1±3ms 92.0±2ms 0.94 lazy_grid_construction.OpenGridChunked.time_open_grid('480km')
2.06M 2.2M 1.07 lazy_grid_construction.OpenGridChunked.track_peakmem_open_grid('480km')
1.58±0.06ms 1.64±0.01ms 1.04 mpas_ocean.CheckNorm.time_check_norm('120km')
1.20±0.01ms 1.21±0.01ms 1.01 mpas_ocean.CheckNorm.time_check_norm('480km')
788±4μs 800±2μs 1.02 mpas_ocean.ConnectivityConstruction.time_face_face_connectivity('120km')
384±7μs 387±10μs 1.01 mpas_ocean.ConnectivityConstruction.time_face_face_connectivity('480km')
526±8μs 516±9μs 0.98 mpas_ocean.ConnectivityConstruction.time_n_nodes_per_face('120km')
421±6μs 422±4μs 1.00 mpas_ocean.ConnectivityConstruction.time_n_nodes_per_face('480km')
3.47±0.07ms 3.59±0.01ms 1.03 mpas_ocean.ConstructFaceLatLon.time_cartesian_averaging('120km')
2.58±0.01ms 2.91±0.06ms ~1.13 mpas_ocean.ConstructFaceLatLon.time_cartesian_averaging('480km')
79.2±0.5ms 79.2±0.5ms 1.00 mpas_ocean.ConstructFaceLatLon.time_welzl('120km')
7.99±0.2ms 7.63±0.3ms 0.95 mpas_ocean.ConstructFaceLatLon.time_welzl('480km')
16.2±0.02ms 16.3±0.04ms 1.00 mpas_ocean.ConstructTreeStructures.time_ball_tree('120km')
854±40μs 831±30μs 0.97 mpas_ocean.ConstructTreeStructures.time_ball_tree('480km')
8.27±0.03ms 8.30±0.07ms 1.00 mpas_ocean.ConstructTreeStructures.time_kd_tree('120km')
485±20μs 499±30μs 1.03 mpas_ocean.ConstructTreeStructures.time_kd_tree('480km')
434±2ms 448±2ms 1.03 mpas_ocean.CrossSections.time_const_lat('120km', 1)
218±2ms 236±0.8ms 1.08 mpas_ocean.CrossSections.time_const_lat('120km', 2)
114±0.3ms 118±1ms 1.03 mpas_ocean.CrossSections.time_const_lat('120km', 4)
371±2ms 411±7ms ~1.11 mpas_ocean.CrossSections.time_const_lat('480km', 1)
187±1ms 200±1ms 1.07 mpas_ocean.CrossSections.time_const_lat('480km', 2)
96.2±0.2ms 102±1ms 1.06 mpas_ocean.CrossSections.time_const_lat('480km', 4)
348M 338M 0.97 mpas_ocean.CrossSectionsPeakMem.track_peakmem_const_lat('120km', 1)
348M 338M 0.97 mpas_ocean.CrossSectionsPeakMem.track_peakmem_const_lat('120km', 2)
348M 338M 0.97 mpas_ocean.CrossSectionsPeakMem.track_peakmem_const_lat('120km', 4)
348M 321M 0.92 mpas_ocean.CrossSectionsPeakMem.track_peakmem_const_lat('480km', 1)
348M 321M 0.92 mpas_ocean.CrossSectionsPeakMem.track_peakmem_const_lat('480km', 2)
348M 321M 0.92 mpas_ocean.CrossSectionsPeakMem.track_peakmem_const_lat('480km', 4)
18.2±0.05ms 18.4±0.2ms 1.01 mpas_ocean.DualMesh.time_dual_mesh_construction('120km')
2.08±0.07ms 2.16±0.04ms 1.04 mpas_ocean.DualMesh.time_dual_mesh_construction('480km')
11.7±0.5ms 11.3±0.7ms 0.96 mpas_ocean.FaceAreas.time_face_areas('120km')
2.99±0.3ms 2.91±0.3ms 0.98 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')
728k 728k 1.00 mpas_ocean.FaceAreas.track_peakmem_face_areas('480km')
647±1ms 646±2ms 1.00 mpas_ocean.GeoDataFrame.time_to_geodataframe('120km', False)
39.3±0.9ms 39.3±1ms 1.00 mpas_ocean.GeoDataFrame.time_to_geodataframe('120km', True)
57.1±0.09ms 57.1±0.3ms 1.00 mpas_ocean.GeoDataFrame.time_to_geodataframe('480km', False)
3.94±0.3ms 4.11±0.1ms 1.04 mpas_ocean.GeoDataFrame.time_to_geodataframe('480km', True)
11.4±0.4ms 11.3±0.1ms 0.99 mpas_ocean.Gradient.time_gradient('120km')
1.31±0.01ms 1.30±0.01ms 0.99 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.2M 3.2M 1.00 mpas_ocean.Gradient.track_peakmem_gradient('120km')
204k 204k 1.00 mpas_ocean.Gradient.track_peakmem_gradient('480km')
348M 333M 0.96 mpas_ocean.GradientColdStartRss.track_peakmem_gradient('120km')
200±2μs 208±2μs 1.04 mpas_ocean.HoleEdgeIndices.time_construct_hole_edge_indices('120km')
99.3±1μs 104±3μs 1.05 mpas_ocean.HoleEdgeIndices.time_construct_hole_edge_indices('480km')
142±7μs 143±1μs 1.00 mpas_ocean.Integrate.time_integrate('120km')
126±2μs 129±2μs 1.02 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')
149±1ms 150±0.5ms 1.00 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'exclude')
150±0.4ms 150±1ms 1.00 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'include')
149±1ms 152±0.8ms 1.02 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'split')
10.7±0.2ms 10.8±0.1ms 1.01 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'exclude')
10.5±0.1ms 10.7±0.1ms 1.02 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'include')
10.6±0.08ms 11.0±0.2ms 1.05 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'split')
212±0.5ms 211±0.4ms 1.00 mpas_ocean.NeighborhoodBuild.time_build('120km', 1.0)
1.11±0s 1.12±0s 1.00 mpas_ocean.NeighborhoodBuild.time_build('120km', 15.0)
438±1ms 436±1ms 1.00 mpas_ocean.NeighborhoodBuild.time_build('120km', 5.0)
11.4±0.06ms 11.5±0.07ms 1.01 mpas_ocean.NeighborhoodBuild.time_build('480km', 1.0)
21.6±0.06ms 21.6±0.2ms 1.00 mpas_ocean.NeighborhoodBuild.time_build('480km', 15.0)
14.1±0.07ms 14.1±0.05ms 1.00 mpas_ocean.NeighborhoodBuild.time_build('480km', 5.0)
206±0.4ms 211±4ms 1.02 mpas_ocean.NeighborhoodBuild.time_query_radius('120km', 1.0)
1.10±0s 1.13±0.02s 1.02 mpas_ocean.NeighborhoodBuild.time_query_radius('120km', 15.0)
433±1ms 440±9ms 1.02 mpas_ocean.NeighborhoodBuild.time_query_radius('120km', 5.0)
11.1±0.02ms 11.5±0.3ms 1.04 mpas_ocean.NeighborhoodBuild.time_query_radius('480km', 1.0)
21.2±0.04ms 22.2±0.8ms 1.05 mpas_ocean.NeighborhoodBuild.time_query_radius('480km', 15.0)
13.7±0.06ms 14.4±0.4ms 1.05 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)
37.9±0.4ms 38.4±0.7ms 1.01 mpas_ocean.NeighborhoodDask.time_mean('120km', 'grid_chunks')
19.4±0.1ms 19.4±0.09ms 1.00 mpas_ocean.NeighborhoodDask.time_mean('120km', 'numpy')
34.7±0.5ms 34.9±0.4ms 1.00 mpas_ocean.NeighborhoodDask.time_mean('120km', 'time_chunks')
8.38±0.2ms 8.70±0.07ms 1.04 mpas_ocean.NeighborhoodDask.time_mean('480km', 'grid_chunks')
462±3μs 470±10μs 1.02 mpas_ocean.NeighborhoodDask.time_mean('480km', 'numpy')
6.27±0.1ms 6.20±0.04ms 0.99 mpas_ocean.NeighborhoodDask.time_mean('480km', 'time_chunks')
5.84M 5.84M 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.69M 5.68M 1.00 mpas_ocean.NeighborhoodDask.track_peakmem_mean('120km', 'time_chunks')
686k 675k 0.98 mpas_ocean.NeighborhoodDask.track_peakmem_mean('480km', 'grid_chunks')
177k 177k 1.00 mpas_ocean.NeighborhoodDask.track_peakmem_mean('480km', 'numpy')
546k 542k 0.99 mpas_ocean.NeighborhoodDask.track_peakmem_mean('480km', 'time_chunks')
10.9±0.01s 10.9±0s 1.00 mpas_ocean.NeighborhoodReduce.time_dataset_reduce('120km', 'mean')
11.6±0.06s 11.5±0.01s 0.99 mpas_ocean.NeighborhoodReduce.time_dataset_reduce('120km', 'median')
195±0.4ms 193±0.5ms 0.99 mpas_ocean.NeighborhoodReduce.time_dataset_reduce('480km', 'mean')
198±0.4ms 197±0.7ms 1.00 mpas_ocean.NeighborhoodReduce.time_dataset_reduce('480km', 'median')
1.15±0s 1.15±0s 1.00 mpas_ocean.NeighborhoodReduce.time_neighborhood_reduce('120km', 'mean')
1.32±0s 1.32±0s 1.00 mpas_ocean.NeighborhoodReduce.time_neighborhood_reduce('120km', 'median')
22.3±0.2ms 22.3±0.1ms 1.00 mpas_ocean.NeighborhoodReduce.time_neighborhood_reduce('480km', 'mean')
23.4±0.2ms 23.5±0.2ms 1.00 mpas_ocean.NeighborhoodReduce.time_neighborhood_reduce('480km', 'median')
33.2±0.1ms 33.1±0.1ms 1.00 mpas_ocean.NeighborhoodReduce.time_reduce('120km', 'mean')
207±0.6ms 207±0.8ms 1.00 mpas_ocean.NeighborhoodReduce.time_reduce('120km', 'median')
526±8μs 495±20μs 0.94 mpas_ocean.NeighborhoodReduce.time_reduce('480km', 'mean')
1.61±0.02ms 1.59±0.01ms 0.98 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')
339±10μs 376±20μs ~1.11 mpas_ocean.PointInPolygon.time_face_search_lonlat('120km')
290±10μs 306±20μs 1.06 mpas_ocean.PointInPolygon.time_face_search_lonlat('480km')
303±20μs 293±10μs 0.97 mpas_ocean.PointInPolygon.time_face_search_xyz('120km')
265±2μs 285±20μs 1.08 mpas_ocean.PointInPolygon.time_face_search_xyz('480km')
96.9±0.3ms 96.2±0.5ms 0.99 mpas_ocean.RemapDownsample.time_bilinear_remapping
13.7±0.2ms 13.9±0.1ms 1.01 mpas_ocean.RemapDownsample.time_inverse_distance_weighted_remapping
12.1±0.03ms 12.4±0.1ms 1.02 mpas_ocean.RemapDownsample.time_nearest_neighbor_remapping
945±5ms 945±3ms 1.00 mpas_ocean.RemapUpsample.time_bilinear_remapping
21.1±0.07ms 21.2±0.2ms 1.00 mpas_ocean.RemapUpsample.time_inverse_distance_weighted_remapping
8.33±0.02ms 8.69±0.2ms 1.04 mpas_ocean.RemapUpsample.time_nearest_neighbor_remapping
6.80±0.1ms 6.82±0.2ms 1.00 mpas_ocean.ZonalAverage.time_zonal_average('120km')
3.84±0.09ms 3.78±0.04ms 0.98 mpas_ocean.ZonalAverage.time_zonal_average('480km')
348M 339M 0.97 mpas_ocean.ZonalAveragePeakMem.track_peakmem_zonal_average('120km')
348M 323M 0.93 mpas_ocean.ZonalAveragePeakMem.track_peakmem_zonal_average('480km')
1.041602755362027 1.0325505018120167 0.99 nogil_scaling.GILScaling.track_gil_scaling
4.51±0.06ms 4.74±0.2ms 1.05 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
72.8k 73.6k 1.01 quad_hexagon.QuadHexagon.track_peakmem_open_dataset
72.3k 72.7k 1.00 quad_hexagon.QuadHexagon.track_peakmem_open_grid

Benchmarks that have got worse:

Change Before [379c895] <v2026.09.1> After [7b25aeb] Ratio Benchmark (Parameter)
+ 5.22±0.09ms 5.89±0.7ms 1.13 quad_hexagon.QuadHexagon.time_open_dataset

@cmdupuis3
cmdupuis3 marked this pull request as ready for review September 28, 2026 21:02
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.

_set_desired_longitude_range forces materialization in open_grid

1 participant