Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
878e075
Port AccuSphGeom EFT algorithms and add spherical geometry user guide
rajeeja May 22, 2026
1bbf349
Fix pre-commit: remove unused imports and variable
rajeeja May 22, 2026
1034a18
Fix pre-commit: split semicolons in notebook cells
rajeeja May 22, 2026
fb74e75
Address Hongyu's review: port AccuSphGeom compensated arithmetic, rew…
rajeeja May 28, 2026
256b347
Fix RTD: remove RST-invalid numbered list from gca_const_lat_intersec…
rajeeja May 29, 2026
e8999e6
Fix RTD notebook kernel metadata
rajeeja Jun 1, 2026
0220276
Address AccuSphGeom review feedback
rajeeja Jun 4, 2026
0fe327c
Separate intersection kernels into three layers: numerical core, stat…
rajeeja Jun 4, 2026
94c06fe
Fix NaN/Inf propagation in intersection kernels for denom=0 and plana…
rajeeja Jun 4, 2026
2f4147b
Add geometry kernel benchmarks and to_raster auto-extent test
rajeeja Jun 10, 2026
3b3f30c
Address AccuSphGeom review: precision wording, dead code removal, cor…
rajeeja Jun 10, 2026
e33a26c
Pin tornado<6.5.7 to fix Windows CI; remove dead shim; fix notebook A…
rajeeja Jun 10, 2026
f3c8618
Trim computing.py docstring; drop inline=always from L1 kernels
rajeeja Jun 10, 2026
59a8020
Revert tornado pin; root cause is openssl 3.6.3 on Windows
rajeeja Jun 10, 2026
35c85a8
Port AccuSphGeom EFT algorithms and add spherical geometry user guide
rajeeja May 22, 2026
6c1497e
Fix pre-commit: remove unused imports and variable
rajeeja May 22, 2026
57373d0
Fix pre-commit: split semicolons in notebook cells
rajeeja May 22, 2026
3a55e5d
Address Hongyu's review: port AccuSphGeom compensated arithmetic, rew…
rajeeja May 28, 2026
683bc9a
Fix RTD: remove RST-invalid numbered list from gca_const_lat_intersec…
rajeeja May 29, 2026
3ae95f3
Fix RTD notebook kernel metadata
rajeeja Jun 1, 2026
7e35cee
Address AccuSphGeom review feedback
rajeeja Jun 4, 2026
1f844e1
Separate intersection kernels into three layers: numerical core, stat…
rajeeja Jun 4, 2026
ec81e16
Fix NaN/Inf propagation in intersection kernels for denom=0 and plana…
rajeeja Jun 4, 2026
58de273
Merge origin/rajeeja/accusphere: resolve geometry kernel conflicts
rajeeja Jun 18, 2026
896d968
Merge branch 'main' into rajeeja/accusphere
rajeeja Jun 18, 2026
c0f281d
computing: use FMA two_prod where available (portable Veltkamp fallback)
rajeeja Jun 19, 2026
3438b90
intersections: add allocation-free scalar const-lat L1 kernel
rajeeja Jun 19, 2026
aab2bbd
Merge branch 'main' into rajeeja/accusphere
rajeeja Jun 23, 2026
16cb171
Merge branch 'main' into rajeeja/accusphere
rajeeja Jun 24, 2026
69c6d48
style: apply ruff formatting to computing.py
rajeeja Jun 24, 2026
4c4fa94
Merge branch 'main' into rajeeja/accusphere
rajeeja Jun 24, 2026
7968008
Merge branch 'main' into rajeeja/accusphere
rajeeja Jun 30, 2026
e323380
Merge branch 'main' into rajeeja/accusphere
hongyuchen1030 Jul 3, 2026
5b6082b
Merge remote-tracking branch 'origin/main' into update-accusphere-1513
rajeeja Jul 7, 2026
456c587
Merge remote-tracking branch 'origin/main' into update-accusphere-1513
rajeeja Jul 7, 2026
07ac029
Clean up spherical geometry review items
rajeeja Jul 7, 2026
273f968
Merge branch 'main' into rajeeja/accusphere
rajeeja Jul 9, 2026
dba0206
Scalarize constant-latitude intersection dispatcher to remove per-cal…
rajeeja Jul 11, 2026
4366e02
Merge remote-tracking branch 'origin/main' into rajeeja/accusphere
rajeeja Jul 11, 2026
ffcc91e
Merge remote-tracking branch 'origin/rajeeja/accusphere' into rajeeja…
rajeeja Jul 11, 2026
64885f6
Merge branch 'main' into rajeeja/accusphere
rajeeja Jul 13, 2026
a463913
Import benchmark kernels inside setup to avoid asv collection failures
rajeeja Jul 13, 2026
465fd4c
Cmd/accusphere (#1579)
cmdupuis3 Jul 16, 2026
75f06c0
Merge remote-tracking branch 'origin/main' into rajeeja/accusphere
rajeeja Jul 16, 2026
0d131d7
Add thread-scaling benchmark for FP64 vs AccuX constlat dispatcher
rajeeja Jul 16, 2026
b9f5bb3
Cap thread-scaling sweep at performance cores to avoid E-core artifact
rajeeja Jul 16, 2026
f61555b
Fix thread-scaling plot ticks to show only 1,2,4,8
rajeeja Jul 16, 2026
146672c
o Fix pre-commit
rajeeja Jul 16, 2026
e9b394f
o doc fixes
rajeeja Jul 16, 2026
a7d8e88
o Remove benchmark image from repo
rajeeja Jul 17, 2026
a5fd603
Split out point-in-face and lat-lon bounds to a separate PR
rajeeja Jul 17, 2026
d2550bc
Merge remote-tracking branch 'origin/main' into rajeeja/accusphere
rajeeja Jul 18, 2026
e87873f
o computing: match AccuSphGeom acc_sqrt_re exactly (residual order, b…
rajeeja Jul 18, 2026
4af6ed1
o intersections: compensated norm in AccuXGCA kernel, branch-free mas…
rajeeja Jul 18, 2026
ff2d6e0
o arcs: branch-free on_minor_arc int mask at tol 1e-8 matching AccuSp…
rajeeja Jul 18, 2026
3e3a972
o benchmarks: drop point-in-polygon kernel bench, moved to point-in-f…
rajeeja Jul 18, 2026
5c80ff7
o benchmarks: drop thread-scaling constlat, moved to separate scaling…
rajeeja Jul 18, 2026
66b907b
o docs: move spherical-geometry notebook out, depends on split-out po…
rajeeja Jul 18, 2026
8e6c88a
o docs: drop spherical-geometry notebook from user-guide toctree
rajeeja Jul 18, 2026
64ecad4
o test_plot: drop to_raster_auto_extent, unrelated to AccuXGCA/AccuXC…
rajeeja Jul 18, 2026
793f351
o fix on_minor_arc antipodal false positive
rajeeja Jul 20, 2026
f78f5ff
o add on_minor_arc degeneracy regression tests
rajeeja Jul 20, 2026
1d00adb
fix diff_of_products addition grouping to match AccuSphGeom eft.hpp
rajeeja Jul 20, 2026
9d88f83
Merge branch 'main' into rajeeja/accusphere
rajeeja Jul 21, 2026
48831eb
o intersections: collapse pos/neg-valid branches via XOR in gca_const…
rajeeja Jul 22, 2026
79e962d
Parallel partial edge_nodes
cmdupuis3 Jul 23, 2026
bf0abbe
Lazy intersections
cmdupuis3 Jul 23, 2026
18ee69b
zonal and cross-section peakmem benchmarks
cmdupuis3 Jul 23, 2026
5cfc64c
Merge main into cmd/accusphere3
cmdupuis3 Jul 28, 2026
e8ad82d
forgot to save; fixes unmerged code
cmdupuis3 Jul 28, 2026
74fb517
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 28, 2026
80fab8d
Merge branch 'main' into cmd/accusphere3
cmdupuis3 Jul 29, 2026
b77a816
_flatnonzero missleading docstring
cmdupuis3 Jul 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions benchmarks/mpas_ocean.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,3 +229,46 @@ def setup(self, resolution, *args, **kwargs):
def time_zonal_average(self, resolution):
lat_step = 10
self.uxds['bottomDepth'].zonal_mean(lat=(-45, 45, lat_step))


class ZonalAveragePeakMem:
"""Peak memory of a cold-start non-conservative zonal-mean sweep."""

param_names = ["resolution"]
params = [["480km", "120km"]]

def setup_cache(self):
"""Compile the njit kernels before anything is measured."""
for resolution in self.params[0]:
grid, data = file_path_dict[resolution]
uxds = ux.open_dataset(grid, data)
uxds.uxgrid.bounds
uxds[data_var].zonal_mean(lat=(-45, 45, 10))

def peakmem_zonal_average(self, resolution):
grid, data = file_path_dict[resolution]
uxds = ux.open_dataset(grid, data)
uxds.uxgrid.bounds
uxds[data_var].zonal_mean(lat=(-45, 45, 10))


class CrossSectionsPeakMem:
"""Peak memory of a cold-start constant-latitude cross-section sweep."""

param_names = ["resolution", "lat_step"]
params = [["480km", "120km"], [1, 2, 4]]

def setup_cache(self):
"""Compile the njit kernels before anything is measured."""
for resolution in self.params[0]:
uxgrid = ux.open_grid(file_path_dict[resolution][0])
uxgrid.normalize_cartesian_coordinates()
uxgrid.bounds
uxgrid.cross_section.constant_latitude(0.0)

def peakmem_const_lat(self, resolution, lat_step):
uxgrid = ux.open_grid(file_path_dict[resolution][0])
uxgrid.normalize_cartesian_coordinates()
uxgrid.bounds
for lat in np.arange(-45, 45, lat_step):
uxgrid.cross_section.constant_latitude(lat)
64 changes: 42 additions & 22 deletions uxarray/core/zonal.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
get_number_of_intersections,
)
from uxarray.grid.utils import (
_get_cartesian_face_edge_nodes_array,
_get_cartesian_face_edge_nodes_array_subset,
_small_angle_of_2_vectors,
)

Expand Down Expand Up @@ -40,14 +40,15 @@ def _compute_non_conservative_zonal_mean(uxda, latitudes, use_robust_weights=Fal
# Create a NumPy array for storing results
result = np.full(shape, np.nan, dtype=result_dtype)

faces_edge_nodes_xyz = _get_cartesian_face_edge_nodes_array(
uxgrid.face_node_connectivity.values,
uxgrid.n_face,
uxgrid.n_max_face_nodes,
uxgrid.node_x.values,
uxgrid.node_y.values,
uxgrid.node_z.values,
)
# Grid arrays are read once here and passed to the per-latitude subset
# builder. The whole-grid (n_face, n_max, 2, 3) edge array is never
# materialized; only the faces intersecting each latitude are built, so peak
# memory scales with the candidate count (~1% of n_face) instead of n_face.
face_node_connectivity = uxgrid.face_node_connectivity.values
n_max_face_edges = uxgrid.n_max_face_nodes
node_x = uxgrid.node_x.values
node_y = uxgrid.node_y.values
node_z = uxgrid.node_z.values

bounds = uxgrid.bounds.values

Expand All @@ -63,7 +64,16 @@ def _compute_non_conservative_zonal_mean(uxda, latitudes, use_robust_weights=Fal

z = np.sin(np.deg2rad(lat))

fe = faces_edge_nodes_xyz[face_indices]
fe = _get_cartesian_face_edge_nodes_array_subset(
face_indices,
face_node_connectivity,
n_nodes_per_face,
n_max_face_edges,
node_x,
node_y,
node_z,
)

nn = n_nodes_per_face[face_indices]
b = bounds[face_indices]

Expand Down Expand Up @@ -258,14 +268,13 @@ def _compute_face_band_weights(uxgrid, bands):
f"bands must be monotonic non-decreasing; got diff(bands)={np.diff(bands)}"
)

faces_edge_nodes_xyz = _get_cartesian_face_edge_nodes_array(
uxgrid.face_node_connectivity.values,
uxgrid.n_face,
uxgrid.n_max_face_nodes,
uxgrid.node_x.values,
uxgrid.node_y.values,
uxgrid.node_z.values,
)
# Read grid arrays once; the per-band partial-face edge subsets are built
# on demand below rather than materializing the whole-grid edge array.
face_node_connectivity = uxgrid.face_node_connectivity.values
n_max_face_edges = uxgrid.n_max_face_nodes
node_x = uxgrid.node_x.values
node_y = uxgrid.node_y.values
node_z = uxgrid.node_z.values
n_nodes_per_face = uxgrid.n_nodes_per_face.values
face_bounds_lat = uxgrid.face_bounds_lat.values
face_areas = uxgrid.face_areas.values
Expand Down Expand Up @@ -301,11 +310,22 @@ def _compute_face_band_weights(uxgrid, bands):

partial = all_overlapping[~fc_mask]
partial_pos = np.nonzero(~fc_mask)[0]
for pos, f in zip(partial_pos, partial):
nedge = n_nodes_per_face[f]
weights[pos] = _compute_band_overlap_area(
faces_edge_nodes_xyz[f, :nedge], zmin, zmax
if partial.size:

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.

New branch (zero partial faces). Add a test for a band with only fully-contained faces.

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.

Not sure what you mean here... The weights are already computed for all faces overlapping the band, this branch only reweights partial faces, so a strict fully-contained set of faces is already accounted for by the time this branch is reached.

# Build edges for only the partially-overlapping faces of this band.
fe_partial = _get_cartesian_face_edge_nodes_array_subset(
partial,
face_node_connectivity,
n_nodes_per_face,
n_max_face_edges,
node_x,
node_y,
node_z,
)
for k, (pos, f) in enumerate(zip(partial_pos, partial)):
nedge = n_nodes_per_face[f]
weights[pos] = _compute_band_overlap_area(
fe_partial[k, :nedge], zmin, zmax
)

per_band.append((all_overlapping.astype(np.int64), weights))

Expand Down
32 changes: 20 additions & 12 deletions uxarray/grid/grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -2576,9 +2576,14 @@ def get_edges_at_constant_latitude(self, lat: float, use_face_bounds: bool = Fal
"is not yet supported."
)
else:
edges = constant_lat_intersections_no_extreme(
lat, self.edge_node_z.values, self.n_edge
)
# Gather per-edge z-coords positionally, mirroring the longitude
# sibling. A concrete connectivity indexer against node_z.data keeps
# node coords lazy on a chunked grid (dask indexed by a numpy array)
# and — unlike xarray indexing with a dask connectivity — does not
# raise. The screener then reduces the gathered array to candidates.
edge_nodes = self.edge_node_connectivity.values
edge_node_z = self.node_z.data[edge_nodes.ravel()].reshape(edge_nodes.shape)
edges = constant_lat_intersections_no_extreme(lat, edge_node_z)

return edges.squeeze()

Expand Down Expand Up @@ -2608,7 +2613,7 @@ def get_faces_at_constant_latitude(

faces = constant_lat_intersections_face_bounds(
lat=lat,
face_bounds_lat=self.face_bounds_lat.values,
face_bounds_lat=self.face_bounds_lat.data,
)
return faces

Expand Down Expand Up @@ -2643,11 +2648,14 @@ def get_edges_at_constant_longitude(
"is not yet supported."
)
else:
edge_node_x = self.node_x[self.edge_node_connectivity].values
edge_node_y = self.node_y[self.edge_node_connectivity].values
edges = constant_lon_intersections_no_extreme(
lon, edge_node_x, edge_node_y, self.n_edge
)
# Positional gather of edge endpoint coords: a concrete connectivity
# indexer against node_[xy].data keeps node coords lazy on a chunked
# grid and does not raise (xarray vindex rejects a dask indexer).
edge_nodes = self.edge_node_connectivity.values
flat = edge_nodes.ravel()
edge_node_x = self.node_x.data[flat].reshape(edge_nodes.shape)
edge_node_y = self.node_y.data[flat].reshape(edge_nodes.shape)
edges = constant_lon_intersections_no_extreme(lon, edge_node_x, edge_node_y)
return edges.squeeze()

def get_faces_at_constant_longitude(self, lon: float):
Expand All @@ -2671,7 +2679,7 @@ def get_faces_at_constant_longitude(self, lon: float):
f"Longitude must be between -180 and 180 degrees. Received {lon}"
)

faces = constant_lon_intersections_face_bounds(lon, self.face_bounds_lon.values)
faces = constant_lon_intersections_face_bounds(lon, self.face_bounds_lon.data)
return faces

def get_faces_between_longitudes(self, lons: tuple[float, float]):
Expand All @@ -2688,7 +2696,7 @@ def get_faces_between_longitudes(self, lons: tuple[float, float]):
An array of face indices that are strictly between two lines of constant longitude.

"""
return faces_within_lon_bounds(lons, self.face_bounds_lon.values)
return faces_within_lon_bounds(lons, self.face_bounds_lon.data)

def get_faces_between_latitudes(self, lats: tuple[float, float]):
"""Identifies the indices of faces that are strictly between two lines of constant latitude.
Expand All @@ -2704,7 +2712,7 @@ def get_faces_between_latitudes(self, lats: tuple[float, float]):
An array of face indices that are strictly between two lines of constant latitude.

"""
return faces_within_lat_bounds(lats, self.face_bounds_lat.values)
return faces_within_lat_bounds(lats, self.face_bounds_lat.data)

def get_faces_containing_point(
self,
Expand Down
Loading