Skip to content

Drive section neighbor-finding from xgcm topology metadata#5

Closed
hdrake wants to merge 1 commit into
masterfrom
topology-driven-neighbors
Closed

Drive section neighbor-finding from xgcm topology metadata#5
hdrake wants to merge 1 commit into
masterfrom
topology-driven-neighbors

Conversation

@hdrake

@hdrake hdrake commented Jun 23, 2026

Copy link
Copy Markdown
Owner

What & why

Section-finding used to hard-code how to deal with a few grid topologies via a topology= string ("latlon"/"cartesian"/"MOM-tripolar"), and the whole transport/tracer pipeline assumed a single logically-rectangular (Y, X) tile. This PR makes neighbor-finding read from the xgcm.Grid the caller already supplies — its per-axis boundary and face_connections metadata — so periodicity, the bipolar north fold, and inter-tile connectivity "just work." It extends section-finding and transports to multi-tile grids (lat-lon-cap, cubed-sphere) and to all three native C-grid staggerings, and ships two real-world example notebooks that double as validation.

Changes

Topology from xgcm metadata (no topology=). Neighbors follow from each axis' boundary"periodic" → wrap, fill/extend wall → clip, and a single-tile bipolar north fold {"Y": {"fold": ...}} → handled natively via xgcm's fold padding. The topology= argument and the hard-coded north-fold formula are gone; gridutils.build_neighbor_maps builds the topology-aware neighbor maps the pathfinder consumes.

Multi-tile geometry (face_connections). build_neighbor_maps builds index-valued arrays, pads them once with xgcm, and reads the halos to get each corner's neighbors with axis rotation/reversal already applied — reusing xgcm's tested padding rather than reimplementing topology. A reciprocity check raises rather than return silently-wrong neighbors. Section state carries a face index; grid_section returns it for multi-tile grids.

All three C-grid staggerings native. Vorticity points may sit at "outer" (MOM6 symmetric), "right" (MOM6 non-symmetric), or "left" (MITgcm/ECCO, incl. lat-lon-cap) positions, distinguished by a per-position velocity index offset (gridutils.corner_position/corner_offset). No conversion to a symmetric grid is needed.

Deterministic walk + curve option. The section walk is deterministic and direction-independent (admits neighbors strictly closer to the endpoint plus any seam twin, breaking ties by index), so periodic/fold-seam crossings no longer hinge on floating-point rounding. grid_section takes curve="great circle" (default geodesic) or "latitude circle" (constant-latitude paths that march in longitude).

Transports / tracers across seams. Each section edge's normal velocity is read in a single face's frame (source-first dual-anchor), so no velocity is rotated across a seam. The per-edge sign is computed geometrically — "does the stored velocity point to the left of the section's direction of travel?" — so it stays consistent across rotated faces without a single global orientation factor. convergent_transport/extract_tracer select the contributing face pointwise.

Examples

  • examples/4_sections_on_global_tripolar_grid.ipynb — sections traced across the bipolar north fold on a global tripolar grid, including a curve="latitude circle" global section.
  • examples/5_MOC_transports_ECCOv4r4.ipynb — end-to-end depth-space global meridional overturning streamfunction from ECCOv4r4 on its native 13-tile lat-lon-cap (LLC90) grid: multi-tile seam crossing + native "left" staggering + latitude-circle sections, on real model output. Its small ECCO data subset is fetched from Zenodo (10.5281/zenodo.21051424) with MD5 checks, so it runs with no NASA Earthdata login.

Verification

  • All existing single-tile tests pass unchanged (the geographic sign reproduces them to machine precision).
  • Split-grid equivalence (a 2-face grid is a flat grid cut in half; transport must match the uncut grid) for symmetric and non-symmetric axis-preserving seams, exact to rtol 1e-12.
  • Streamfunction-difference test for a genuinely rotated 2-face grid: transport across the rotated seam = psi(P2) - psi(P1) — a known, non-zero, rotation-independent answer needing no model data.
  • Bipolar-fold and reversibility tests; the ECCO MOC example reproduces the expected global overturning structure on a real lat-lon-cap grid.
  • 41 tests pass, deterministic across PYTHONHASHSEED (new files: test_section_multitile.py, test_section_fold.py, test_section_reversibility.py, test_left_grid.py, test_section_cornercases.py).

Dependency note

The bipolar-fold path relies on fold-aware xgcm (bipolar fold xgcm#711 + face-connection padding fix #713), currently the unreleased hdrake/xgcm@tripolar-north-fold. The fold tests skip without it; everything else runs on xgcm>=0.9.0.

Known limitations (guarded, not silent)

  • A tripolar/bipolar fold expressed as a face self-connection in face_connections raises NotImplementedError — build the grid as a single tile with the fold as a boundary ({"X": "periodic", "Y": {"fold": "corner"}}) instead, which is supported natively.

🤖 Generated with Claude Code

Replace the hard-coded `topology=` string ("latlon"/"cartesian"/"MOM-tripolar")
with neighbor-finding inferred from the `xgcm.Grid` the caller already supplies,
and extend section-finding and transports to multi-tile grids (face_connections).

Single-tile grids: neighbors follow from each axis' `boundary` metadata
("periodic" -> wrap, else clip), read via the public `axis.boundary` property
(no more private `._boundary`). Behavior is unchanged; existing tests pass as-is.

Multi-tile grids (lat-lon-cap, cubed-sphere): `gridutils.build_neighbor_maps`
pads index-valued arrays with xgcm and reads the halos to get each corner's
neighbors with axis-rotation/reversal already applied, reusing xgcm's tested
padding. A reciprocity check raises instead of returning the silently-wrong
neighbors xgcm's padding produces (hash-seed-dependently) for the most complex
reversed topologies. Section state carries a `face` index; `grid_section`
returns the face component for multi-tile grids.

Transports/tracers across face seams: each section edge's normal velocity is
read in a single face's frame (source-first dual-anchor), so nothing is rotated
across the seam, and the per-edge sign is computed geometrically ("does the
velocity point left of travel?") so it stays consistent across rotated faces
without a single global orientation factor. Verified by split-grid equivalence
(a 2-face grid is a flat grid cut in half) for symmetric and non-symmetric
axis-preserving seams, and by a streamfunction-difference test (transport
P1->P2 = psi(P2)-psi(P1)) for a genuinely rotated 2-face grid.

The tripolar/bipolar fold (a face self-connection) is detected and rejected
with NotImplementedError; xgcm does not support it either (xgcm issue #194).

New tests in sectionate/tests/test_section_multitile.py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@hdrake

hdrake commented Jun 23, 2026

Copy link
Copy Markdown
Owner Author

Reopening against MOM6-community/sectionate instead.

@hdrake hdrake closed this Jun 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant