Skip to content

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

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

Avoid converting chunked data to Numpy arrays (i.e. .values calls)#1588
cmdupuis3 wants to merge 19 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 [73101af] After [8c477ed] Ratio Benchmark (Parameter)
- 524M 336M 0.64 face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
- 631M 336M 0.53 face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
- 646±0.6ms 511±1ms 0.79 mpas_ocean.CrossSections.time_const_lat('120km', 1)
- 328±0.7ms 259±1ms 0.79 mpas_ocean.CrossSections.time_const_lat('120km', 2)
- 170±0.4ms 133±0.3ms 0.78 mpas_ocean.CrossSections.time_const_lat('120km', 4)
- 452M 329M 0.73 mpas_ocean.Gradient.peakmem_gradient('480km')

Benchmarks that have stayed the same:

Change Before [73101af] After [8c477ed] Ratio Benchmark (Parameter)
10.1±0.1μs 10.1±0.2μs 1 bench_connectivity.Connectivity.time_edge_face('120km')
10.2±0.07μs 10.2±0.03μs 0.99 bench_connectivity.Connectivity.time_edge_face('480km')
9.85±0.03μs 10.0±0.2μs 1.02 bench_connectivity.Connectivity.time_edge_node('120km')
10.1±0.1μs 10.1±0.03μs 1 bench_connectivity.Connectivity.time_edge_node('480km')
9.79±0.06μs 9.84±0.1μs 1 bench_connectivity.Connectivity.time_face_edge('120km')
10.2±0.09μs 10.1±0.04μs 0.98 bench_connectivity.Connectivity.time_face_edge('480km')
9.87±0.05μs 10.1±0.1μs 1.03 bench_connectivity.Connectivity.time_face_face('120km')
9.97±0.06μs 10.1±0.04μs 1.01 bench_connectivity.Connectivity.time_face_face('480km')
19.7±0.08μs 20.0±0.1μs 1.02 bench_connectivity.Connectivity.time_face_node('120km')
20.7±0.2μs 20.6±0.05μs 1 bench_connectivity.Connectivity.time_face_node('480km')
9.88±0.09μs 9.76±0.03μs 0.99 bench_connectivity.Connectivity.time_node_edge('120km')
9.98±0.06μs 9.98±0.05μs 1 bench_connectivity.Connectivity.time_node_edge('480km')
9.77±0.06μs 9.88±0.08μs 1.01 bench_connectivity.Connectivity.time_node_face('120km')
10.2±0.2μs 10.2±0.1μs 1 bench_connectivity.Connectivity.time_node_face('480km')
334M 334M 1 face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
364M 363M 1 face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
20.0±0.07μs 20.1±0.04μs 1.01 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
9.53±0.05μs 9.66±0.03μs 1.01 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
9.41±0.04ms 9.36±0.06ms 0.99 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
1.94±0.03ms 1.99±0.01ms 1.03 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
920±20ns 933±10ns 1.01 geometry_kernels.AccucrossKernels.time_accucross
2.36±0.07μs 2.33±0.01μs 0.99 geometry_kernels.AccucrossKernels.time_accucross_pair
267±0.8ns 271±3ns 1.02 geometry_kernels.EFTPrimitives.time_acc_sqrt_re
263±1ns 262±2ns 1 geometry_kernels.EFTPrimitives.time_diff_of_products
210±2ns 213±0.7ns 1.01 geometry_kernels.EFTPrimitives.time_two_prod
217±5ns 211±0.3ns 0.97 geometry_kernels.EFTPrimitives.time_two_sum
1.15±0μs 1.17±0μs 1.01 geometry_kernels.GCAConstLatIntersection.time_accux_constlat_kernel
851±8ns 845±7ns 0.99 geometry_kernels.GCAConstLatIntersection.time_gca_const_lat_intersection
1.57±0.01μs 1.55±0μs 0.99 geometry_kernels.GCAConstLatIntersection.time_try_gca_const_lat_intersection
1.28±0μs 1.27±0.01μs 0.99 geometry_kernels.GCAGCAIntersection.time_accux_gca_kernel
1.10±0μs 1.11±0.01μs 1.01 geometry_kernels.GCAGCAIntersection.time_gca_gca_intersection
1.79±0.02μs 1.78±0.01μs 1 geometry_kernels.GCAGCAIntersection.time_try_gca_gca_intersection
38.9±0.2μs 38.3±0.2μs 0.99 geometry_kernels.OrientPredicates.time_on_minor_arc
464±1ns 471±0.9ns 1.02 geometry_kernels.OrientPredicates.time_orient3d_on_sphere
2.91±0.1ms 2.80±0.01ms 0.96 geometry_samebody.SameBodyConstLat.time_accux_dispatch
1.33±0ms 1.32±0ms 1 geometry_samebody.SameBodyConstLat.time_accux_kernel
1.79±0.01ms 1.78±0.01ms 0.99 geometry_samebody.SameBodyConstLat.time_fp64_dispatch
152±0.4μs 151±0.3μs 1 geometry_samebody.SameBodyConstLat.time_fp64_kernel
32.6±0.03ms 32.6±0.3ms 1 geometry_samebody_gcagca.SameBodyGcaGca.time_accux_dispatch
10.6±0.06ms 10.6±0.05ms 1 geometry_samebody_gcagca.SameBodyGcaGca.time_accux_kernel
27.7±0.03ms 28.1±0.6ms 1.01 geometry_samebody_gcagca.SameBodyGcaGca.time_fp64_dispatch
4.46±0.04ms 4.42±0.01ms 0.99 geometry_samebody_gcagca.SameBodyGcaGca.time_fp64_kernel
767±2ms 767±2ms 1 import.Imports.timeraw_import_uxarray
878±3ns 886±2ns 1.01 mpas_ocean.CheckNorm.time_check_norm('120km')
885±20ns 845±10ns 0.96 mpas_ocean.CheckNorm.time_check_norm('480km')
715±3ms 715±4ms 1 mpas_ocean.ConnectivityConstruction.time_face_face_connectivity('120km')
45.4±0.2ms 45.2±0.4ms 1 mpas_ocean.ConnectivityConstruction.time_face_face_connectivity('480km')
13.3±0.08μs 13.5±0.2μs 1.02 mpas_ocean.ConnectivityConstruction.time_n_nodes_per_face('120km')
13.6±0.1μs 14.0±0.2μs 1.03 mpas_ocean.ConnectivityConstruction.time_n_nodes_per_face('480km')
4.54±0.05ms 4.53±0.02ms 1 mpas_ocean.ConstructFaceLatLon.time_cartesian_averaging('120km')
2.98±0.01ms 2.97±0.01ms 1 mpas_ocean.ConstructFaceLatLon.time_cartesian_averaging('480km')
3.29±0.1s 3.27±0.01s 0.99 mpas_ocean.ConstructFaceLatLon.time_welzl('120km')
212±0.7ms 211±1ms 1 mpas_ocean.ConstructFaceLatLon.time_welzl('480km')
19.6±0.01ms 19.6±0.01ms 1 mpas_ocean.ConstructTreeStructures.time_ball_tree('120km')
1.05±0.02ms 1.06±0.01ms 1 mpas_ocean.ConstructTreeStructures.time_ball_tree('480km')
10.6±0.02ms 10.6±0.01ms 1 mpas_ocean.ConstructTreeStructures.time_kd_tree('120km')
692±7μs 690±10μs 1 mpas_ocean.ConstructTreeStructures.time_kd_tree('480km')
483±0.8ms 454±4ms 0.94 mpas_ocean.CrossSections.time_const_lat('480km', 1)
244±0.7ms 230±0.9ms 0.94 mpas_ocean.CrossSections.time_const_lat('480km', 2)
126±1ms 118±0.5ms 0.93 mpas_ocean.CrossSections.time_const_lat('480km', 4)
22.0±0.06ms 22.1±0.4ms 1.01 mpas_ocean.DualMesh.time_dual_mesh_construction('120km')
2.52±0.02ms 2.54±0.01ms 1.01 mpas_ocean.DualMesh.time_dual_mesh_construction('480km')
839±1ms 849±8ms 1.01 mpas_ocean.GeoDataFrame.time_to_geodataframe('120km', False)
50.4±0.5ms 50.6±1ms 1 mpas_ocean.GeoDataFrame.time_to_geodataframe('120km', True)
72.1±0.4ms 71.7±0.3ms 1 mpas_ocean.GeoDataFrame.time_to_geodataframe('480km', False)
5.28±0.06ms 5.28±0.07ms 1 mpas_ocean.GeoDataFrame.time_to_geodataframe('480km', True)
350M 350M 1 mpas_ocean.Gradient.peakmem_gradient('120km')
165±0.8ms 164±0.3ms 0.99 mpas_ocean.Gradient.time_gradient('120km')
11.8±0.4ms 11.3±0.01ms 0.96 mpas_ocean.Gradient.time_gradient('480km')
189±1μs 192±2μs 1.01 mpas_ocean.HoleEdgeIndices.time_construct_hole_edge_indices('120km')
87.0±0.5μs 87.2±2μs 1 mpas_ocean.HoleEdgeIndices.time_construct_hole_edge_indices('480km')
349M 350M 1 mpas_ocean.Integrate.peakmem_integrate('120km')
329M 329M 1 mpas_ocean.Integrate.peakmem_integrate('480km')
179±0.9μs 180±1μs 1 mpas_ocean.Integrate.time_integrate('120km')
160±3μs 158±1μs 0.99 mpas_ocean.Integrate.time_integrate('480km')
187±1ms 186±0.7ms 0.99 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'exclude')
187±0.7ms 187±2ms 1 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'include')
186±1ms 186±1ms 1 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'split')
13.6±0.03ms 13.7±0.04ms 1.01 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'exclude')
13.6±0.1ms 13.8±0.04ms 1.01 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'include')
13.6±0.06ms 13.8±0.08ms 1.01 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'split')
285±2μs 282±2μs 0.99 mpas_ocean.PointInPolygon.time_face_search_lonlat('120km')
286±1μs 288±2μs 1 mpas_ocean.PointInPolygon.time_face_search_lonlat('480km')
266±1μs 266±0.6μs 1 mpas_ocean.PointInPolygon.time_face_search_xyz('120km')
268±1μs 267±0.9μs 1 mpas_ocean.PointInPolygon.time_face_search_xyz('480km')
210±0.5ms 211±1ms 1 mpas_ocean.RemapDownsample.time_bilinear_remapping
230±0.4ms 232±0.7ms 1.01 mpas_ocean.RemapDownsample.time_inverse_distance_weighted_remapping
4.21±0.01ms 4.21±0.01ms 1 mpas_ocean.RemapDownsample.time_nearest_neighbor_remapping
1.19±0s 1.18±0s 1 mpas_ocean.RemapUpsample.time_bilinear_remapping
35.0±0.4ms 35.6±0.2ms 1.02 mpas_ocean.RemapUpsample.time_inverse_distance_weighted_remapping
8.83±0.1ms 8.79±0.1ms 1 mpas_ocean.RemapUpsample.time_nearest_neighbor_remapping
26.6±0.2ms 25.6±0.6ms 0.96 mpas_ocean.ZonalAverage.time_zonal_average('120km')
5.18±0.08ms 5.23±0.04ms 1.01 mpas_ocean.ZonalAverage.time_zonal_average('480km')
324M 328M 1.01 quad_hexagon.QuadHexagon.peakmem_open_dataset
324M 324M 1 quad_hexagon.QuadHexagon.peakmem_open_grid
6.48±0.09ms 6.46±0.09ms 1 quad_hexagon.QuadHexagon.time_open_dataset
5.43±0.03ms 5.46±0.03ms 1 quad_hexagon.QuadHexagon.time_open_grid

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

Comment thread test/core/test_topological_agg.py
@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

@cmdupuis3
cmdupuis3 requested a review from Sevans711 July 28, 2026 16:54
@cmdupuis3

Copy link
Copy Markdown
Collaborator Author

@erogluorhan Sam and I were wondering about your philosophy with respect to dask versus numpy routines. I was thinking that if we have a case where the numpy routine could be faster than a dask routine in some situations, we might want to keep both, or maybe you'd want to go all in on dask for simplicity.

if isinstance(uxda.data, np.ndarray):
    aggregated_var = _apply_node_to_face_aggregation_numpy(uxda, ...)
elif isinstance(uxda.data, da.Array):
    # apply aggregation on dask array, TODO:
    aggregated_var = _apply_node_to_face_aggregation_numpy(uxda, ...)
else:
    raise ValueError

This is something in main's aggregate.py. I fleshed out the dask branch, but now we still have this logic, and I left the numpy routine in on this branch.

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