Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 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
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: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,22 @@ jobs:
- name: Set up conda environment
run: |
mamba env update -f ci/environment.yml
# TODO: drop these explicit installs once their upstreams release the required
# fixes to PyPI; the floors in pyproject.toml will then suffice.
# (2) the topology-driven sectionate API + the outer-lattice multi-tile corner
# topology / exact padded transports. hdrake/sectionate@topology-driven-neighbors
# is the consolidated dev branch carrying this work (formerly PRs #47/#48),
# and gives the most complete multi-tile boundary treatment.
python -m pip install "sectionate @ git+https://github.com/hdrake/sectionate.git@topology-driven-neighbors"
python -m pip install -e .
# (1) the xgcm dev-v1.0.0 branch, which consolidates the bipolar north-fold
# boundary (formerly xgcm#711), the face_connections halo-padding fix
# (xgcm#712) needed for correct multi-tile (lat-lon-cap / cap) padding, and
# the bare-DataArray other_component vector-pad fix (xgcm#749) so vector
# diff/interp is correct across rotated/reversed seams. Installed LAST with
# --force-reinstall --no-deps so the git build wins over any PyPI xgcm that
# an earlier step pulled in to satisfy the xgcm>=0.9.0 floors above.
python -m pip install --force-reinstall --no-deps "xgcm @ git+https://github.com/hdrake/xgcm.git@dev-v1.0.0"
- name: Conda list information
run: |
conda env list
Expand Down
31 changes: 25 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,25 @@ 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 consolidated on [`hdrake/xgcm@dev-v1.0.0`](https://github.com/hdrake/xgcm/tree/dev-v1.0.0).

## AI Usage Policy

This repository has no formal AI Usage Policy of its own yet, so follow the one drafted for the sibling project xgcm ([`hdrake/xgcm@add-Claude.md`](https://github.com/hdrake/xgcm/tree/add-Claude.md)): **the person running the AI is responsible for every change it makes.** That means AI assistance must be disclosed, and the human must be able to explain the full diff. In practice:

- Do not produce changes the user could not stand behind and explain — no cargo-culted edits, no "it probably works."
- Surface uncertainty explicitly rather than hiding it behind confident-looking code.
- Keep diffs small and reviewable so they *can* be explained.

## Engineering norms

Adapted from the xgcm draft above; apply these when changing regionate's code.

1. **Deprecate by removing, not warning.** When you rename or remove public API, do not keep the old name limping along behind a `DeprecationWarning`. Remove it and make the old name fail immediately with a clear message (e.g. `raise ValueError("Argument 'old' renamed to 'new'.")`). Breaking changes are acceptable here; multi-release deprecation machinery is avoidable maintenance cost.
2. **Raise on bad input; never return a wrong answer.** regionate already does this — e.g. the corner-coordinate / grid validation the constructors run before building a mask. Keep it up: a silently wrong mask or boundary is worse than an exception in scientific software. On ambiguous/invalid/unsupported input, raise a specific error rather than guessing.
3. **Do not grow core dependencies.** The core deps are `sectionate` (the sibling that owns grid-section tracing and transports), plus `pyproj`, `geopandas`, `regionmask`, and `contourpy` (see `pyproject.toml`). Prefer pushing grid/transport logic *down into `sectionate`* over adding new dependencies here. Before adding any import, confirm it isn't pulling in a heavy new dependency.
4. **Every code change ships with tests.** Build test data from the synthetic grid fixtures in `regionate/tests/` (e.g. `initialize_spherical_grid()`) rather than hand-rolling datasets. For a bug fix, first write a test that fails, then make it pass. Exercise both construction directions (boundary→mask via `GriddedRegion`, mask→boundary via `MaskRegions`) and both orientations (clockwise/counterclockwise) where relevant.
5. **Version by effort; breaking changes are allowed.** Bump `regionate/version.py` (`__version__`, read by hatchling). Favor a clean break (norm 1) over a compatibility shim.

## Commands

Expand All @@ -16,28 +34,29 @@ 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` and `xgcm` from `hdrake/xgcm@dev-v1.0.0` until those are released, does `pip install -e .`, 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`.

## 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
8 changes: 8 additions & 0 deletions conda/meta.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,16 @@ requirements:
- python >={{ python_min }}
- pyproj
- geopandas
- shapely
# Multi-tile (lat-lon-cap / cap) padding needs the face_connections
# halo-padding fix + north-fold/vector-pad fixes, consolidated on
# hdrake/xgcm@dev-v1.0.0; floor stays 0.9.0.
- xgcm >=0.9.0
- regionmask
- contourpy
# Needs the topology-driven sectionate API + 'left'/rotated-seam corner-neighbour
# fix, consolidated on hdrake/sectionate@topology-driven-neighbors; the branch
# self-reports 0.3.3, so the floor stays there until release.
- sectionate >=0.3.3

test:
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
- sphinx
- nbsphinx
Expand Down
4 changes: 3 additions & 1 deletion docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ regionate: a package for creating xgcm-grid consistent regional masks and bounda
examples/1_thickness_budget
examples/2_advective_heat_convergence
examples/3_Arctic_heat_CM4p25
examples/4_bounded_by_named_sections
examples/4_bounded_by_named_sections
examples/5_ECCO_LLC90_multiface_regions
examples/6_idealized_corner_cases
7 changes: 6 additions & 1 deletion docs/source/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ Requirements
^^^^^^^^^^^^

regionate is compatible with python 3 (>= version 3.11). It requires xgcm
(>= version 0.9.0).
(>= version 0.9.0) and the topology-driven ``sectionate`` API
(`sectionate#47 <https://github.com/MOM6-community/sectionate/pull/47>`_).
Until that is released, install ``sectionate`` from the pull-request branch
first::

pip install "sectionate @ git+https://github.com/MOM6-community/sectionate.git@refs/pull/47/head"

Installation from Conda Forge
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Expand Down
68 changes: 58 additions & 10 deletions examples/1_thickness_budget.ipynb

Large diffs are not rendered by default.

177 changes: 151 additions & 26 deletions examples/2_advective_heat_convergence.ipynb

Large diffs are not rendered by default.

226 changes: 174 additions & 52 deletions examples/3_Arctic_heat_CM4p25.ipynb

Large diffs are not rendered by default.

120 changes: 102 additions & 18 deletions examples/4_bounded_by_named_sections.ipynb

Large diffs are not rendered by default.

Loading
Loading