Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
514c650
Feature 2: antimeridian geometry helpers + topology-driven grid_conform
hdrake Jun 25, 2026
448014e
Feature 1: arbitrary-topology grid_boundaries_from_mask
hdrake Jun 25, 2026
18acd29
Merge Feature 2: antimeridian geometry helpers + topology-driven grid…
hdrake Jun 25, 2026
f0f6a19
Refactor: thread face index f_c through region classes; fix .gr I/O
hdrake Jun 25, 2026
9bd3e39
Deps/CI: add shapely + xgcm; pin topology-driven sectionate in CI
hdrake Jun 25, 2026
a0db635
Tests: multi-tile seam stitching + optional real-grid checks; export …
hdrake Jun 25, 2026
418f228
Restore pole-encircling mask handling in mask_from_grid_boundaries
hdrake Jun 25, 2026
b53293e
Docs: re-run example notebooks; document multi-tile/f_c and sectionat…
hdrake Jun 25, 2026
13beeed
Remove unused GFDL example grid loader
hdrake Jun 25, 2026
dfcb653
Add ECCOv4r4 LLC90 multi-tile example notebook
hdrake Jun 25, 2026
0e3cdd0
Stitch region boundaries across rotated/reversed tile seams
hdrake Jun 26, 2026
e422c19
test: symmetrized rotated (X->Y) tile-pair seam stitching
hdrake Jun 26, 2026
0d6d962
Rewrite multi-tile mask->boundary: contourpy-per-face + topological s…
hdrake Jun 27, 2026
e75bd7c
ECCO example: published Atlantic mask, verified divergence theorem, l…
hdrake Jun 27, 2026
c5ff7b2
Multi-tile: full-basin budget check, idiomatic vector divergence, ver…
hdrake Jun 27, 2026
650d37e
ECCO example: Zenodo data + real heat-transport divergence check
hdrake Jun 30, 2026
7c0bf6a
Address PR review: gate ECCO real-data tests, atomic download, doc fixes
hdrake Jun 30, 2026
9a697d0
Single-tile bipolar north-fold: trace fold-straddling regions as one …
hdrake Jul 3, 2026
f738f88
boundaries: degrade gracefully when xgcm lacks north-fold support
hdrake Jul 3, 2026
adef715
ci: install fold xgcm last (--force-reinstall --no-deps)
hdrake Jul 3, 2026
25a7c67
Migrate off deprecated diff_2d_vector -> grid.diff(other_component=)
hdrake Jul 3, 2026
3746419
Unify boundary tracing: stitch across all seams (periodic-X, fold, fa…
hdrake Jul 3, 2026
1fe021f
docs: update CLAUDE.md boundaries.py description for the unified tracer
hdrake Jul 3, 2026
eeff588
examples: re-run all notebooks against the unified seam tracer
hdrake Jul 3, 2026
53fd212
examples: add idealized corner-cases notebook (mask<->boundary on MOM…
hdrake Jul 3, 2026
a66d244
Stitch multi-tile boundaries on sectionate's outer corner-node graph
hdrake Jul 4, 2026
220c3f4
examples: exact ECCO budget via padded transports; southern cap is a …
hdrake Jul 4, 2026
788614e
Base on sectionate #48 (outer-lattice) + xgcm #749; re-execute all no…
hdrake Jul 4, 2026
01f8d37
Add AI Usage Policy and engineering norms to CLAUDE.md
hdrake Jul 13, 2026
c947ad9
Retarget deps to xgcm@dev-v1.0.0 and sectionate@topology-driven-neigh…
hdrake Jul 13, 2026
1bdd4f2
Support boundary->mask on multi-tile grids (per-tile rasterize + stitch)
hdrake Jul 13, 2026
9d25b99
Default boundary tracing to latitude circles (curve='latitude circle')
hdrake Jul 13, 2026
212ec0c
Fix F5/F6/F8 correctness nits; add rotated-seam divergence test (F1/F9)
hdrake Jul 13, 2026
430e1d7
F4: pole-aware boundary->mask closure; cover cube-vertex junctions
hdrake Jul 13, 2026
fedc223
F3: keep overlap alignment's grid indices consistent with coordinates
hdrake Jul 13, 2026
fc45e39
F7: reload .gr children as gridded sections, not bare Sections
hdrake Jul 13, 2026
f6733db
F2 (1/N): topology-aware connected-component labeling
hdrake Jul 13, 2026
75712ae
F2 (2/N): MaskRegion per connected component with unambiguous mask
hdrake Jul 14, 2026
4fbbc35
F2 (3/N): multi-loop .gr/.grs serialization for MaskRegion
hdrake Jul 14, 2026
fb7f32f
F2 (4/N): de-tautologize the mask<->boundary realdata test; update co…
hdrake Jul 14, 2026
27ec43e
F2 (5/N): update worked-example notebooks to the component/boundaries…
hdrake Jul 14, 2026
6d9b26a
examples(nb6): trace the diamond corner-case with great-circle edges
hdrake Jul 14, 2026
fe66c66
Default boundary tracing to great circles (was latitude circle)
hdrake Jul 14, 2026
dd43a0c
deps: depend on released xgcm >= 0.10.1 from PyPI (drop dev-v1.0.0 gi…
hdrake Jul 21, 2026
b8c641e
Fix MaskRegions(2d_mask, 3d_grid): pad only axes the field spans (#24…
hdrake Jul 22, 2026
e0cb20c
Merge branch 'main' into topology-overhaul
hdrake Aug 1, 2026
824a5da
docs: fix two docstrings that break the -W Sphinx build
hdrake Aug 1, 2026
0d03648
Update xgcm and sectionate dependencies in pyproject.toml
hdrake Aug 2, 2026
5e4a17b
Update requirements.txt to remove sectionate comment
hdrake Aug 2, 2026
b8baa1d
Track sectionate 0.4.0rc2: drop the removed layer/interface kwargs (#31)
hdrake Aug 10, 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
15 changes: 8 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## What this is

`regionate` builds **xgcm-grid-consistent** regional masks and boundaries for ocean/climate model output (currently mostly MOM6, but with plans to support any ocean model). Given a geographic polygon, it snaps the polygon to a discrete model grid, producing a boolean cell mask plus the staggered (u,v) velocity faces that trace the region's boundary — so that volume/mass/heat budgets integrated over the masked region are exactly consistent with fluxes through the boundary faces. It leans heavily on its sibling package [`sectionate`](https://github.com/MOM6-community/sectionate) for the section/face-tracing math.
`regionate` builds **xgcm-grid-consistent** regional masks and boundaries for ocean/climate model output. It supports **arbitrary `xgcm.Grid` topologies** — single-tile periodic and bipolar-fold (Arctic) MOM6 grids, and genuinely multi-tile grids defined by `face_connections` (e.g. ECCOv4r4 lat-lon-cap, cubed-sphere) — by driving all grid logic from the topology-aware `sectionate` API rather than hard-coded MOM6 specifics. Given a geographic polygon, it snaps the polygon to a discrete model grid, producing a boolean cell mask plus the staggered (u,v) velocity faces that trace the region's boundary — so that volume/mass/heat budgets integrated over the masked region are exactly consistent with fluxes through the boundary faces. It leans heavily on its sibling package [`sectionate`](https://github.com/MOM6-community/sectionate) for the section/face-tracing math (requires the topology-driven API on [`hdrake/sectionate@topology-driven-neighbors`](https://github.com/hdrake/sectionate/tree/topology-driven-neighbors), formerly PRs #47/#48), and on the bipolar north-fold boundary (`padding={..., "Y": {"fold": ...}}`, formerly xgcm#711) plus the multi-tile `face_connections` padding fix (xgcm#712) and the vector-pad fix (xgcm#749), all released in `xgcm >= 0.10.1` (on PyPI).

## AI Usage Policy

Expand Down Expand Up @@ -34,7 +34,7 @@ pytest -k <name> # run a single test by name
pip install -e . # editable install (after creating env)
```

Dev environment is conda-based (see README). CI (`.github/workflows/ci.yml`) installs `ci/environment.yml`, does `pip install -e .`, then runs `pytest` across Python 3.11–3.14. There is no linter configured.
Dev environment is conda-based (see README). CI (`.github/workflows/ci.yml`) installs `ci/environment.yml`, installs `sectionate` from `hdrake/sectionate@topology-driven-neighbors` (still unreleased), does `pip install -e .` (which pulls `xgcm >= 0.10.1` straight from PyPI — 0.10.1 ships the north-fold boundary and the multi-tile padding fixes), then runs `pytest` across Python 3.11–3.14. There is no linter configured. Optional end-to-end tests against real MOM6/ECCO grids are skipped unless `REGIONATE_REALDATA_TESTS=1`.

## Versioning

Expand All @@ -45,29 +45,30 @@ Consequences worth remembering when editing:
- **Never add a version literal back to the tree**, and never "fix" a `0.0.0+unknown` by hardcoding one — it means the package was imported without being built or installed.
- **Any CI job that installs the package needs `fetch-depth: 0`.** A shallow clone cannot see the tag, so hatch-vcs silently resolves a `0.1.devN` version instead of failing. The checkouts in `ci.yml` and `publish-to-pypi.yml` set it, and `.readthedocs.yaml` unshallows in `post_checkout` for the same reason. The same applies to a `pip install git+https://…` of a fork with no tags: it reports `0.1.devN`, which can fall below a downstream floor.
- `_version.py` **is** shipped inside the sdist, so building from the sdist (as conda-forge does) works with no git present. Do not add it to `[tool.hatch.build] exclude`.
- conda builds run with `--no-build-isolation`, so `hatch-vcs` must sit next to `hatchling` in the recipe's `host` requirements — in `conda/meta.yaml` here and on the conda-forge feedstock.
- conda builds run with `--no-build-isolation`, so `hatch-vcs` must sit next to `hatchling` in the recipe's `host` requirements — on the conda-forge feedstock (the in-tree `conda/meta.yaml` has been removed).
- Releasing is just publishing a GitHub Release tagged `vX.Y.Z`; there is no bump commit. See "Releasing" in `README.md`.

## Architecture

The class hierarchy in `regionate/region.py` is the spine of the package:

- **`Region`** — a named polygon, just `(lons, lats)` corner arrays. Can force counterclockwise winding (`is_section_counterclockwise` from sectionate) and prune duplicate points. No grid awareness.
- **`GriddedRegion(Region)`** — a `Region` bound to an `xgcm.Grid`. Construction calls `get_region_boundary_grid_indices` + `mask_from_grid_boundaries` (in `grid_conform.py`) to compute: corner grid indices (`i_c`, `j_c`), the boundary `(u,v)` velocity faces, and the boolean cell `mask`. This is the central object — masks and boundaries are the whole point.
- **`GriddedRegion(Region)`** — a `Region` bound to an `xgcm.Grid`. Construction calls `get_region_boundary_grid_indices` + `mask_from_grid_boundaries` (in `grid_conform.py`) to compute: corner grid indices (`i_c`, `j_c`, and the per-corner face index `f_c` — `None` for single-tile grids), the boundary `(u,v)` velocity faces, and the boolean cell `mask`. This is the central object — masks and boundaries are the whole point.
- **`BoundedRegion(GriddedRegion)`** — a GriddedRegion whose boundary is defined by named sections rather than a raw polygon.

Supporting modules:
- `regions.py` — **`Regions`**, a dict-like collection mapping names → `Region`/`GriddedRegion`; `overlaps.py` handles intersections between members.
- `grid_conform.py` — the core grid-snapping logic (polygon → grid indices → mask), using `regionmask`, `shapely`, and sectionate.
- `boundaries.py` — `grid_boundaries_from_mask` (inverse: recover boundary faces from a mask).
- `grid_conform.py` — the core grid-snapping logic (polygon → grid indices → mask). `mask_from_grid_boundaries` rasterizes a boundary by splitting it at the antimeridian into a `[-180,180]` MultiPolygon and OR-ing per-piece `regionmask` masks (pole-encircling boundaries are extended to the South Pole first).
- `geometry.py` — antimeridian / MultiPolygon shapely helpers (`split_at_antimeridian`, `normalize_lon`, ...) used by `grid_conform.py`.
- `boundaries.py` — `grid_boundaries_from_mask` (inverse: recover boundary faces from a mask). A shared front-end `_trace_and_drop` traces the mask per face with `contourpy` and drops boundary segments interior to a seam (both separated cells in-mask in the topology-aware halo `_pad_center`); two back-ends then stitch the surviving arcs: `_single_tile_boundaries_from_mask` stitches by physical seam coincidence (handles walls, **periodic** axes, and the bipolar **fold** uniformly; `f_c=None`), and `_multitile_boundaries_from_mask` stitches on the grid's outer (shared-corner) corner-node graph from `sectionate.gridutils.outer_topology`: every traced corner resolves to a physical corner node (uniform across rotated/reversed seams, cube-vertex junctions, the pole, and grid cuts/folds), edges traced from both sides of an undeclared cut/fold annihilate as interior, surviving segments chain into closed loops by node identity, and each node is emitted as the native corner that stores it (`f_c_list`; `None` for single-tile). Boundaries through points stored on no face (LLC90's 4th Arctic-cap vertex, single-sided contact with its lon=-115 Antarctic cut) raise rather than fabricate indices. A region wrapping any seam yields one seam-consistent boundary (an annulus/strip may yield several loops); `_pad_center` pads only genuine seams (periodic/fold/face_connections) with real neighbours and everything else with NaN, so walls are never mistaken for seams. For the budget's *area side* on multi-tile staggered grids, use `outer_topology(grid).padded_transports(u, v)` — exact and xgcm-independent, and the only path that handles edges stored on no face (walls, grid cuts, a cap's un-stored vertex). (xgcm#749 fixes the bare-`DataArray` `grid.diff(..., other_component=)` path across rotated/reversed seams — the dict form was always exact — but even a correct pad cannot supply a halo for edges stored on no face, so `padded_transports` remains preferred here.)
- `integrate.py` — `check_global_coverage` validates that a `Regions` set is non-overlapping and tiles the globe.
- `utilities.py` — shared helpers (re-exported widely via `from .utilities import *`).

`regionate/__init__.py` flattens everything into the top-level namespace via `import *`, so public symbols are referenced as `regionate.Region`, `regionate.GriddedRegion`, etc.

### The `.gr` save format

`GriddedRegion.save()` writes a **directory** (`<name>.gr/`), not a single file: `region.nc` plus the grid dataset and section sub-directories as separate NetCDFs. `open_gr(path, ds_to_grid)` reloads it — note it takes a `ds_to_grid` callback because the `xgcm.Grid` cannot be serialized directly and must be reconstructed from the saved dataset.
`GriddedRegion.to_gr(path)` writes a **directory** (`<name>.gr/`), not a single file: `region.nc` (boundary coords `lons_c`/`lats_c`, corner indices `i_c`/`j_c`/`f_c`, optional `lons_uv`/`lats_uv`, and the `mask`) plus the grid dataset and child-section sub-directories as separate NetCDFs. `open_gr(path, ds_to_grid)` reloads it — note it takes a `ds_to_grid` callback because the `xgcm.Grid` cannot be serialized directly and must be reconstructed from the saved dataset. The face index `f_c` is persisted and reloaded so multi-tile regions do not silently reload as single-tile.

## Conventions

Expand Down
2 changes: 2 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@
"2_advective_heat_convergence.ipynb",
"3_Arctic_heat_CM4p25.ipynb",
"4_bounded_by_named_sections.ipynb",
"5_ECCO_LLC90_multiface_regions.ipynb",
"6_idealized_corner_cases.ipynb",
]


Expand Down
2 changes: 1 addition & 1 deletion docs/environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ dependencies:
- jupyterlab
- matplotlib
- netcdf4
- pydap
- pandoc
- pytest
- ipykernel
# Docs toolchain (standardized "xeos stack": Sphinx + Furo + myst-nb).
Expand Down
7 changes: 7 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ or heat budget integrated over the masked region is exactly consistent with the
fluxes through its boundary — the property that makes closed regional budgets
possible.

All grid logic is driven by the grid's own topology, so this works on **arbitrary
`xgcm.Grid` topologies**: single-tile periodic grids, bipolar-fold (Arctic) MOM6
grids, and genuinely multi-tile grids defined by `face_connections` (e.g. the
ECCOv4r4 lat-lon-cap grid, cubed spheres).

It leans on its sibling package
[`sectionate`](https://github.com/MOM6-community/sectionate) for the
section- and face-tracing math, and builds on
Expand All @@ -29,6 +34,8 @@ examples/1_thickness_budget
examples/2_advective_heat_convergence
examples/3_Arctic_heat_CM4p25
examples/4_bounded_by_named_sections
examples/5_ECCO_LLC90_multiface_regions
examples/6_idealized_corner_cases
contributing
api
```
11 changes: 10 additions & 1 deletion docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,18 @@
## Requirements

regionate is compatible with python 3 (>= version 3.11). It requires
[xgcm](https://xgcm.readthedocs.io/en/stable/) and its sibling package
[xgcm](https://xgcm.readthedocs.io/en/stable/) (>= 0.10.1, which ships the
bipolar north-fold boundary and the multi-tile `face_connections` padding fixes)
and the topology-driven API of its sibling package
Comment on lines +6 to +8

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Remove the version qualifiers on these packages. Dependency information lives in pyproject.toml and the rationale for the specific version pins lives in git history.

[sectionate](https://github.com/MOM6-community/sectionate).

The topology-driven sectionate API is not on PyPI yet, so install it from its
development branch first:

```bash
pip install "sectionate @ git+https://github.com/hdrake/sectionate.git@topology-driven-neighbors"
```

Comment on lines +11 to +17

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Remove this.

## From conda-forge

```bash
Expand Down
68 changes: 58 additions & 10 deletions examples/1_thickness_budget.ipynb

Large diffs are not rendered by default.

223 changes: 175 additions & 48 deletions examples/2_advective_heat_convergence.ipynb

Large diffs are not rendered by default.

270 changes: 199 additions & 71 deletions examples/3_Arctic_heat_CM4p25.ipynb

Large diffs are not rendered by default.

120 changes: 101 additions & 19 deletions examples/4_bounded_by_named_sections.ipynb

Large diffs are not rendered by default.

378 changes: 378 additions & 0 deletions examples/5_ECCO_LLC90_multiface_regions.ipynb

Large diffs are not rendered by default.

Loading
Loading