Skip to content

Avoid converting chunked data to Numpy arrays (i.e. .values calls) - #1588

Open
cmdupuis3 wants to merge 16 commits into
UXARRAY:mainfrom
cmdupuis3:cmd/devalue
Open

Avoid converting chunked data to Numpy arrays (i.e. .values calls)#1588
cmdupuis3 wants to merge 16 commits into
UXARRAY:mainfrom
cmdupuis3:cmd/devalue

Conversation

@cmdupuis3

@cmdupuis3 cmdupuis3 commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

Partly addresses #1583

Overview

This PR is to resolve suboptimal usage of .values throughout the repo, primarily by deferring to lazy xarray and dask operations. See the table of usage sites in Issue #1583.

This specific PR is for the first two tables of usage sites; dead code and bugfixes aren't really included here. Aside from simple cases, some cases were solved by branching on whether the data type was already chunked, so scalability for some parts will depend on if you're using numpy or xarray/dask arrays at those points.

PR Checklist

General

  • An issue is linked created and linked
  • Add appropriate labels
  • Filled out Overview and Expected Usage (if applicable) sections

Testing

  • Adequate tests are created if there is new functionality
  • Tests cover all possible logical paths in your function
  • Tests are not too basic (such as simply calling a function and nothing else)

Documentation

  • Docstrings have been added to all new functions
  • Docstrings have updated with any function changes

@cmdupuis3 cmdupuis3 self-assigned this Jul 18, 2026
@cmdupuis3 cmdupuis3 added scalability Related to scalability & performance efforts run-benchmark Run ASV benchmark workflow labels Jul 18, 2026
@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown

ASV Benchmarking

Benchmark Comparison Results

Benchmarks that have improved:

Change Before [058a991] After [a9f30ad] Ratio Benchmark (Parameter)
- 12.2±0.6μs 10.7±0.3μs 0.88 bench_connectivity.Connectivity.time_node_face('480km')
- 524M 335M 0.64 face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
- 643M 335M 0.52 face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
- 706±4ms 568±6ms 0.8 mpas_ocean.CrossSections.time_const_lat('120km', 1)
- 355±2ms 292±1ms 0.82 mpas_ocean.CrossSections.time_const_lat('120km', 2)
- 182±0.2ms 149±0.8ms 0.82 mpas_ocean.CrossSections.time_const_lat('120km', 4)
- 450M 329M 0.73 mpas_ocean.Gradient.peakmem_gradient('480km')
- 4.76±0.05ms 4.32±0.04ms 0.91 mpas_ocean.RemapDownsample.time_nearest_neighbor_remapping

Benchmarks that have stayed the same:

Change Before [058a991] After [a9f30ad] Ratio Benchmark (Parameter)
10.6±0.07μs 10.7±0.05μs 1.01 bench_connectivity.Connectivity.time_edge_face('120km')
10.8±0.07μs 10.8±0.07μs 1 bench_connectivity.Connectivity.time_edge_face('480km')
10.8±0.2μs 10.5±0.06μs 0.97 bench_connectivity.Connectivity.time_edge_node('120km')
10.9±0.2μs 10.9±0.1μs 1 bench_connectivity.Connectivity.time_edge_node('480km')
10.6±0.1μs 10.4±0.3μs 0.98 bench_connectivity.Connectivity.time_face_edge('120km')
11.3±0.1μs 10.7±0.1μs 0.95 bench_connectivity.Connectivity.time_face_edge('480km')
10.8±0.1μs 10.4±0.03μs 0.96 bench_connectivity.Connectivity.time_face_face('120km')
11.2±0.1μs 10.5±0.3μs 0.93 bench_connectivity.Connectivity.time_face_face('480km')
21.5±0.4μs 21.2±0.2μs 0.99 bench_connectivity.Connectivity.time_face_node('120km')
22.4±0.2μs 21.6±0.05μs 0.96 bench_connectivity.Connectivity.time_face_node('480km')
10.9±0.07μs 10.5±0.2μs 0.96 bench_connectivity.Connectivity.time_node_edge('120km')
11.0±0.09μs 11.1±0.1μs 1.01 bench_connectivity.Connectivity.time_node_edge('480km')
10.5±0.09μs 10.4±0.05μs 0.99 bench_connectivity.Connectivity.time_node_face('120km')
334M 334M 1 face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
363M 365M 1.01 face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
14.3±0.08ms 14.4±0.1ms 1.01 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
3.66±0.02ms 3.64±0.03ms 1 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
18.9±0.05ms 19.0±0.01ms 1.01 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
2.17±0.02ms 2.17±0.02ms 1 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
922±3ns 908±8ns 0.99 mpas_ocean.CheckNorm.time_check_norm('120km')
902±8ns 891±3ns 0.99 mpas_ocean.CheckNorm.time_check_norm('480km')
839±7ms 810±5ms 0.97 mpas_ocean.ConnectivityConstruction.time_face_face_connectivity('120km')
54.4±0.8ms 52.6±0.8ms 0.97 mpas_ocean.ConnectivityConstruction.time_face_face_connectivity('480km')
685±20μs 665±8μs 0.97 mpas_ocean.ConnectivityConstruction.time_n_nodes_per_face('120km')
597±9μs 582±20μs 0.98 mpas_ocean.ConnectivityConstruction.time_n_nodes_per_face('480km')
5.39±0.05ms 5.33±0.01ms 0.99 mpas_ocean.ConstructFaceLatLon.time_cartesian_averaging('120km')
3.98±0.01ms 3.90±0.04ms 0.98 mpas_ocean.ConstructFaceLatLon.time_cartesian_averaging('480km')
3.45±0s 3.50±0.05s 1.01 mpas_ocean.ConstructFaceLatLon.time_welzl('120km')
224±0.8ms 223±1ms 0.99 mpas_ocean.ConstructFaceLatLon.time_welzl('480km')
18.2±0.05ms 18.2±0.03ms 1 mpas_ocean.ConstructTreeStructures.time_ball_tree('120km')
1.04±0.01ms 1.03±0.02ms 0.99 mpas_ocean.ConstructTreeStructures.time_ball_tree('480km')
10.6±0.03ms 10.6±0.04ms 1 mpas_ocean.ConstructTreeStructures.time_kd_tree('120km')
724±8μs 713±30μs 0.98 mpas_ocean.ConstructTreeStructures.time_kd_tree('480km')
542±0.9ms 523±4ms 0.96 mpas_ocean.CrossSections.time_const_lat('480km', 1)
277±2ms 264±0.8ms 0.95 mpas_ocean.CrossSections.time_const_lat('480km', 2)
143±0.6ms 136±0.4ms 0.95 mpas_ocean.CrossSections.time_const_lat('480km', 4)
24.7±0.08ms 24.3±0.1ms 0.98 mpas_ocean.DualMesh.time_dual_mesh_construction('120km')
3.18±0.07ms 3.11±0.05ms 0.98 mpas_ocean.DualMesh.time_dual_mesh_construction('480km')
965±10ms 942±5ms 0.98 mpas_ocean.GeoDataFrame.time_to_geodataframe('120km', False)
51.0±0.7ms 51.4±0.5ms 1.01 mpas_ocean.GeoDataFrame.time_to_geodataframe('120km', True)
85.5±0.2ms 85.1±0.2ms 1 mpas_ocean.GeoDataFrame.time_to_geodataframe('480km', False)
5.76±0.06ms 5.58±0.04ms 0.97 mpas_ocean.GeoDataFrame.time_to_geodataframe('480km', True)
349M 349M 1 mpas_ocean.Gradient.peakmem_gradient('120km')
174±0.8ms 175±2ms 1 mpas_ocean.Gradient.time_gradient('120km')
12.4±0.09ms 12.3±0.06ms 0.99 mpas_ocean.Gradient.time_gradient('480km')
226±1μs 223±1μs 0.99 mpas_ocean.HoleEdgeIndices.time_construct_hole_edge_indices('120km')
133±0.8μs 130±0.8μs 0.97 mpas_ocean.HoleEdgeIndices.time_construct_hole_edge_indices('480km')
298M 299M 1 mpas_ocean.Integrate.peakmem_integrate('120km')
277M 278M 1 mpas_ocean.Integrate.peakmem_integrate('480km')
219±0.8μs 216±3μs 0.99 mpas_ocean.Integrate.time_integrate('120km')
203±4μs 198±1μs 0.98 mpas_ocean.Integrate.time_integrate('480km')
182±0.6ms 184±1ms 1.01 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'exclude')
184±2ms 184±1ms 1 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'include')
181±1ms 183±2ms 1.01 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'split')
13.6±0.07ms 13.9±0.3ms 1.02 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'exclude')
13.6±0.1ms 13.7±0.3ms 1.01 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'include')
13.8±0.1ms 13.8±0.3ms 1 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'split')
354±1μs 346±2μs 0.98 mpas_ocean.PointInPolygon.time_face_search_lonlat('120km')
355±1μs 355±1μs 1 mpas_ocean.PointInPolygon.time_face_search_lonlat('480km')
337±1μs 333±1μs 0.99 mpas_ocean.PointInPolygon.time_face_search_xyz('120km')
337±2μs 337±2μs 1 mpas_ocean.PointInPolygon.time_face_search_xyz('480km')
242±1ms 246±1ms 1.01 mpas_ocean.RemapDownsample.time_bilinear_remapping
292±2ms 289±3ms 0.99 mpas_ocean.RemapDownsample.time_inverse_distance_weighted_remapping
1.44±0.01s 1.44±0s 1 mpas_ocean.RemapUpsample.time_bilinear_remapping
36.9±0.5ms 37.1±0.5ms 1.01 mpas_ocean.RemapUpsample.time_inverse_distance_weighted_remapping
9.56±0.1ms 9.39±0.1ms 0.98 mpas_ocean.RemapUpsample.time_nearest_neighbor_remapping
28.4±0.5ms 27.9±0.5ms 0.98 mpas_ocean.ZonalAverage.time_zonal_average('120km')
6.71±0.07ms 6.34±0.02ms 0.94 mpas_ocean.ZonalAverage.time_zonal_average('480km')
275M 273M 0.99 quad_hexagon.QuadHexagon.peakmem_open_dataset
272M 272M 1 quad_hexagon.QuadHexagon.peakmem_open_grid
7.15±0.3ms 6.75±0.08ms 0.94 quad_hexagon.QuadHexagon.time_open_dataset
5.91±0.08ms 5.68±0.03ms 0.96 quad_hexagon.QuadHexagon.time_open_grid

Benchmarks that have got worse:

Change Before [058a991] After [a9f30ad] Ratio Benchmark (Parameter)
+ 583±8ms 847±4ms 1.45 import.Imports.timeraw_import_uxarray

@cmdupuis3

Copy link
Copy Markdown
Collaborator Author

pre-commit.ci autofix

@cmdupuis3

Copy link
Copy Markdown
Collaborator Author

There's some spurious asv results from machine variability, but the benchmarks consistenly show peak-mem reductions and some speedups for cross-sections.

@cmdupuis3
cmdupuis3 requested a review from erogluorhan July 20, 2026 23:25
@cmdupuis3
cmdupuis3 marked this pull request as ready for review July 20, 2026 23:26
@cmdupuis3
cmdupuis3 requested a review from rajeeja July 23, 2026 23:26
@cmdupuis3

Copy link
Copy Markdown
Collaborator Author

pre-commit.ci autofix

@Sevans711
Sevans711 self-requested a review July 27, 2026 16:18

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

Hi @cmdupuis3, thank you for proposing these changes! Overall these look like good clean changes which should help improve the scalability of uxarray.

I have some notes/suggestions/requested changes. Primarily, I noticed that there are missing regression tests. This may be especially important for places with significant changes (more significant than just replacing obj.values with obj.data):

  • UxDataArray.integrate()
  • UxDataArrayCrossSectionAccessor.__call__()
  • uxarray.plot.matplotlib._nearest_neighbor_resample()
  • RemapAccessor.apply_weights()

It may be nice to add a regression test for UxDataset.to_xarray() too, even though the changes here are minor, since that feels like a very core part of the functionality.

The tests could be similar to what you added already in test_topological_agg.py (plus my additional request on that file): ensure that numpy and dask inputs ultimately give the same values (plus assert that the dask inputs lead to dask outputs).

Other parts of the code changes here might benefit from similar tests, but I don't know if that should be necessary. For example, the changes in _geos just replace obj.values with obj.data; does a regression test need to be added for that or no? Curious to hear from @erogluorhan and/or @rajeeja on this question in particular.

Misc. note: I'm not sure if I fully understand the changes to uxarray/cross_sections files. I need to make sure to take a closer look at those during a subsequent review.

Comment thread uxarray/core/aggregation.py
Comment thread uxarray/core/aggregation.py Outdated
Comment thread uxarray/core/aggregation.py
Comment thread uxarray/core/aggregation.py Outdated
Comment thread uxarray/core/dataarray.py
)

if self.values.size == self.uxgrid.n_face:
if self.size == self.uxgrid.n_face:

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.

These changes look good here; no further revision needed in this area, just flagging #1616 as related. Fixing 1616 will lead to these lines being replaced by something like if self._face_centered() instead.

Comment thread uxarray/core/dataarray.py
"i,...i", self.uxgrid.face_areas.values, self.values
)
else:
# dask-backed data: xr.dot keeps the reduction lazy

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.

Does integration need a regression test which is sensitive to dask versus numpy? Perhaps, assert that the result is a dask array, and that the values equal the numpy version, similar to the tests already added by this PR into test_topological_agg.


assert numpy_result.dims == dask_result.dims
assert np.allclose(numpy_result.values, dask_result.values, equal_nan=True)

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.

This test currently passes on main, so it does not serve as a regression test yet. One possible fix is to add: import dask; assert isinstance(dask_result.data, dask.array.Array). Then, it fails on main but passes on this branch.

This applies to the node_to_edge case below as well.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The idea is that if someone is expecting bitwise reproducibility and switching to dask flips bits, these tests will catch it.

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.

That makes sense. Clarifying, I think the bitwise reproducibility part of these tests are important to include and your implementation of them looks good!

Separately, I think there should also be tests to ensure the result is actually a dask array. But, those don't need to be separate test functions, for example it would be sufficient to add them via something like the assert statement above.

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.

Separately, I think there should also be tests to ensure the result is actually a dask array. But, those don't need to be separate test functions, for example it would be sufficient to add them via something like the assert statement above.

This makes sense! However, either they need to be in separate tests or the naming of the test case here should slightly be adjusted (currently the name suggests whether input output values and dims are all close)

@erogluorhan erogluorhan changed the title Devalue uxarray to make it less expensive Avoid converting chunked data to Numpy arrays (i.e. .values calls) in the code Jul 27, 2026
@erogluorhan erogluorhan changed the title Avoid converting chunked data to Numpy arrays (i.e. .values calls) in the code Avoid converting chunked data to Numpy arrays (i.e. .values calls) Jul 27, 2026
@cmdupuis3

Copy link
Copy Markdown
Collaborator Author

pre-commit.ci autofix

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.

3 participants