Skip to content

Support reusing an existing Grid when opening datasets - #1827

Merged
rajeeja merged 9 commits into
UXARRAY:mainfrom
sunxiuguo:fix-grid-reuse-1818
Nov 3, 2026
Merged

rajeeja merged 9 commits into
UXARRAY:mainfrom
sunxiuguo:fix-grid-reuse-1818

Conversation

@sunxiuguo

@sunxiuguo sunxiuguo commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1818

Overview

Allow open_dataset(existing_grid, data) and open_mfdataset(existing_grid, paths) to reuse the same Grid, its topology and cached properties without reconstructing it or sending it to a file backend.

The current implementation preserves the Grid's existing chunking and source-dimension mapping. Data chunks are mapped without mutating the caller's dictionary, including overlapping source/canonical dimension names. Canonical dimensions are preserved when node and face counts coincide. Grid-construction options use_dual=True and nonempty grid_kwargs raise actionable errors for an existing Grid.

The intended scope is unstructured grid/data inputs. Following the maintainer clarification, structured-input-related tests and usage guidance were removed, and the retained reuse tests were consolidated into test/core/test_api.py. Ordinary cases use the existing quad-hexagon grid; a small equal-count topology covers dimension/alias regressions. Future warnings/errors for structured lat/lon inputs are tracked separately in #1854.

The November 3 revision implements the agreed chunking contract: passing an existing Grid, non-None chunks, and chunk_grid=True (including its default) raises ValueError with guidance to call Grid.chunk() explicitly. Both opening APIs reject this combination before opening the data. chunks=None preserves the shared Grid, its current chunking, and caches; chunk_grid=False permits data-only chunking. No shared Grid is silently copied or rechunked. The public API docstrings describe the contract, and the tests now select data-only chunking explicitly rather than hiding that option in a fixture.

The branch merges main through 79b7aa6e999474fb09f1aec8e52303a19a9aaebd, retaining both histories. The sole merge conflict was in uxarray/core/api.py: the Grid-reuse path is preserved together with upstream's removal of deprecated latlon forwarding and its updated helper signature. Upstream changes to the utilities and API tests are retained. Alignment validation and its documentation remain scoped to #1853.

Expected Usage

import uxarray as ux

grid = ux.open_grid("grid.nc")
member_a = ux.open_dataset(
    grid, "member_a.nc", chunks={"n_face": 1000}, chunk_grid=False
)
member_b = ux.open_dataset(
    grid, "member_b.nc", chunks={"n_face": 1000}, chunk_grid=False
)
assert member_a.uxgrid is member_b.uxgrid is grid

In-memory xarray Dataset members and multi-file data are covered as well.

Validation

November 3 local revision

  • Merged-main baseline: 57 Grid-reuse tests passed before the contract change.
  • Real red/green regression: all 24 new rejection cases failed on that baseline because no ValueError was raised. The final reuse group passed 90 tests, including default/explicit chunk_grid=True, chunks=None under either flag, prechunked Grids, data-only chunks, and caller/Grid/cache preservation.
  • Related broader suite: python -m pytest test/core test/io test/grid/grid/test_io.py test/grid/grid/test_initialization.py -q passed 505 tests, with 33 warnings, in 132.90 seconds. This includes the full API tests and the retained upstream drop_variables regression. It is not a full-repository test run.
  • Repository-pinned pre-commit hooks, Ruff 0.15.22 lint/format, and git diff --check passed. All six existing synthetic opening benchmark combinations passed a smoke run; no new ASV performance comparison is claimed.
  • Linux, Python 3.12.14; xarray 2026.9.0, Dask 2026.8.0, NumPy 2.5.4, Numba 0.68.1. Dependencies were resolved from official PyPI into a version/hash lock, with binary-only dependency installation. The reviewed project setuptools backend was used for the local editable install. Conda is unavailable here, so this is a pip/uv environment, not the hosted Conda matrix.
  • Independent static review passed on the complete main-to-candidate diff, the narrow contract patch, merge-conflict resolution, both API execution paths, fixture preservation assertions, and this PR wording. The independent reviewer did not separately execute tests or recompute local hashes; the runtime results above were run by the implementation worker.
  • Published head 723f6d8a273b42aa8fe26ec755ed0cc7048d14cc has tree 51d55bb5a3f1197e8d2299d0522867d70652761e, identical to local tested/reviewed candidate bd1f080e424bd8640e7dd5189e19434c4a58b701. The GitHub connector created the published commit metadata, so the commit SHA differs while the source tree is unchanged. Merge commit 3d33843e745f880595826e621cac7f1e8d368d5b retains both original head d2c2ad9f and main 79b7aa6e as parents. A normal, non-force update was guarded by the expected original head. Remote commit/tree and all four changed files were read back; file bytes, Git blobs, and SHA-256 hashes match the tested source.
  • Hosted status checked on 2026-11-03 at this published head: pre-commit.ci passed. CI run 41666054614 and ASV run 41666054574 both report action_required with zero jobs; they await maintainer approval and have not run successfully. Read the Docs build 35617442 is pending. No new-head hosted full-suite success, Windows success, rendered-documentation-build success, or ASV no-regression result is claimed.

Local review revisions — run on 2026-10-27

  • The docs/tests revision published as 62f4d411 passed 60 targeted tests, 19 deselected: 57 unstructured Grid-reuse cases and 3 unchanged synthetic API tests. The retained reuse group is 295 lines plus 4 import lines, replacing the separate 535-line module.
  • The final docstring-only candidate, subsequently published as d2c2ad9f, also passed 60 targeted tests, 19 deselected (11.95 seconds). Ruff lint/format, docstring-first/debug-statement hooks, whitespace and patch-applicability checks passed; executable AST was unchanged from 62f4d411.
  • Independent review verified the final patch, source/tree hashes, executable-AST invariance, lint/format and applicability, and reviewed that test evidence. It did not perform another runtime test run for the final sentence-only change.
  • These are recorded October 27 runs, not tests rerun for this October 28 body update. No local full-suite, Windows, or rendered-documentation-build success is claimed.

Environment for these local checks: Linux, Python 3.12.14; xarray 2026.9.0, Dask 2026.8.0, NumPy 2.5.4.

Hosted results — checked 2026-10-28

At head d2c2ad9fef5400967cf3d93f9cd64d0ac3aaf1d1, CI run 40573219453 and ASV run 40573219452 both completed successfully on attempt 2.

Successful ASV execution is not a no-regression result. The posted comparison, labeled db33c50b versus 7fd57840, reports increases including 1.36× import peak memory, approximately 1.26× face-bounds cold-start RSS, and 1.18× time_two_prod. These observations are not claimed resolved or attributed to a particular change.

Earlier implementation evidence and limitations

The original PR description recorded reproduction of the reported netCDF TypeError for both filename and Dataset members, 266 tests plus 28 subtests passing, and an independent 56 tests plus 28 subtests with six benchmark smoke cases. Those are historical results, not reruns on the current head. The synthetic benchmark covers all six file/Dataset/Grid input combinations; its tiny-fixture timings are not production-performance claims.

An earlier local full-suite attempt ended with exit 137 and did not complete. A later combined integration check had 26 passes and one failure when the thread-pool test's child process exited with -9; its cause was not established. The subsequent hosted successes above do not retroactively turn those local attempts into passes. Existing UxDataset close-callback behavior is not refactored by this PR.

PR Checklist

General

  • An issue is created and linked
  • [N/A] Added appropriate labels (no label changes requested)
  • Filled out Overview and Expected Usage

Testing & Benchmarking

  • Added regression coverage
  • Reported synthetic benchmark and hosted ASV execution, including limitations and flagged regressions

Documentation and Examples

  • Updated public API docstrings and added helper/benchmark docstrings
  • [N/A] No new public function
  • [N/A] No notebooks changed

AI Disclosure

AI Usage: OpenAI Codex assisted with investigation, implementation, synthetic tests, benchmarking, and independent code review.

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

@Sevans711
Sevans711 self-requested a review October 15, 2026 23:00
@Sevans711 Sevans711 added run-benchmark Run ASV benchmark workflow bug Something isn't working scalability Related to scalability & performance efforts labels Oct 20, 2026
@rajeeja
rajeeja requested review from rajeeja and a balanced review from Copilot October 20, 2026 21:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Structured node-only and edge-only members can crash, and overlapping chunk aliases can produce incorrect chunk sizes.

2 open issues
What changed in this PR

Adds support for reusing an existing Grid across dataset opens, addressing #1818 without rebuilding topology or cached properties.

Changes:

  • Supports Grid inputs in both dataset-opening APIs.
  • Preserves shared-grid state and maps member dimensions and chunks.
  • Adds regression tests, API documentation, and a synthetic benchmark.
File Description
uxarray/​core/​api.py Implements Grid reuse and member mapping.
test/​core/​test_grid_reuse.py Tests reuse, chunking, mapping, and input validation.
benchmarks/​grid_reuse.py Compares rebuilt and reused Grid inputs.

Review effort: Balanced

Comment thread uxarray/core/api.py Outdated
source_dims = grid._source_dims_dict.copy()
if grid.source_grid_spec == "Structured":
lon_dim, lat_dim = source_dims["n_face"]
if "n_face" in ds.dims and lon_dim not in ds.dims and lat_dim not in ds.dims:
Comment thread uxarray/core/api.py Outdated
Comment on lines +662 to +665
chunks = chunks.copy()
for source_dim, ugrid_dim in grid_filename_or_obj._source_dims_dict.items():
if isinstance(ugrid_dim, str) and ugrid_dim in chunks:
chunks[source_dim] = chunks[ugrid_dim]
@Sevans711

Copy link
Copy Markdown
Collaborator

Hi @rajeeja, as the person who requested the github copilot review, could you clarify a bit further how important is it for its review comments to be addressed? I don't think it has been particularly common for recent uxarray PRs to get copilot reviews, and I think it could be especially helpful to clarify here because this PR was drafted by a first-time contributor. E.g., are you just seeing it as a helpful starting point for your own review, or are you hoping its suggestions might be treated with the same level of importance/consideration as comments from a human reviewer?

Sidenote: reviewing this PR is high on my to-do list, I'm hoping to get to it sometime very soon (probably tomorrow or within the next few days)!

@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

ASV Benchmarking

Benchmark Comparison Results

Benchmarks that have improved:

Change Before [79b7aa6] <v2026.10.1> After [f9e402f] Ratio Benchmark (Parameter)
- 2.32M 2.11M 0.91 lazy_grid_construction.OpenGridChunked.track_peakmem_open_grid('480km')
- 755k 676k 0.90 mpas_ocean.NeighborhoodDask.track_peakmem_mean('480km', 'grid_chunks')
* failed 188±10μs n/a open_dataset.OpenDataset.time_open_dataset('grid', 'dataset')
* failed 1.17±0ms n/a open_dataset.OpenDataset.time_open_dataset('grid', 'file')

Benchmarks that have stayed the same:

Change Before [79b7aa6] <v2026.10.1> After [f9e402f] Ratio Benchmark (Parameter)
933±40μs 967±10μs 1.04 connectivity.Connectivity.time_edge_face('120km')
459±10μs 464±6μs 1.01 connectivity.Connectivity.time_edge_face('480km')
6.34±0.7ms 6.34±0.09ms 1.00 connectivity.Connectivity.time_edge_node('120km')
1.23±0.06ms 1.24±0.2ms 1.01 connectivity.Connectivity.time_edge_node('480km')
61.0±3μs 66.5±8μs 1.09 connectivity.Connectivity.time_face_edge('120km')
60.1±4μs 64.1±7μs 1.07 connectivity.Connectivity.time_face_edge('480km')
959±20μs 954±10μs 0.99 connectivity.Connectivity.time_face_face('120km')
402±20μs 377±20μs 0.94 connectivity.Connectivity.time_face_face('480km')
57.4±10μs 57.0±2μs 0.99 connectivity.Connectivity.time_face_node('120km')
60.3±9μs 57.8±2μs 0.96 connectivity.Connectivity.time_face_node('480km')
449±20μs 458±20μs 1.02 connectivity.Connectivity.time_n_nodes_per_face('120km')
379±30μs 386±10μs 1.02 connectivity.Connectivity.time_n_nodes_per_face('480km')
1.38±0.05ms 1.33±0.05ms 0.97 connectivity.Connectivity.time_node_edge('120km')
453±10μs 465±10μs 1.03 connectivity.Connectivity.time_node_edge('480km')
86.0±2ms 88.0±3ms 1.02 connectivity.Connectivity.time_node_face('120km')
5.44±0.04ms 5.61±0.08ms 1.03 connectivity.Connectivity.time_node_face('480km')
1.42M 1.42M 1.00 connectivity.ConnectivityTracemalloc.track_peakmem_edge_face('120km')
105k 105k 1.00 connectivity.ConnectivityTracemalloc.track_peakmem_edge_face('480km')
6.48M 6.48M 1.00 connectivity.ConnectivityTracemalloc.track_peakmem_edge_node('120km')
419k 419k 1.00 connectivity.ConnectivityTracemalloc.track_peakmem_edge_node('480km')
2.05k 2.05k 1.00 connectivity.ConnectivityTracemalloc.track_peakmem_face_edge('120km')
2.05k 2.05k 1.00 connectivity.ConnectivityTracemalloc.track_peakmem_face_edge('480km')
1.6M 1.6M 1.00 connectivity.ConnectivityTracemalloc.track_peakmem_face_face('120km')
101k 101k 1.00 connectivity.ConnectivityTracemalloc.track_peakmem_face_face('480km')
2.18k 2.18k 1.00 connectivity.ConnectivityTracemalloc.track_peakmem_face_node('120km')
2.18k 2.18k 1.00 connectivity.ConnectivityTracemalloc.track_peakmem_face_node('480km')
240k 240k 1.00 connectivity.ConnectivityTracemalloc.track_peakmem_n_nodes_per_face('120km')
26k 26k 1.00 connectivity.ConnectivityTracemalloc.track_peakmem_n_nodes_per_face('480km')
1.9M 1.9M 1.00 connectivity.ConnectivityTracemalloc.track_peakmem_node_edge('120km')
127k 127k 1.00 connectivity.ConnectivityTracemalloc.track_peakmem_node_edge('480km')
11.9M 11.9M 1.00 connectivity.ConnectivityTracemalloc.track_peakmem_node_face('120km')
747k 747k 1.00 connectivity.ConnectivityTracemalloc.track_peakmem_node_face('480km')
8.40±0.05ms 8.12±0.05ms 0.97 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
2.60±0.1ms 2.68±0.1ms 1.03 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
9.86±0.4ms 9.98±9ms 1.01 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
1.52±0.06ms 1.46±0.03ms 0.96 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.83M 1.83M 1.00 face_bounds.FaceBounds.track_peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
1.83M 1.83M 1.00 face_bounds.FaceBounds.track_peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
1.99M 1.99M 1.00 face_bounds.FaceBounds.track_peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
35.3k 35.3k 1.00 face_bounds.FaceBounds.track_peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
1.20±0.03μs 1.20±0.02μs 1.00 geometry_kernels.AccucrossKernels.time_accucross
2.70±0.02μs 2.65±0.04μs 0.99 geometry_kernels.AccucrossKernels.time_accucross_pair
510±30ns 501±20ns 0.98 geometry_kernels.EFTPrimitives.time_acc_sqrt_re
481±30ns 471±20ns 0.98 geometry_kernels.EFTPrimitives.time_diff_of_products
466±50ns 406±10ns ~0.87 geometry_kernels.EFTPrimitives.time_two_prod
441±20ns 461±30ns 1.05 geometry_kernels.EFTPrimitives.time_two_sum
722±40ns 706±10ns 0.98 geometry_kernels.GCAConstLatIntersection.time_accux_constlat_kernel
786±20ns 766±10ns 0.98 geometry_kernels.GCAConstLatIntersection.time_gca_const_lat_intersection
846±20ns 862±20ns 1.02 geometry_kernels.GCAConstLatIntersection.time_try_gca_const_lat_intersection
416±10ns 416±6ns 1.00 geometry_kernels.GCAGCAIntersection.time_accux_gca_kernel
581±9ns 571±8ns 0.98 geometry_kernels.GCAGCAIntersection.time_gca_gca_intersection
651±20ns 641±10ns 0.98 geometry_kernels.GCAGCAIntersection.time_try_gca_gca_intersection
20.4±0.3μs 21.2±0.9μs 1.04 geometry_kernels.OrientPredicates.time_on_minor_arc
20.8±0.7μs 20.7±0.4μs 0.99 geometry_kernels.OrientPredicates.time_orient3d_on_sphere
3.05±0ms 3.08±0.03ms 1.01 geometry_samebody.SameBodyConstLat.time_accux_dispatch
1.17±0.01ms 1.17±0ms 1.00 geometry_samebody.SameBodyConstLat.time_accux_kernel
2.29±0ms 2.33±0.02ms 1.02 geometry_samebody.SameBodyConstLat.time_fp64_dispatch
148±0.2μs 148±4μs 1.00 geometry_samebody.SameBodyConstLat.time_fp64_kernel
29.9±0.3ms 30.3±0.02ms 1.01 geometry_samebody_gcagca.SameBodyGcaGca.time_accux_dispatch
6.29±0.03ms 6.28±0ms 1.00 geometry_samebody_gcagca.SameBodyGcaGca.time_accux_kernel
23.6±0.02ms 24.3±0.02ms 1.03 geometry_samebody_gcagca.SameBodyGcaGca.time_fp64_dispatch
861±8μs 865±20μs 1.00 geometry_samebody_gcagca.SameBodyGcaGca.time_fp64_kernel
943±10ms 945±4ms 1.00 import.Imports.timeraw_import_uxarray
1 1 1.00 lazy_grid_construction.LazyGridConstruction.track_computes_isel
2 2 1.00 lazy_grid_construction.LazyGridConstruction.track_computes_open_grid_chunked
52.5±0.8ms 54.2±2ms 1.03 lazy_grid_construction.OpenGridChunked.time_open_grid('120km')
78.0±0.8ms 80.1±2ms 1.03 lazy_grid_construction.OpenGridChunked.time_open_grid('480km')
1.99M 2.01M 1.01 lazy_grid_construction.OpenGridChunked.track_peakmem_open_grid('120km')
2.36±0.02ms 2.49±0.02ms 1.06 mpas_ocean.CheckNorm.time_check_norm('120km')
1.91±0.01ms 1.91±0.01ms 1.00 mpas_ocean.CheckNorm.time_check_norm('480km')
1.15±0.02ms 1.13±0.01ms 0.99 mpas_ocean.ConnectivityConstruction.time_face_face_connectivity('120km')
519±7μs 529±10μs 1.02 mpas_ocean.ConnectivityConstruction.time_face_face_connectivity('480km')
630±9μs 642±8μs 1.02 mpas_ocean.ConnectivityConstruction.time_n_nodes_per_face('120km')
570±10μs 582±6μs 1.02 mpas_ocean.ConnectivityConstruction.time_n_nodes_per_face('480km')
5.20±0.07ms 5.14±0.01ms 0.99 mpas_ocean.ConstructFaceLatLon.time_cartesian_averaging('120km')
3.62±0.03ms 3.63±0.02ms 1.00 mpas_ocean.ConstructFaceLatLon.time_cartesian_averaging('480km')
104±0.5ms 104±0.2ms 1.00 mpas_ocean.ConstructFaceLatLon.time_welzl('120km')
9.59±0.04ms 9.92±0.3ms 1.03 mpas_ocean.ConstructFaceLatLon.time_welzl('480km')
13.4±0.2ms 13.8±0.1ms 1.03 mpas_ocean.ConstructTreeStructures.time_ball_tree('120km')
645±40μs 682±40μs 1.06 mpas_ocean.ConstructTreeStructures.time_ball_tree('480km')
6.51±0.06ms 6.64±0.2ms 1.02 mpas_ocean.ConstructTreeStructures.time_kd_tree('120km')
397±30μs 411±70μs 1.04 mpas_ocean.ConstructTreeStructures.time_kd_tree('480km')
337±4ms 325±8ms 0.97 mpas_ocean.CrossSections.time_const_lat('120km', 1)
171±2ms 165±1ms 0.97 mpas_ocean.CrossSections.time_const_lat('120km', 2)
89.9±0.07ms 87.6±0.8ms 0.97 mpas_ocean.CrossSections.time_const_lat('120km', 4)
280±3ms 281±10ms 1.00 mpas_ocean.CrossSections.time_const_lat('480km', 1)
141±3ms 136±4ms 0.97 mpas_ocean.CrossSections.time_const_lat('480km', 2)
74.9±0.8ms 73.2±0.8ms 0.98 mpas_ocean.CrossSections.time_const_lat('480km', 4)
25.7±0.1ms 25.5±0.1ms 0.99 mpas_ocean.DualMesh.time_dual_mesh_construction('120km')
2.96±0.01ms 3.07±0.06ms 1.03 mpas_ocean.DualMesh.time_dual_mesh_construction('480km')
14.0±0.8ms 14.2±0.5ms 1.01 mpas_ocean.FaceAreas.time_face_areas('120km')
4.06±0.1ms 4.12±0.2ms 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')
721k 721k 1.00 mpas_ocean.FaceAreas.track_peakmem_face_areas('480km')
54.6±1ms 53.9±0.9ms 0.99 mpas_ocean.GeoDataFrame.time_to_geodataframe('120km', 'exclude')
160±4ms 158±3ms 0.99 mpas_ocean.GeoDataFrame.time_to_geodataframe('120km', 'split')
4.95±0.2ms 4.88±0.2ms 0.99 mpas_ocean.GeoDataFrame.time_to_geodataframe('480km', 'exclude')
31.3±0.3ms 31.1±1ms 1.00 mpas_ocean.GeoDataFrame.time_to_geodataframe('480km', 'split')
10.1±0.3ms 9.91±0.4ms 0.98 mpas_ocean.Gradient.time_gradient('120km')
1.40±0.02ms 1.42±0.03ms 1.02 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.44M 3.44M 1.00 mpas_ocean.Gradient.track_peakmem_gradient('120km')
226k 226k 1.00 mpas_ocean.Gradient.track_peakmem_gradient('480km')
286±3μs 288±6μs 1.01 mpas_ocean.HoleEdgeIndices.time_construct_hole_edge_indices('120km')
154±3μs 148±2μs 0.96 mpas_ocean.HoleEdgeIndices.time_construct_hole_edge_indices('480km')
173±2μs 176±2μs 1.02 mpas_ocean.Integrate.time_integrate('120km')
164±9μs 156±2μs 0.95 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')
139±2ms 139±0.6ms 1.00 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'exclude')
138±1ms 138±1ms 1.00 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'include')
140±1ms 144±1ms 1.03 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'split')
9.76±0.08ms 9.83±0.1ms 1.01 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'exclude')
9.76±0.2ms 10.1±0.2ms 1.03 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'include')
9.79±0.2ms 9.90±0.06ms 1.01 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'split')
171±0.8ms 167±2ms 0.98 mpas_ocean.NeighborhoodBuild.time_build('120km', 1.0)
367±7ms 360±8ms 0.98 mpas_ocean.NeighborhoodBuild.time_build('120km', 5.0)
9.33±0.06ms 9.21±0.1ms 0.99 mpas_ocean.NeighborhoodBuild.time_build('480km', 1.0)
11.8±0.3ms 11.5±0.05ms 0.97 mpas_ocean.NeighborhoodBuild.time_build('480km', 5.0)
168±3ms 168±0.9ms 1.00 mpas_ocean.NeighborhoodBuild.time_query_radius('120km', 1.0)
361±7ms 355±2ms 0.99 mpas_ocean.NeighborhoodBuild.time_query_radius('120km', 5.0)
9.19±0.2ms 8.99±0.2ms 0.98 mpas_ocean.NeighborhoodBuild.time_query_radius('480km', 1.0)
11.3±0.3ms 11.6±0.2ms 1.03 mpas_ocean.NeighborhoodBuild.time_query_radius('480km', 5.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)
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)
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)
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)
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)
824k 824k 1.00 mpas_ocean.NeighborhoodBuild.track_peakmem_build('480km', 15.0)
384k 384k 1.00 mpas_ocean.NeighborhoodBuild.track_peakmem_build('480km', 5.0)
27.0±0.5ms 26.7±0.3ms 0.99 mpas_ocean.NeighborhoodDask.time_mean('120km', 'grid_chunks')
11.2±0.09ms 11.5±0.07ms 1.02 mpas_ocean.NeighborhoodDask.time_mean('120km', 'numpy')
24.2±0.4ms 24.0±0.3ms 0.99 mpas_ocean.NeighborhoodDask.time_mean('120km', 'time_chunks')
6.17±0.1ms 6.46±0.08ms 1.05 mpas_ocean.NeighborhoodDask.time_mean('480km', 'grid_chunks')
329±7μs 333±3μs 1.01 mpas_ocean.NeighborhoodDask.time_mean('480km', 'numpy')
3.97±0.06ms 4.31±2ms 1.09 mpas_ocean.NeighborhoodDask.time_mean('480km', 'time_chunks')
5.82M 5.82M 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.67M 5.68M 1.00 mpas_ocean.NeighborhoodDask.track_peakmem_mean('120km', 'time_chunks')
177k 177k 1.00 mpas_ocean.NeighborhoodDask.track_peakmem_mean('480km', 'numpy')
521k 528k 1.01 mpas_ocean.NeighborhoodDask.track_peakmem_mean('480km', 'time_chunks')
4.49±0.01s 4.51±0s 1.00 mpas_ocean.NeighborhoodReduce.time_dataset_reduce('120km', 'mean')
4.61±0.01s 4.65±0.01s 1.01 mpas_ocean.NeighborhoodReduce.time_dataset_reduce('120km', 'median')
119±0.3ms 119±0.6ms 1.00 mpas_ocean.NeighborhoodReduce.time_dataset_reduce('480km', 'mean')
122±0.6ms 120±0.2ms 0.99 mpas_ocean.NeighborhoodReduce.time_dataset_reduce('480km', 'median')
508±3ms 511±3ms 1.01 mpas_ocean.NeighborhoodReduce.time_neighborhood_reduce('120km', 'mean')
539±0.6ms 544±2ms 1.01 mpas_ocean.NeighborhoodReduce.time_neighborhood_reduce('120km', 'median')
16.5±0.04ms 16.4±0.04ms 1.00 mpas_ocean.NeighborhoodReduce.time_neighborhood_reduce('480km', 'mean')
16.9±0.2ms 17.0±0.3ms 1.00 mpas_ocean.NeighborhoodReduce.time_neighborhood_reduce('480km', 'median')
4.51±0.04ms 4.49±0.04ms 1.00 mpas_ocean.NeighborhoodReduce.time_reduce('120km', 'mean')
37.0±0.07ms 37.1±0.09ms 1.00 mpas_ocean.NeighborhoodReduce.time_reduce('120km', 'median')
196±1μs 195±9μs 1.00 mpas_ocean.NeighborhoodReduce.time_reduce('480km', 'mean')
473±6μs 473±9μs 1.00 mpas_ocean.NeighborhoodReduce.time_reduce('480km', 'median')
234k 234k 1.00 mpas_ocean.NeighborhoodReduce.track_peakmem_reduce('120km', 'mean')
235k 235k 1.00 mpas_ocean.NeighborhoodReduce.track_peakmem_reduce('120km', 'median')
19.5k 19.5k 1.00 mpas_ocean.NeighborhoodReduce.track_peakmem_reduce('480km', 'mean')
19.6k 19.6k 1.00 mpas_ocean.NeighborhoodReduce.track_peakmem_reduce('480km', 'median')
387±20μs 362±30μs 0.93 mpas_ocean.PointInPolygon.time_face_search_lonlat('120km')
365±10μs 358±20μs 0.98 mpas_ocean.PointInPolygon.time_face_search_lonlat('480km')
335±10μs 344±10μs 1.03 mpas_ocean.PointInPolygon.time_face_search_xyz('120km')
337±9μs 336±20μs 1.00 mpas_ocean.PointInPolygon.time_face_search_xyz('480km')
64.3±1ms 64.3±0.4ms 1.00 mpas_ocean.RemapDownsample.time_bilinear_remapping
11.1±0.09ms 11.0±0.1ms 0.99 mpas_ocean.RemapDownsample.time_inverse_distance_weighted_remapping
9.74±0.05ms 9.64±0.1ms 0.99 mpas_ocean.RemapDownsample.time_nearest_neighbor_remapping
1.11±0.01s 1.10±0s 0.99 mpas_ocean.RemapUpsample.time_bilinear_remapping
27.6±0.4ms 27.8±0.4ms 1.01 mpas_ocean.RemapUpsample.time_inverse_distance_weighted_remapping
11.5±0.1ms 11.4±0.2ms 1.00 mpas_ocean.RemapUpsample.time_nearest_neighbor_remapping
8.11±0.1ms 8.40±0.2ms 1.04 mpas_ocean.ZonalAverage.time_zonal_average('120km')
5.02±0.4ms 4.83±0.1ms 0.96 mpas_ocean.ZonalAverage.time_zonal_average('480km')
1.031365457378758 1.0371829198191669 1.01 nogil_scaling.GILScaling.track_gil_scaling
1.04±0.01ms 1.06±0.01ms 1.02 open_dataset.OpenDataset.time_open_dataset('dataset', 'dataset')
2.07±0.05ms 2.10±0.03ms 1.01 open_dataset.OpenDataset.time_open_dataset('dataset', 'file')
4.63±0.07ms 4.68±0.08ms 1.01 open_dataset.OpenDataset.time_open_dataset('file', 'dataset')
5.80±0.3ms 5.95±0.3ms 1.03 open_dataset.OpenDataset.time_open_dataset('file', 'file')
328±5ms 336±4ms 1.02 plots.PlotHealpix.time_default_plot(6)
2.94±0.02s 3.01±0.02s 1.02 plots.PlotHealpix.time_default_plot(8)
28M 27.9M 1.00 plots.PlotHealpix.track_peakmem_default_plot(6)
443M 443M 1.00 plots.PlotHealpix.track_peakmem_default_plot(8)
4.15±0.04ms 4.25±0.1ms 1.02 quad_hexagon.QuadHexagon.time_open_dataset
3.53±0.03ms 3.51±0.04ms 0.99 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
70.1k 70.1k 1.00 quad_hexagon.QuadHexagon.track_peakmem_open_dataset
69.9k 69.9k 1.00 quad_hexagon.QuadHexagon.track_peakmem_open_grid
746±6ms 742±3ms 0.99 to_raster.ToRaster.time_to_raster((10000.0, 10.0))
1.54±0.01s 1.54±0.01s 1.00 to_raster.ToRaster.time_to_raster((10000.0, 100.0))
1.80±0.01s 1.80±0.01s 1.00 to_raster.ToRaster.time_to_raster((100000.0, 100.0))
270M 270M 1.00 to_raster.ToRaster.track_peakmem((10000.0, 10.0))
680M 680M 1.00 to_raster.ToRaster.track_peakmem((10000.0, 100.0))
765M 765M 1.00 to_raster.ToRaster.track_peakmem((100000.0, 100.0))

Benchmarks that have got worse:

Change Before [79b7aa6] <v2026.10.1> After [f9e402f] Ratio Benchmark (Parameter)
+ 301M 385M 1.28 face_bounds.FaceBoundsColdStartRss.track_peakmem_open_and_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
+ 301M 385M 1.28 face_bounds.FaceBoundsColdStartRss.track_peakmem_open_and_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
+ 302M 385M 1.27 face_bounds.FaceBoundsColdStartRss.track_peakmem_open_and_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
+ 302M 385M 1.28 face_bounds.FaceBoundsColdStartRss.track_peakmem_open_and_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
+ 277M 385M 1.39 import.Imports.track_peakmem_import_uxarray
+ 320M 385M 1.2 mpas_ocean.CrossSectionsPeakMem.track_peakmem_const_lat('120km', 1)
+ 320M 385M 1.2 mpas_ocean.CrossSectionsPeakMem.track_peakmem_const_lat('120km', 2)
+ 321M 385M 1.2 mpas_ocean.CrossSectionsPeakMem.track_peakmem_const_lat('120km', 4)
+ 304M 385M 1.27 mpas_ocean.CrossSectionsPeakMem.track_peakmem_const_lat('480km', 1)
+ 304M 385M 1.27 mpas_ocean.CrossSectionsPeakMem.track_peakmem_const_lat('480km', 2)
+ 304M 385M 1.27 mpas_ocean.CrossSectionsPeakMem.track_peakmem_const_lat('480km', 4)
+ 317M 386M 1.22 mpas_ocean.GradientColdStartRss.track_peakmem_gradient('120km')
+ 297M 386M 1.3 mpas_ocean.GradientColdStartRss.track_peakmem_gradient('480km')
+ 322M 385M 1.2 mpas_ocean.ZonalAveragePeakMem.track_peakmem_zonal_average('120km')
+ 304M 385M 1.27 mpas_ocean.ZonalAveragePeakMem.track_peakmem_zonal_average('480km')

@rajeeja

rajeeja commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

Hey @Sevans711, I use Copilot as a starting point for my own review, not as a replacement for human review. Some comments are nitpicks, while others are genuinely useful once verified. I confirmed these two issues:

  • Structured members containing only n_node or n_edge can raise UnboundLocalError because dim_ordered is never initialized.
  • Overlapping source and canonical dimension names can silently apply the wrong chunk size because the chunk mapping is modified while it is being read.

Please fix these two cases.

@sunxiuguo

Copy link
Copy Markdown
Contributor Author

@rajeeja Thanks for confirming both cases. Fixed in 9b4fc45:

  • Structured members skip flattening when neither original spatial dimension is present, including canonical n_node-only and n_edge-only members.
  • Chunk aliases are read from the unchanged input request and written to a separate mapping, so overlapping source/canonical names cannot cascade overwritten values.

The two new regression methods exercise both opening APIs, in-memory/Dask members and synthetic NetCDF files, both alias insertion orders, and preservation of the caller's chunk dictionary. They produce 20 failing subcases against the previous head; the revised file passes all 17 tests and 58 subtests. Five additional existing synthetic API/structured-grid tests and all applicable changed-file formatting/lint hooks also pass. An independent synthetic oracle passes 91/91 cases after reproducing 58 failures on the old implementation. Normal Numba JIT was enabled (Linux, Python 3.12).

This revision is limited to these two cases. I have not rerun the complete repository suite or the full ASV performance comparison locally; the earlier memory/performance measurements are not claimed resolved. The existing backend-close-callback limitation and PR #1828 dimension-order scope are unchanged.

@Sevans711 Sevans711 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.

Thank you for putting this together! Overall, this looks really great, and being able to re-use an existing grid during open_dataset is a very welcome addition to uxarray. I read through all the changes, confirmed locally that tests pass, and tried out this new feature with a couple of small examples. I left a few minor inline comments but after these small points get fixed I think I would be ready to approve!

Comment thread uxarray/core/api.py Resolved
Comment thread uxarray/core/api.py Resolved
Comment thread test/core/test_grid_reuse.py Resolved
Comment thread benchmarks/grid_reuse.py Resolved
@sunxiuguo

Copy link
Copy Markdown
Contributor Author

@Sevans711 Thank you for the careful review and for trying the examples. Addressed the four points in 7acbe33:

  • Moved _map_dataset_to_ugrid unchanged into uxarray/core/utils.py and imported it from the API module.
  • Clarified that _get_grid returns the (grid, chunks) tuple.
  • Added assertEqual(result.uxgrid, self.grid) alongside the distinct-object assertion for the existing file/Dataset grid inputs.
  • Chose the broader benchmark-scope option: renamed the module to benchmarks/open_dataset.py and the class to OpenDataset, and updated the module docstring. All six input combinations remain. This renames their ASV identifiers; it does not establish a performance improvement.

Validation on this revision: 17 Grid-reuse tests and 58 subtests passed, independently rerun with the same result; all six benchmark setup/time/teardown smoke cases passed; Ruff lint/format and applicable whitespace, EOF, docstring-first and debug-statement hooks passed. Three adjacent synthetic API tests also passed. Three additional fixture-dependent API checks could not complete in this environment (one missing mesh fixture and two read-only data-cache failures). I have not rerun the complete repository suite or full ASV timing comparison, and the new remote CI is not yet confirmed.

AI Usage: OpenAI Codex assisted with this revision and independent verification. The changes are ready for another look; I have left the review threads unresolved as requested by the contribution guide.

@Sevans711
Sevans711 self-requested a review October 26, 2026 20:37

@Sevans711 Sevans711 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.

@sunxiuguo Thank you for fixing all of my previous points (and also for leaving the threads unresolved; I always find it helpful to go through my previous points one-by-one while re-reviewing)!

I'd be happy to approve, but it looks like some of the CI tests failed. Since it was only the windows tests, I am guessing it is caused by #1844, which has been fixed on main (via a workaround for now, which is why that issue isn't closed yet). Would you be able to update your fork & branch for this PR to be up to date with main?

I suspect that then the tests would pass. Once tests are passing, everything else here looks good and I would be happy to approve!

@sunxiuguo

Copy link
Copy Markdown
Contributor Author

@Sevans711 Thanks — I merged current upstream main (db33c50) into this PR branch without rewriting history. The new head is fe3acf0deac7f24d55e6ee99a2fb9884adeed941; its tree matches the independently checked integration candidate.

Local validation of that candidate: the 17 Grid-reuse tests (58 subtests) and the longitude-related tests passed. The combined focused run finished with 26 passed and 1 failed: the thread-pool test's child process exited with -9, with the cause still unresolved. Earlier isolated runs of that test passed, but do not establish a clean combined run or Windows/full-suite success.

The new CI run and ASV run are awaiting maintainer approval; neither has executed jobs yet. I have not treated them as passing.

AI Usage: OpenAI Codex assisted with the update and verification.

@Sevans711
Sevans711 self-requested a review October 27, 2026 15:41

@Sevans711 Sevans711 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 my previous comments have been resolved, all tests passing, and benchmarking results still look good (the handful of apparent degradations are just a quirk of the ASV suite right now, as can be seen in #1710, not a result of the changes in this PR). Happy to approve, and thank you again @sunxiuguo for all your efforts here!

@erogluorhan erogluorhan 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.

Thanks for working on this! I have some inline comments below

Comment thread uxarray/core/api.py Resolved
Comment thread uxarray/core/api.py Resolved
Comment thread uxarray/core/api.py Resolved
Comment thread uxarray/core/api.py Resolved
Comment thread test/core/test_grid_reuse.py Resolved
Comment thread test/core/test_grid_reuse.py Resolved
Comment thread test/core/test_grid_reuse.py Resolved
Comment thread test/core/test_grid_reuse.py Resolved
@Sevans711

Sevans711 commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

Putting here a report-back from a discussion I just had with @erogluorhan, to keep you informed, @sunxiuguo:

The biggest point we discussed was that ux.open_dataset() isn't actually intended to support structured (lat/lon) grid and data inputs, even though it does happen to support them currently (#1827 (comment)), that support is just by accident. This should probably be clarified in the docstring and possibly fixed in the future (not part of this PR) to make a warning or crash if the inputs are on a lat/lon grid. I've opened issue report #1854 for this so we don't lose track of it.

Because of that, all the new structured-inputs-related tests proposed in this PR are most likely no longer necessary and should be removed (@erogluorhan flagged some of them with inline comments above). If this also makes it easy to condense the tests a bit (e.g., <300 lines instead of >500 lines), it would be helpful to uxarray maintainers if these could be moved to test_api.py instead of being added into their own file!

@dylannelson

Copy link
Copy Markdown
Member

Looks functionally clear on my end. Passing an opened Grid gives the same result as main on every grid+data pair tested. Still recommend following the other teams design ideas before approving. Happy to review again later when changes are made

@erogluorhan erogluorhan 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.

Thanks for addressing the comments from the previous cycle. Some more review comments below.

Comment thread uxarray/core/api.py Resolved

@rajeeja rajeeja left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thank you @sunxiuguo for this useful addition that lets datasets share an existing Grid and its cached properties without rebuilding it, with explicit chunking checks to protect that shared state.

@rajeeja
rajeeja merged commit f67dba7 into UXARRAY:main Nov 3, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working 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.

ux.open_dataset(grid_object, file) crashes with TypeError: invalid filename for netCDF4 backend: <uxarray.Grid>

6 participants