From 3a96f47669275d973b9e1360bb58fdc9dca900c5 Mon Sep 17 00:00:00 2001 From: Henri Drake Date: Tue, 11 Aug 2026 11:17:47 -0700 Subject: [PATCH 1/3] Resolve corner identity across the bipolar fold seam A seam gives one physical corner two index representations. Crossing a single-tile bipolar fold, the walk steps from one representation straight to a corner adjacent to the *other*, so the pair is a real physical edge but not an index-adjacent one -- and the single-tile velocity-face arithmetic in `uvindices_from_qindices` assumes index adjacency. It read the velocity column off the source corner, which is then the wrong representative: it named the mirror image of the face the section actually crossed, with the opposite sign, and on a closed crossing emitted one face twice. `_insert_seam_twins` splices the twin back in wherever a step skips it, so the crossing becomes one ordinary edge plus one zero-length twin edge, and zero-length edges already emit no velocity face. This is the single-tile counterpart of the corner canonicalization `_OuterTopology` performs for multi-tile grids, which is why LLC90 and the cubed-sphere were already exact. It runs in the deterministic corner->face derivation, so sections reloaded from saved (i_c, j_c) get it too. The ORCA-style sign flip of the duplicated seam row needs no handling of its own: the traversal direction and the stored velocity are read in the same index frame, so the flip cancels. Sections running *along* the seam were already exact and still are. A declared fold whose corner coordinates do not carry the fold symmetry has no twins to splice; such a crossing now raises instead of silently attributing the edge to some other face. Tests use a streamfunction fixture -- psi single-valued in physical space, transports from index-space differences of it -- so the flow is exactly non-divergent, the expected answer is analytic (psi(end) - psi(start)), and the duplicated, sign-flipped seam row is reproduced without being hard-coded. `_pinched_fold_grid` carries the corner-pivot fold identity exactly and needs no downloaded data; the real MOM6 tripolar grid covers traced sections. Without the fix the crossing tests fail by 1.3e-2 on a latitude circle whose true answer is zero. Reported by Geoffrey Stanley in MOM6-community/sectionate#47. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 3 +- sectionate/tests/test_section_fold.py | 217 +++++++++++++++++++++++++- sectionate/transports.py | 114 ++++++++++++++ 3 files changed, 332 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3e8276b..744cf5b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -148,7 +148,7 @@ The package is organized around a pipeline: define sections → map to grid → - **`section.py`** — Section definition and grid path algorithms. `Section` holds named waypoint coordinates; `GriddedSection` extends it with grid index information. `grid_section()` is the main entry point that maps geographic waypoints to grid vorticity-point indices `(i_c, j_c)` by walking the grid between waypoints along the `curve` requested (`"great circle"`, the default geodesic, or `"latitude circle"`); each segment must span less than 180°. The walk is deterministic and direction-independent: it admits neighbors strictly closer to the endpoint plus any seam twin of the current cell (so periodic/fold-seam crossings do not depend on floating-point rounding), and breaks ties by index. Grid topology is inferred entirely from `xgcm.Grid` metadata — each axis' `boundary` (periodic wrap, fill/extend wall, or a single-tile bipolar north fold `{"Y": {"fold": ...}}`) and `face_connections` for multi-tile grids — so there is no `topology` keyword. The pathfinder consumes topology-aware neighbor maps built by `gridutils.build_neighbor_maps`. -- **`transports.py`** — Transport computation along sections. `uvindices_from_qindices()` converts vorticity-point indices to U/V velocity-point indices using a per-position corner offset (`gridutils.corner_offset`) covering all three C-grid staggerings: 'outer', 'right', and 'left' (see "Corner staggering" below). `convergent_transport()` is the main function: it lazily computes signed normal transports with configurable orientation (positive inward to the polygon defined by the section). +- **`transports.py`** — Transport computation along sections. `uvindices_from_qindices()` converts vorticity-point indices to U/V velocity-point indices using a per-position corner offset (`gridutils.corner_offset`) covering all three C-grid staggerings: 'outer', 'right', and 'left' (see "Corner staggering" below). Before doing so it resolves **corner identity**, since a seam gives one physical corner two indices: multi-tile grids canonicalize each corner through `_OuterTopology`, and single-tile grids run `_insert_seam_twins()`, which splices the seam twin into the corner list wherever a step skips it (see "Seam twins" below). `convergent_transport()` is the main function: it lazily computes signed normal transports with configurable orientation (positive inward to the polygon defined by the section). - **`tracers.py`** — `extract_tracer()` interpolates tracer data to U/V points along a section path for cross-section plotting. @@ -160,6 +160,7 @@ The package is organized around a pipeline: define sections → map to grid → - **Vorticity points (q-points):** Sections are defined as paths through vorticity-point indices `(i_c, j_c)`. Consecutive q-points define velocity faces (either U or V). - **Corner staggering (three positions):** Vorticity sits at one of three xgcm positions: `"outer"` (MOM6 symmetric, M+1×N+1), `"right"` (MOM6 non-symmetric, M×N), or `"left"` (MITgcm/ECCO, incl. the lat-lon-cap, M×N). All three are native; they differ only by a per-position velocity index offset (`gridutils.corner_offset`: outer→0, right→+1, left→0). `"left"` indexes like `"outer"`; it differs only in array length and in that the *high* corner row/column is absent (so a section exactly on the north/east domain wall clips one row inside). +- **Seam twins (corner identity):** A seam gives one physical corner more than one index — the periodic wrap's first/last column, a shared multi-tile boundary corner, and the bipolar fold's mirrored seam-row columns. Transport attribution therefore has to know *which* indices denote the same point, or it will name the mirror image of the face a section crossed (with the sign that goes with it) and can count a face twice. Multi-tile grids get this from `_OuterTopology`, which resolves every corner to one canonical native `(face, j, i)`. Single-tile grids get it from `transports._insert_seam_twins()`: wherever consecutive corners are a physical edge but *not* index-adjacent — which is how the walk crosses a fold seam — it splices in the twin, so the crossing becomes one ordinary edge plus one zero-length twin edge, and zero-length edges emit no velocity face. This is the same construction as the `umaskutil`/`vmaskutil` masking of duplicated points in NEMO. The **sign flip** of an ORCA-style duplicated seam row (NEMO 4.2 manual, Appendix E) needs no handling of its own: the traversal direction and the stored velocity are read in the same index frame (geographically on both sides, for multi-tile), so a frame reversal cancels. A declared fold whose corner coordinates do not carry the fold symmetry has no twins to splice, and a crossing raises rather than silently mis-attributing. - **Sign conventions:** For a *closed* section, `convergent_transport()` determines orientation (clockwise/counterclockwise) using stereographic projection and signed polygon area, then applies sign corrections so positive transport means "inward" (toward the enclosed polygon). For an *open* section there is no enclosing polygon, so `positive_in` is undefined; it instead uses the **left-of-transect** convention — `positive_in=True` makes positive transport point to the left of the section as traversed from the first to the last waypoint — and emits a `UserWarning`. `is_section_counterclockwise()` is only consulted in the closed case. - **xgcm.Grid dependency:** The package relies heavily on `xgcm.Grid` for grid metadata (axis boundaries, coordinate positions, dataset access via `grid._ds`). diff --git a/sectionate/tests/test_section_fold.py b/sectionate/tests/test_section_fold.py index a6f681d..b46d9a7 100644 --- a/sectionate/tests/test_section_fold.py +++ b/sectionate/tests/test_section_fold.py @@ -5,6 +5,7 @@ seam connectivity from xgcm's padding with no fold-specific code of its own. """ import os +import warnings import numpy as np import pytest @@ -12,7 +13,8 @@ import xgcm from sectionate.gridutils import get_geo_corners, build_neighbor_maps -from sectionate.section import grid_section +from sectionate.section import grid_section, distance_on_unit_sphere +from sectionate.transports import convergent_transport, uvindices_from_qindices def _fold_grid(nx=8, ny=5, pivot="corner"): @@ -111,3 +113,216 @@ def test_fold_section_crosses_arctic_on_real_grid(): assert np.all(np.abs(np.asarray(lats_c) - 80.0) < 1.0) # It reaches the northern fold row. assert j_c.max() == ds.yq.size - 1 + + +# --------------------------------------------------------------------------- +# Transports across the fold seam. +# +# The seam row identifies each corner with its mirrored column, so the same +# physical corner carries two indices. Crossing the seam, the walk steps from one +# of them to a corner adjacent to the *other*, which is a physical edge but not an +# index-adjacent one. The tests below pin down that such a crossing names the +# velocity face the section actually crossed, with the right sign, exactly once. +# +# They use a streamfunction fixture: psi is a single-valued function of *physical* +# position, so seam twins get identical psi, and transports are built from +# index-space differences of psi. That makes the flow exactly non-divergent and the +# answer for any section known analytically -- transport = psi(end) - psi(start) -- +# while automatically reproducing the duplicated, sign-flipped seam row of an +# ORCA-style fold (NEMO 4.2 manual, Appendix E) without hard-coding the flip. +# --------------------------------------------------------------------------- + + +def _pinched_fold_grid(nxh=16, ny=6): + """ + A single-tile C-grid that genuinely carries the corner-pivot fold identity. + + Picture a cylinder (X-periodic) whose cross-section is pinched shut as `j` rises: + at row `j` the corners sit on an ellipse with semi-axes ``(a_j, b_j)``, and + ``b_{ny-1} = 0``, so the top row collapses onto a segment traversed out and back. + Under ``i <-> nxh - i`` that row is therefore *exactly* mirror-symmetric, which is + the corner-pivot fold identity, with its two fold poles at ``i = 0`` and + ``i = nxh/2``. ``a_j`` grows with `j` so the ellipses nest and every corner below + the seam stays distinct. + + Unlike `_fold_grid`, whose plain lat/lon coordinates do not carry the fold its + metadata declares, this one is geometrically consistent with the fold, so + transports across the seam are well defined. + """ + i_q, j_q = np.arange(nxh + 1), np.arange(ny) + i_h, j_h = np.arange(nxh) + 0.5, np.arange(ny - 1) + 0.5 + + def lonlat(jj, ii): + theta = 2.0 * np.pi * np.asarray(ii) / nxh + t = np.asarray(jj) / (ny - 1) + x = (1.0 + 0.5 * t) * np.cos(theta) + y = (1.0 - t) * np.sin(theta) + return 25.0 * x, 45.0 + 18.0 * y + + lon_c, lat_c = lonlat(*np.meshgrid(j_q, i_q, indexing="ij")) + lon_h, lat_h = lonlat(*np.meshgrid(j_h, i_h, indexing="ij")) + + ds = xr.Dataset(coords={ + "xq": i_q, "yq": j_q, "xh": i_h, "yh": j_h, + "geolon_c": (("yq", "xq"), lon_c), "geolat_c": (("yq", "xq"), lat_c), + "geolon": (("yh", "xh"), lon_h), "geolat": (("yh", "xh"), lat_h), + }) + return xgcm.Grid( + ds, + coords={"X": {"center": "xh", "outer": "xq"}, "Y": {"center": "yh", "outer": "yq"}}, + padding={"X": "periodic", "Y": {"fold": "corner"}}, + autoparse_metadata=False, + ) + + +def _streamfunction(lon, lat): + """A degree-1 spherical harmonic: smooth everywhere (poles included) and + single-valued in physical space, so seam twins receive identical values.""" + lo, la = np.deg2rad(lon), np.deg2rad(lat) + return (0.3 * np.cos(la) * np.cos(lo) + - 0.8 * np.cos(la) * np.sin(lo) + + 0.5 * np.sin(la)) + + +def _add_streamfunction_transports(grid, utr="utest", vtr="vtest"): + """Attach exactly non-divergent transports derived from `_streamfunction`. + + Returns the corner-array streamfunction and a grid carrying the transports. + `umo[j,i] = psi[j,i] - psi[j+1,i]` and `vmo[j,i] = psi[j,i+1] - psi[j,i]` make + the transport across any section telescope to `psi(end) - psi(start)`. + """ + geo = get_geo_corners(grid) + psi = _streamfunction(np.asarray(geo["X"].values), np.asarray(geo["Y"].values)) + ydim, xdim = geo["X"].dims[-2], geo["X"].dims[-1] + ycenter = grid.axes["Y"].coords["center"] + xcenter = grid.axes["X"].coords["center"] + + ds = grid._ds.copy() + ds[utr] = xr.DataArray(psi[:-1, :] - psi[1:, :], dims=(ycenter, xdim)) + ds[vtr] = xr.DataArray(psi[:, 1:] - psi[:, :-1], dims=(ydim, xcenter)) + regridded = xgcm.Grid( + ds, + coords={ax: dict(grid.axes[ax].coords) for ax in ("X", "Y")}, + padding={ax: grid.axes[ax].padding for ax in ("X", "Y")}, + autoparse_metadata=False, + ) + return psi, regridded + + +def _section_transport(grid, i_c, j_c, utr="utest", vtr="vtest"): + with warnings.catch_warnings(): + warnings.simplefilter("ignore") # open sections warn about `positive_in` + out = convergent_transport(grid, i_c, j_c, utr=utr, vtr=vtr) + return float(out["conv_mass_transport"].sum().values) + + +def test_pinched_fold_grid_carries_the_fold_symmetry(): + """The fixture really does duplicate its seam row, and the streamfunction + transports really do reproduce the ORCA duplicate-with-sign-flip structure.""" + nxh, ny = 16, 6 + grid = _pinched_fold_grid(nxh, ny) + geo = get_geo_corners(grid) + lon, lat = np.asarray(geo["X"].values), np.asarray(geo["Y"].values) + top = ny - 1 + + # Seam row: corner i and its mirror nxh-i are the same physical point ... + mirrored = distance_on_unit_sphere(lon[top], lat[top], lon[top, ::-1], lat[top, ::-1]) + assert np.nanmax(mirrored) < 1.0e-6 + # ... while the row just below it holds two genuinely distinct halves. + below = distance_on_unit_sphere(lon[top-1], lat[top-1], lon[top-1, ::-1], lat[top-1, ::-1]) + assert below[1:nxh // 2].min() > 1.0e4 + + psi, tgrid = _add_streamfunction_transports(grid) + v = tgrid._ds["vtest"].values + # The seam row's V transports are duplicates carrying a sign flip, and the + # transports are exactly non-divergent -- neither was put in by hand. + assert np.abs(v[top] + v[top, ::-1]).max() < 1.0e-12 * max(np.abs(v[top]).max(), 1.0) + u = tgrid._ds["utest"].values + conv = (u[:, :-1] - u[:, 1:]) + (v[:-1, :] - v[1:, :]) + assert np.abs(conv).max() < 1.0e-12 + + +def test_transport_crossing_the_fold_seam_is_exact(): + """A path that goes up to the seam and back down the mirrored side must transport + exactly `psi(end) - psi(start)`, using each velocity face once. + + Before the seam-twin fix this crossing was attributed to the mirror column's face + with the wrong sign, so the total was wrong by ~2x that face's transport. + """ + nxh, ny = 16, 6 + top = ny - 1 + psi, grid = _add_streamfunction_transports(_pinched_fold_grid(nxh, ny)) + + for i0 in (3, 5, 11): + mirror = nxh - i0 + # up to the seam at column i0, across the fold, back down at the mirror column + j_c = np.array([top - 2, top - 1, top, top - 1, top - 2]) + i_c = np.array([i0, i0, i0, mirror, mirror]) + + got = _section_transport(grid, i_c, j_c) + want = psi[j_c[-1], i_c[-1]] - psi[j_c[0], i_c[0]] + assert got == pytest.approx(want, abs=1.0e-12), f"column {i0}" + + # no velocity face is used twice, and the zero-length twin edge emits none + uv = uvindices_from_qindices(grid, i_c, j_c) + faces = list(zip(uv["var"].tolist(), uv["i"].tolist(), uv["j"].tolist())) + assert len(faces) == len(set(faces)) == 4 + + +def test_transport_along_the_fold_seam_is_exact(): + """Running *along* the duplicated, sign-flipped seam row needs no fold-specific + handling: the traversal direction and the stored velocity share an index frame, + so the flip cancels. This already held before the fix and must keep holding.""" + nxh, ny = 16, 6 + top = ny - 1 + psi, grid = _add_streamfunction_transports(_pinched_fold_grid(nxh, ny)) + + for i0, i1 in ((2, 6), (6, 12), (4, 12)): # last one straddles the fold pivot + i_c = np.concatenate([np.arange(i0, i1 + 1), [i1]]) + j_c = np.concatenate([np.full(i1 - i0 + 1, top), [top - 1]]) + got = _section_transport(grid, i_c, j_c) + want = psi[j_c[-1], i_c[-1]] - psi[j_c[0], i_c[0]] + assert got == pytest.approx(want, abs=1.0e-12), f"columns {i0}..{i1}" + + +def test_fold_crossing_raises_when_coordinates_lack_the_fold_symmetry(): + """`_fold_grid` declares a fold its plain lat/lon coordinates do not carry, so the + seam has no twins and the crossing edge is genuinely undefined. Say so, rather + than silently attributing it to some other face.""" + nxh, ny = 8, 5 + grid = _fold_grid(nxh, ny) + j_c = np.array([ny - 2, ny - 1, ny - 2]) + i_c = np.array([3, 3, nxh - 3]) + with pytest.raises(ValueError, match="fold"): + uvindices_from_qindices(grid, i_c, j_c) + + +@pytest.mark.skipif( + not os.path.exists(_mom6_example_path()), + reason="MOM6 example dataset not present (download via examples/load_example_model_grid.py)", +) +def test_fold_crossing_transport_is_exact_on_real_tripolar_grid(): + """The real-grid counterpart: traced sections that cross the tripolar seam -- + a closed Arctic latitude circle and an open path over the pole -- transport + exactly `psi(end) - psi(start)`, and use no velocity face twice.""" + ds = xr.open_dataset(_mom6_example_path()) + coords = {"X": {"center": "xh", "outer": "xq"}, "Y": {"center": "yh", "outer": "yq"}} + grid = xgcm.Grid(ds, coords=coords, padding={"X": "periodic", "Y": {"fold": "corner"}}, + autoparse_metadata=False) + psi, tgrid = _add_streamfunction_transports(grid) + + sections = { + "latitude circle 80N": (np.arange(0.0, 360.0 + 5.0, 5.0), [80.0] * 73, "latitude circle"), + "meridional over pole": ([100.0, 100.0, 280.0, 280.0], [60.0, 89.0, 89.0, 60.0], "great circle"), + } + for name, (lons, lats, curve) in sections.items(): + i_c, j_c, _, _ = grid_section(grid, lons, lats, curve=curve) + assert j_c.max() == ds.yq.size - 1, f"{name} never reached the seam" + + got = _section_transport(tgrid, i_c, j_c) + want = psi[j_c[-1], i_c[-1]] - psi[j_c[0], i_c[0]] + assert got == pytest.approx(want, abs=1.0e-12), name + + uv = uvindices_from_qindices(grid, i_c, j_c) + faces = list(zip(uv["var"].tolist(), uv["i"].tolist(), uv["j"].tolist())) + assert len(faces) == len(set(faces)), f"{name} counted a velocity face twice" diff --git a/sectionate/transports.py b/sectionate/transports.py index 5e24a91..dff23f3 100644 --- a/sectionate/transports.py +++ b/sectionate/transports.py @@ -115,6 +115,113 @@ def _uv_for_edge(A, B, neighbor_maps, offset, ranges, glon, glat): return "0", 0, 0, int(fB), 0 +def _axis_periodic(grid, axis): + """Whether `axis` wraps, so its first and last index columns are neighbors.""" + ax = grid.axes.get(axis) + return (ax is not None) and (ax.padding == "periodic") + + +def _index_adjacent(ja, ia, jb, ib, ny, nx, y_periodic, x_periodic): + """ + Whether two corners are neighbors in the *raw index* lattice -- the assumption the + single-tile velocity-face arithmetic in `uvindices_from_qindices` is built on. A + wrap across a periodic axis counts, since that arithmetic handles it explicitly. + """ + dj, di = int(jb) - int(ja), int(ib) - int(ia) + if dj == 0 and di == 0: + return True # degenerate; dropped downstream + if dj == 0: + return abs(di) == 1 or (x_periodic and abs(di) == nx - 1) + if di == 0: + return abs(dj) == 1 or (y_periodic and abs(dj) == ny - 1) + return False + + +def _adjacent_candidates(j, i, ny, nx, y_periodic, x_periodic): + """Every corner index adjacent to (j,i) in the raw index lattice, in a fixed order.""" + cand = [(j, i - 1), (j, i + 1), (j - 1, i), (j + 1, i)] + if x_periodic: + cand += [(j, 0), (j, nx - 1)] + if y_periodic: + cand += [(0, i), (ny - 1, i)] + out = [] + for (jj, ii) in cand: + if ((0 <= jj < ny) and (0 <= ii < nx) and ((jj, ii) not in out) + and _index_adjacent(j, i, jj, ii, ny, nx, y_periodic, x_periodic)): + out.append((jj, ii)) + return out + + +def _insert_seam_twins(i_c, j_c, glon, glat, grid): + """ + Splice in seam twins so that every consecutive pair of corners is index-adjacent. + + A seam gives one physical corner two index representations. The bipolar north fold is + the single-tile case: it identifies each seam-row corner with its mirrored column, so + the walk can step from one representation straight to a corner adjacent to the *other*. + That pair is a real physical edge but not an index-adjacent one, and the arithmetic in + `uvindices_from_qindices` reads the velocity column off the source corner -- which is + then the wrong representative. It names the mirror image of the face the section + actually crossed, and (because the mirrored half runs the opposite way in index space) + with the opposite sign. On a closed fold-crossing section that also emits one face + twice, double-counting it. + + Splicing the twin in restores the invariant the arithmetic needs: the crossing becomes + one ordinary edge plus one zero-length twin edge, and zero-length edges are dropped at + the end of `uvindices_from_qindices`, so nothing is counted twice. This is the + single-tile counterpart of the corner canonicalization `_OuterTopology` performs for + multi-tile grids; NEMO reaches the same place from the other direction, carrying the + duplicated points and masking them out of transports with `umaskutil`/`vmaskutil`. + + Runs on every single-tile grid and is a no-op unless a step actually skips the twin, so + grids without seam duplicates (and fold sections that never cross the seam) are untouched. + """ + if len(i_c) < 2: + return np.asarray(i_c), np.asarray(j_c) + ny, nx = glon.shape + y_periodic, x_periodic = _axis_periodic(grid, "Y"), _axis_periodic(grid, "X") + + def coincident(ja, ia, jb, ib): + return distance_on_unit_sphere( + glon[ja, ia], glat[ja, ia], glon[jb, ib], glat[jb, ib] + ) < COINCIDENT_TOLERANCE_M + + out_i, out_j = [int(i_c[0])], [int(j_c[0])] + for k in range(len(i_c) - 1): + ja, ia = int(j_c[k]), int(i_c[k]) + jb, ib = int(j_c[k + 1]), int(i_c[k + 1]) + if not _index_adjacent(ja, ia, jb, ib, ny, nx, y_periodic, x_periodic): + # Either a twin of A adjacent to B, or a twin of B adjacent to A, turns the + # step into two index-adjacent ones. Which of the two exists depends on + # whether the section is entering or leaving the seam. + twin = next( + (t for t in _adjacent_candidates(jb, ib, ny, nx, y_periodic, x_periodic) + if coincident(*t, ja, ia)), + None, + ) + if twin is None: + twin = next( + (t for t in _adjacent_candidates(ja, ia, ny, nx, y_periodic, x_periodic) + if coincident(*t, jb, ib)), + None, + ) + if twin is None: + raise ValueError( + f"Section steps between corners (j={ja}, i={ia}) and (j={jb}, i={ib}), " + "which are neither adjacent in index space nor separated by a seam twin, " + "so the velocity face between them is undefined. On a grid with a bipolar " + "fold this means the corner coordinates do not carry the fold symmetry the " + "grid declares (the seam row's mirrored columns should hold identical " + "coordinates)." + ) + out_j.append(twin[0]) + out_i.append(twin[1]) + out_j.append(jb) + out_i.append(ib) + + return np.asarray(out_i), np.asarray(out_j) + + def uvindices_from_qindices(grid, i_c, j_c, f_c=None): """ Find the `grid` indices of the N-1 velocity points defined by the consecutive indices of @@ -171,6 +278,13 @@ def uvindices_from_qindices(grid, i_c, j_c, f_c=None): keep[1:] = nodes[1:] != nodes[:-1] nat = ot.node_native[nodes[keep]] f_c, j_c, i_c = nat[:, 0].copy(), nat[:, 1].copy(), nat[:, 2].copy() + else: + # Single-tile grids have no face topology to canonicalize corners through, but + # they can still carry seam duplicates: a bipolar north fold identifies every + # seam-row corner with its mirrored column. Splice those twins in so that the + # index arithmetic below -- which assumes consecutive corners are index-adjacent + # -- reads the velocity face the section actually crossed. See `_insert_seam_twins`. + i_c, j_c = _insert_seam_twins(i_c, j_c, glon, glat, grid) nsec = i_c.size uvindices = { From bdf6fdfcba756ef3199b19854c2b0bc81f1901b1 Mon Sep 17 00:00:00 2001 From: Henri Drake Date: Tue, 11 Aug 2026 11:17:55 -0700 Subject: [PATCH 2/3] Re-execute the example notebooks on this branch No figure or numerical output changed -- none of the notebooks compute transports across a single-tile fold seam -- only execution metadata and the line number in a captured UserWarning traceback. Co-Authored-By: Claude Opus 5 (1M context) --- examples/1_creating_an_OSNAP_section.ipynb | 180 +++++++++--------- examples/2_OSNAP_transports_CM4p25.ipynb | 178 ++++++++--------- examples/3_Labrador_convergence_CM4p25.ipynb | 76 ++++---- .../4_sections_on_global_tripolar_grid.ipynb | 140 +++++++------- examples/5_MOC_transports_ECCOv4r4.ipynb | 50 ++--- 5 files changed, 312 insertions(+), 312 deletions(-) diff --git a/examples/1_creating_an_OSNAP_section.ipynb b/examples/1_creating_an_OSNAP_section.ipynb index 7f58a59..82fcafb 100644 --- a/examples/1_creating_an_OSNAP_section.ipynb +++ b/examples/1_creating_an_OSNAP_section.ipynb @@ -23,10 +23,10 @@ "execution_count": 1, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:42.488689Z", - "iopub.status.busy": "2026-08-04T02:01:42.488449Z", - "iopub.status.idle": "2026-08-04T02:01:45.442158Z", - "shell.execute_reply": "2026-08-04T02:01:45.441763Z" + "iopub.execute_input": "2026-08-11T18:13:51.227132Z", + "iopub.status.busy": "2026-08-11T18:13:51.226904Z", + "iopub.status.idle": "2026-08-11T18:13:53.583670Z", + "shell.execute_reply": "2026-08-11T18:13:53.583387Z" } }, "outputs": [ @@ -34,7 +34,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Sectionate version: 0.4.0rc2.dev3\n" + "Sectionate version: 0.4.0rc3.dev0\n" ] } ], @@ -74,10 +74,10 @@ "execution_count": 2, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:45.456437Z", - "iopub.status.busy": "2026-08-04T02:01:45.456296Z", - "iopub.status.idle": "2026-08-04T02:01:45.489296Z", - "shell.execute_reply": "2026-08-04T02:01:45.488946Z" + "iopub.execute_input": "2026-08-11T18:13:53.605094Z", + "iopub.status.busy": "2026-08-11T18:13:53.604862Z", + "iopub.status.idle": "2026-08-11T18:13:53.695014Z", + "shell.execute_reply": "2026-08-11T18:13:53.694727Z" } }, "outputs": [ @@ -122,10 +122,10 @@ "execution_count": 3, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:45.490519Z", - "iopub.status.busy": "2026-08-04T02:01:45.490459Z", - "iopub.status.idle": "2026-08-04T02:01:45.492106Z", - "shell.execute_reply": "2026-08-04T02:01:45.491756Z" + "iopub.execute_input": "2026-08-11T18:13:53.696391Z", + "iopub.status.busy": "2026-08-11T18:13:53.696310Z", + "iopub.status.idle": "2026-08-11T18:13:53.697927Z", + "shell.execute_reply": "2026-08-11T18:13:53.697718Z" } }, "outputs": [], @@ -147,10 +147,10 @@ "execution_count": 4, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:45.493150Z", - "iopub.status.busy": "2026-08-04T02:01:45.493100Z", - "iopub.status.idle": "2026-08-04T02:01:45.875493Z", - "shell.execute_reply": "2026-08-04T02:01:45.874991Z" + "iopub.execute_input": "2026-08-11T18:13:53.699136Z", + "iopub.status.busy": "2026-08-11T18:13:53.699057Z", + "iopub.status.idle": "2026-08-11T18:13:54.292906Z", + "shell.execute_reply": "2026-08-11T18:13:54.292549Z" } }, "outputs": [], @@ -167,10 +167,10 @@ "execution_count": 5, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:45.876703Z", - "iopub.status.busy": "2026-08-04T02:01:45.876630Z", - "iopub.status.idle": "2026-08-04T02:01:46.650395Z", - "shell.execute_reply": "2026-08-04T02:01:46.649969Z" + "iopub.execute_input": "2026-08-11T18:13:54.294387Z", + "iopub.status.busy": "2026-08-11T18:13:54.294305Z", + "iopub.status.idle": "2026-08-11T18:13:55.137695Z", + "shell.execute_reply": "2026-08-11T18:13:55.137360Z" } }, "outputs": [ @@ -218,10 +218,10 @@ "execution_count": 6, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:46.651743Z", - "iopub.status.busy": "2026-08-04T02:01:46.651652Z", - "iopub.status.idle": "2026-08-04T02:01:46.673811Z", - "shell.execute_reply": "2026-08-04T02:01:46.673484Z" + "iopub.execute_input": "2026-08-11T18:13:55.140166Z", + "iopub.status.busy": "2026-08-11T18:13:55.140060Z", + "iopub.status.idle": "2026-08-11T18:13:55.166293Z", + "shell.execute_reply": "2026-08-11T18:13:55.165848Z" } }, "outputs": [], @@ -249,10 +249,10 @@ "execution_count": 7, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:46.674910Z", - "iopub.status.busy": "2026-08-04T02:01:46.674851Z", - "iopub.status.idle": "2026-08-04T02:01:46.808809Z", - "shell.execute_reply": "2026-08-04T02:01:46.808475Z" + "iopub.execute_input": "2026-08-11T18:13:55.168440Z", + "iopub.status.busy": "2026-08-11T18:13:55.168321Z", + "iopub.status.idle": "2026-08-11T18:13:55.383254Z", + "shell.execute_reply": "2026-08-11T18:13:55.382975Z" } }, "outputs": [ @@ -307,10 +307,10 @@ "execution_count": 8, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:46.810059Z", - "iopub.status.busy": "2026-08-04T02:01:46.809995Z", - "iopub.status.idle": "2026-08-04T02:01:46.912776Z", - "shell.execute_reply": "2026-08-04T02:01:46.912484Z" + "iopub.execute_input": "2026-08-11T18:13:55.384829Z", + "iopub.status.busy": "2026-08-11T18:13:55.384705Z", + "iopub.status.idle": "2026-08-11T18:13:55.571638Z", + "shell.execute_reply": "2026-08-11T18:13:55.570890Z" } }, "outputs": [ @@ -318,7 +318,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/hfdrake/miniforge3/envs/docs_env_sectionate_fix-layer-interface-matching/lib/python3.14/site-packages/xgcm/grid.py:463: UserWarning: The north-fold (tripolar) boundary condition is experimental. Its API and numerical behavior may change in future releases, and it has not yet been validated across the full range of grid configurations. Please review results carefully and report any issues at https://github.com/xgcm/xgcm/issues.\n", + "/opt/anaconda3/envs/docs_env_sectionate_fix-fold-crossing-transports/lib/python3.14t/site-packages/xgcm/grid.py:463: UserWarning: The north-fold (tripolar) boundary condition is experimental. Its API and numerical behavior may change in future releases, and it has not yet been validated across the full range of grid configurations. Please review results carefully and report any issues at https://github.com/xgcm/xgcm/issues.\n", " warnings.warn(\n" ] } @@ -343,10 +343,10 @@ "execution_count": 9, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:46.913869Z", - "iopub.status.busy": "2026-08-04T02:01:46.913814Z", - "iopub.status.idle": "2026-08-04T02:01:46.915373Z", - "shell.execute_reply": "2026-08-04T02:01:46.915068Z" + "iopub.execute_input": "2026-08-11T18:13:55.575078Z", + "iopub.status.busy": "2026-08-11T18:13:55.574948Z", + "iopub.status.idle": "2026-08-11T18:13:55.577084Z", + "shell.execute_reply": "2026-08-11T18:13:55.576764Z" } }, "outputs": [], @@ -370,10 +370,10 @@ "execution_count": 10, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:46.916215Z", - "iopub.status.busy": "2026-08-04T02:01:46.916166Z", - "iopub.status.idle": "2026-08-04T02:01:46.932327Z", - "shell.execute_reply": "2026-08-04T02:01:46.931936Z" + "iopub.execute_input": "2026-08-11T18:13:55.578526Z", + "iopub.status.busy": "2026-08-11T18:13:55.578406Z", + "iopub.status.idle": "2026-08-11T18:13:55.615644Z", + "shell.execute_reply": "2026-08-11T18:13:55.615073Z" } }, "outputs": [], @@ -390,10 +390,10 @@ "execution_count": 11, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:46.933351Z", - "iopub.status.busy": "2026-08-04T02:01:46.933294Z", - "iopub.status.idle": "2026-08-04T02:01:47.029043Z", - "shell.execute_reply": "2026-08-04T02:01:47.028616Z" + "iopub.execute_input": "2026-08-11T18:13:55.619166Z", + "iopub.status.busy": "2026-08-11T18:13:55.618896Z", + "iopub.status.idle": "2026-08-11T18:13:55.875675Z", + "shell.execute_reply": "2026-08-11T18:13:55.875273Z" } }, "outputs": [ @@ -433,10 +433,10 @@ "execution_count": 12, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:47.030083Z", - "iopub.status.busy": "2026-08-04T02:01:47.030000Z", - "iopub.status.idle": "2026-08-04T02:01:47.045217Z", - "shell.execute_reply": "2026-08-04T02:01:47.044885Z" + "iopub.execute_input": "2026-08-11T18:13:55.877087Z", + "iopub.status.busy": "2026-08-11T18:13:55.876976Z", + "iopub.status.idle": "2026-08-11T18:13:55.906195Z", + "shell.execute_reply": "2026-08-11T18:13:55.905737Z" } }, "outputs": [], @@ -453,10 +453,10 @@ "execution_count": 13, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:47.046152Z", - "iopub.status.busy": "2026-08-04T02:01:47.046094Z", - "iopub.status.idle": "2026-08-04T02:01:47.111056Z", - "shell.execute_reply": "2026-08-04T02:01:47.110790Z" + "iopub.execute_input": "2026-08-11T18:13:55.910245Z", + "iopub.status.busy": "2026-08-11T18:13:55.909992Z", + "iopub.status.idle": "2026-08-11T18:13:56.021033Z", + "shell.execute_reply": "2026-08-11T18:13:56.020748Z" } }, "outputs": [ @@ -529,10 +529,10 @@ "execution_count": 14, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:47.112255Z", - "iopub.status.busy": "2026-08-04T02:01:47.112186Z", - "iopub.status.idle": "2026-08-04T02:01:47.114192Z", - "shell.execute_reply": "2026-08-04T02:01:47.113925Z" + "iopub.execute_input": "2026-08-11T18:13:56.022672Z", + "iopub.status.busy": "2026-08-11T18:13:56.022573Z", + "iopub.status.idle": "2026-08-11T18:13:56.025055Z", + "shell.execute_reply": "2026-08-11T18:13:56.024801Z" } }, "outputs": [ @@ -593,10 +593,10 @@ "execution_count": 15, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:47.115147Z", - "iopub.status.busy": "2026-08-04T02:01:47.115093Z", - "iopub.status.idle": "2026-08-04T02:01:47.117491Z", - "shell.execute_reply": "2026-08-04T02:01:47.117191Z" + "iopub.execute_input": "2026-08-11T18:13:56.026353Z", + "iopub.status.busy": "2026-08-11T18:13:56.026253Z", + "iopub.status.idle": "2026-08-11T18:13:56.030299Z", + "shell.execute_reply": "2026-08-11T18:13:56.030053Z" } }, "outputs": [ @@ -644,10 +644,10 @@ "execution_count": 16, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:47.118350Z", - "iopub.status.busy": "2026-08-04T02:01:47.118300Z", - "iopub.status.idle": "2026-08-04T02:01:47.120829Z", - "shell.execute_reply": "2026-08-04T02:01:47.120449Z" + "iopub.execute_input": "2026-08-11T18:13:56.031550Z", + "iopub.status.busy": "2026-08-11T18:13:56.031475Z", + "iopub.status.idle": "2026-08-11T18:13:56.034202Z", + "shell.execute_reply": "2026-08-11T18:13:56.033847Z" } }, "outputs": [ @@ -702,10 +702,10 @@ "execution_count": 17, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:47.121682Z", - "iopub.status.busy": "2026-08-04T02:01:47.121634Z", - "iopub.status.idle": "2026-08-04T02:01:47.123385Z", - "shell.execute_reply": "2026-08-04T02:01:47.123069Z" + "iopub.execute_input": "2026-08-11T18:13:56.035749Z", + "iopub.status.busy": "2026-08-11T18:13:56.035645Z", + "iopub.status.idle": "2026-08-11T18:13:56.037505Z", + "shell.execute_reply": "2026-08-11T18:13:56.037259Z" } }, "outputs": [ @@ -745,10 +745,10 @@ "execution_count": 18, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:47.124215Z", - "iopub.status.busy": "2026-08-04T02:01:47.124167Z", - "iopub.status.idle": "2026-08-04T02:01:47.162616Z", - "shell.execute_reply": "2026-08-04T02:01:47.162264Z" + "iopub.execute_input": "2026-08-11T18:13:56.038815Z", + "iopub.status.busy": "2026-08-11T18:13:56.038727Z", + "iopub.status.idle": "2026-08-11T18:13:56.106945Z", + "shell.execute_reply": "2026-08-11T18:13:56.105751Z" } }, "outputs": [], @@ -765,10 +765,10 @@ "execution_count": 19, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:47.163631Z", - "iopub.status.busy": "2026-08-04T02:01:47.163580Z", - "iopub.status.idle": "2026-08-04T02:01:47.255905Z", - "shell.execute_reply": "2026-08-04T02:01:47.255590Z" + "iopub.execute_input": "2026-08-11T18:13:56.109208Z", + "iopub.status.busy": "2026-08-11T18:13:56.109074Z", + "iopub.status.idle": "2026-08-11T18:13:56.257673Z", + "shell.execute_reply": "2026-08-11T18:13:56.257292Z" } }, "outputs": [ @@ -817,10 +817,10 @@ "execution_count": 20, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:47.256958Z", - "iopub.status.busy": "2026-08-04T02:01:47.256900Z", - "iopub.status.idle": "2026-08-04T02:01:47.296309Z", - "shell.execute_reply": "2026-08-04T02:01:47.295955Z" + "iopub.execute_input": "2026-08-11T18:13:56.259429Z", + "iopub.status.busy": "2026-08-11T18:13:56.259319Z", + "iopub.status.idle": "2026-08-11T18:13:56.313586Z", + "shell.execute_reply": "2026-08-11T18:13:56.313274Z" } }, "outputs": [ @@ -868,10 +868,10 @@ "execution_count": 21, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:47.297356Z", - "iopub.status.busy": "2026-08-04T02:01:47.297301Z", - "iopub.status.idle": "2026-08-04T02:01:47.299807Z", - "shell.execute_reply": "2026-08-04T02:01:47.299484Z" + "iopub.execute_input": "2026-08-11T18:13:56.314870Z", + "iopub.status.busy": "2026-08-11T18:13:56.314780Z", + "iopub.status.idle": "2026-08-11T18:13:56.319651Z", + "shell.execute_reply": "2026-08-11T18:13:56.319388Z" } }, "outputs": [], @@ -895,10 +895,10 @@ "execution_count": 22, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:47.300821Z", - "iopub.status.busy": "2026-08-04T02:01:47.300769Z", - "iopub.status.idle": "2026-08-04T02:01:47.393286Z", - "shell.execute_reply": "2026-08-04T02:01:47.392961Z" + "iopub.execute_input": "2026-08-11T18:13:56.320980Z", + "iopub.status.busy": "2026-08-11T18:13:56.320909Z", + "iopub.status.idle": "2026-08-11T18:13:56.450492Z", + "shell.execute_reply": "2026-08-11T18:13:56.450206Z" } }, "outputs": [ diff --git a/examples/2_OSNAP_transports_CM4p25.ipynb b/examples/2_OSNAP_transports_CM4p25.ipynb index a1e02c6..59ab903 100644 --- a/examples/2_OSNAP_transports_CM4p25.ipynb +++ b/examples/2_OSNAP_transports_CM4p25.ipynb @@ -21,10 +21,10 @@ "execution_count": 1, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:48.388420Z", - "iopub.status.busy": "2026-08-04T02:01:48.388248Z", - "iopub.status.idle": "2026-08-04T02:01:49.080762Z", - "shell.execute_reply": "2026-08-04T02:01:49.080434Z" + "iopub.execute_input": "2026-08-11T18:13:57.587869Z", + "iopub.status.busy": "2026-08-11T18:13:57.587724Z", + "iopub.status.idle": "2026-08-11T18:13:58.798097Z", + "shell.execute_reply": "2026-08-11T18:13:58.797780Z" } }, "outputs": [ @@ -32,7 +32,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Sectionate version: 0.4.0rc2.dev3\n" + "Sectionate version: 0.4.0rc3.dev0\n" ] } ], @@ -60,10 +60,10 @@ "execution_count": 2, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:49.082054Z", - "iopub.status.busy": "2026-08-04T02:01:49.081933Z", - "iopub.status.idle": "2026-08-04T02:01:49.208403Z", - "shell.execute_reply": "2026-08-04T02:01:49.207974Z" + "iopub.execute_input": "2026-08-11T18:13:58.799532Z", + "iopub.status.busy": "2026-08-11T18:13:58.799367Z", + "iopub.status.idle": "2026-08-11T18:13:59.000995Z", + "shell.execute_reply": "2026-08-11T18:13:59.000540Z" } }, "outputs": [ @@ -71,7 +71,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/hfdrake/miniforge3/envs/docs_env_sectionate_fix-layer-interface-matching/lib/python3.14/site-packages/xgcm/grid.py:463: UserWarning: The north-fold (tripolar) boundary condition is experimental. Its API and numerical behavior may change in future releases, and it has not yet been validated across the full range of grid configurations. Please review results carefully and report any issues at https://github.com/xgcm/xgcm/issues.\n", + "/opt/anaconda3/envs/docs_env_sectionate_fix-fold-crossing-transports/lib/python3.14t/site-packages/xgcm/grid.py:463: UserWarning: The north-fold (tripolar) boundary condition is experimental. Its API and numerical behavior may change in future releases, and it has not yet been validated across the full range of grid configurations. Please review results carefully and report any issues at https://github.com/xgcm/xgcm/issues.\n", " warnings.warn(\n" ] } @@ -94,10 +94,10 @@ "execution_count": 3, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:49.209584Z", - "iopub.status.busy": "2026-08-04T02:01:49.209516Z", - "iopub.status.idle": "2026-08-04T02:01:49.211241Z", - "shell.execute_reply": "2026-08-04T02:01:49.210950Z" + "iopub.execute_input": "2026-08-11T18:13:59.003387Z", + "iopub.status.busy": "2026-08-11T18:13:59.003078Z", + "iopub.status.idle": "2026-08-11T18:13:59.010771Z", + "shell.execute_reply": "2026-08-11T18:13:59.006555Z" } }, "outputs": [], @@ -124,10 +124,10 @@ "execution_count": 4, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:49.212231Z", - "iopub.status.busy": "2026-08-04T02:01:49.212180Z", - "iopub.status.idle": "2026-08-04T02:01:49.226750Z", - "shell.execute_reply": "2026-08-04T02:01:49.226317Z" + "iopub.execute_input": "2026-08-11T18:13:59.025892Z", + "iopub.status.busy": "2026-08-11T18:13:59.024237Z", + "iopub.status.idle": "2026-08-11T18:13:59.116734Z", + "shell.execute_reply": "2026-08-11T18:13:59.116391Z" } }, "outputs": [], @@ -144,10 +144,10 @@ "execution_count": 5, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:49.227864Z", - "iopub.status.busy": "2026-08-04T02:01:49.227804Z", - "iopub.status.idle": "2026-08-04T02:01:49.343097Z", - "shell.execute_reply": "2026-08-04T02:01:49.342741Z" + "iopub.execute_input": "2026-08-11T18:13:59.118993Z", + "iopub.status.busy": "2026-08-11T18:13:59.118861Z", + "iopub.status.idle": "2026-08-11T18:13:59.367867Z", + "shell.execute_reply": "2026-08-11T18:13:59.367497Z" }, "tags": [] }, @@ -195,10 +195,10 @@ "execution_count": 6, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:49.344132Z", - "iopub.status.busy": "2026-08-04T02:01:49.344069Z", - "iopub.status.idle": "2026-08-04T02:01:49.364171Z", - "shell.execute_reply": "2026-08-04T02:01:49.363778Z" + "iopub.execute_input": "2026-08-11T18:13:59.369696Z", + "iopub.status.busy": "2026-08-11T18:13:59.369596Z", + "iopub.status.idle": "2026-08-11T18:13:59.417103Z", + "shell.execute_reply": "2026-08-11T18:13:59.416861Z" } }, "outputs": [ @@ -206,7 +206,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/hfdrake/code/sectionate/sectionate/transports.py:565: UserWarning: This section is open (its first and last waypoints are far apart), so it does not enclose a polygon and the inward/outward sense of `positive_in` is undefined. Sectionate applies the left-of-transect convention instead: with `positive_in=True` positive transport points to the LEFT of the section as it is traversed from the first to the last waypoint (set `positive_in=False` to flip to right-of-transect). Verify the sign matches your expectations, e.g. by plotting the section's normal vectors.\n", + "/Users/hfdrake/code/sectionate/sectionate/transports.py:675: UserWarning: This section is open (its first and last waypoints are far apart), so it does not enclose a polygon and the inward/outward sense of `positive_in` is undefined. Sectionate applies the left-of-transect convention instead: with `positive_in=True` positive transport points to the LEFT of the section as it is traversed from the first to the last waypoint (set `positive_in=False` to flip to right-of-transect). Verify the sign matches your expectations, e.g. by plotting the section's normal vectors.\n", " warnings.warn(\n" ] } @@ -220,10 +220,10 @@ "execution_count": 7, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:49.365281Z", - "iopub.status.busy": "2026-08-04T02:01:49.365213Z", - "iopub.status.idle": "2026-08-04T02:01:49.446445Z", - "shell.execute_reply": "2026-08-04T02:01:49.446021Z" + "iopub.execute_input": "2026-08-11T18:13:59.418404Z", + "iopub.status.busy": "2026-08-11T18:13:59.418318Z", + "iopub.status.idle": "2026-08-11T18:13:59.538455Z", + "shell.execute_reply": "2026-08-11T18:13:59.538141Z" } }, "outputs": [ @@ -255,10 +255,10 @@ "execution_count": 8, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:49.447595Z", - "iopub.status.busy": "2026-08-04T02:01:49.447529Z", - "iopub.status.idle": "2026-08-04T02:01:49.522301Z", - "shell.execute_reply": "2026-08-04T02:01:49.521934Z" + "iopub.execute_input": "2026-08-11T18:13:59.540158Z", + "iopub.status.busy": "2026-08-11T18:13:59.540062Z", + "iopub.status.idle": "2026-08-11T18:13:59.689274Z", + "shell.execute_reply": "2026-08-11T18:13:59.688872Z" } }, "outputs": [ @@ -317,10 +317,10 @@ "execution_count": 9, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:49.523408Z", - "iopub.status.busy": "2026-08-04T02:01:49.523349Z", - "iopub.status.idle": "2026-08-04T02:01:49.524914Z", - "shell.execute_reply": "2026-08-04T02:01:49.524639Z" + "iopub.execute_input": "2026-08-11T18:13:59.690890Z", + "iopub.status.busy": "2026-08-11T18:13:59.690784Z", + "iopub.status.idle": "2026-08-11T18:13:59.692701Z", + "shell.execute_reply": "2026-08-11T18:13:59.692421Z" } }, "outputs": [], @@ -340,10 +340,10 @@ "execution_count": 10, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:49.525806Z", - "iopub.status.busy": "2026-08-04T02:01:49.525746Z", - "iopub.status.idle": "2026-08-04T02:01:49.540458Z", - "shell.execute_reply": "2026-08-04T02:01:49.540125Z" + "iopub.execute_input": "2026-08-11T18:13:59.694298Z", + "iopub.status.busy": "2026-08-11T18:13:59.694195Z", + "iopub.status.idle": "2026-08-11T18:13:59.719846Z", + "shell.execute_reply": "2026-08-11T18:13:59.719152Z" } }, "outputs": [], @@ -360,10 +360,10 @@ "execution_count": 11, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:49.541502Z", - "iopub.status.busy": "2026-08-04T02:01:49.541448Z", - "iopub.status.idle": "2026-08-04T02:01:49.631238Z", - "shell.execute_reply": "2026-08-04T02:01:49.630940Z" + "iopub.execute_input": "2026-08-11T18:13:59.722608Z", + "iopub.status.busy": "2026-08-11T18:13:59.722483Z", + "iopub.status.idle": "2026-08-11T18:13:59.887954Z", + "shell.execute_reply": "2026-08-11T18:13:59.887650Z" } }, "outputs": [ @@ -410,10 +410,10 @@ "execution_count": 12, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:49.632524Z", - "iopub.status.busy": "2026-08-04T02:01:49.632463Z", - "iopub.status.idle": "2026-08-04T02:01:49.670014Z", - "shell.execute_reply": "2026-08-04T02:01:49.669657Z" + "iopub.execute_input": "2026-08-11T18:13:59.889643Z", + "iopub.status.busy": "2026-08-11T18:13:59.889548Z", + "iopub.status.idle": "2026-08-11T18:13:59.933828Z", + "shell.execute_reply": "2026-08-11T18:13:59.933554Z" } }, "outputs": [ @@ -421,7 +421,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/hfdrake/code/sectionate/sectionate/transports.py:565: UserWarning: This section is open (its first and last waypoints are far apart), so it does not enclose a polygon and the inward/outward sense of `positive_in` is undefined. Sectionate applies the left-of-transect convention instead: with `positive_in=True` positive transport points to the LEFT of the section as it is traversed from the first to the last waypoint (set `positive_in=False` to flip to right-of-transect). Verify the sign matches your expectations, e.g. by plotting the section's normal vectors.\n", + "/Users/hfdrake/code/sectionate/sectionate/transports.py:675: UserWarning: This section is open (its first and last waypoints are far apart), so it does not enclose a polygon and the inward/outward sense of `positive_in` is undefined. Sectionate applies the left-of-transect convention instead: with `positive_in=True` positive transport points to the LEFT of the section as it is traversed from the first to the last waypoint (set `positive_in=False` to flip to right-of-transect). Verify the sign matches your expectations, e.g. by plotting the section's normal vectors.\n", " warnings.warn(\n" ] } @@ -435,10 +435,10 @@ "execution_count": 13, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:49.671028Z", - "iopub.status.busy": "2026-08-04T02:01:49.670972Z", - "iopub.status.idle": "2026-08-04T02:01:49.749043Z", - "shell.execute_reply": "2026-08-04T02:01:49.748684Z" + "iopub.execute_input": "2026-08-11T18:13:59.935275Z", + "iopub.status.busy": "2026-08-11T18:13:59.935180Z", + "iopub.status.idle": "2026-08-11T18:14:00.046029Z", + "shell.execute_reply": "2026-08-11T18:14:00.045755Z" } }, "outputs": [ @@ -470,10 +470,10 @@ "execution_count": 14, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:49.750155Z", - "iopub.status.busy": "2026-08-04T02:01:49.750092Z", - "iopub.status.idle": "2026-08-04T02:01:49.836438Z", - "shell.execute_reply": "2026-08-04T02:01:49.836017Z" + "iopub.execute_input": "2026-08-11T18:14:00.047501Z", + "iopub.status.busy": "2026-08-11T18:14:00.047391Z", + "iopub.status.idle": "2026-08-11T18:14:00.180084Z", + "shell.execute_reply": "2026-08-11T18:14:00.179615Z" } }, "outputs": [ @@ -530,10 +530,10 @@ "execution_count": 15, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:49.837429Z", - "iopub.status.busy": "2026-08-04T02:01:49.837368Z", - "iopub.status.idle": "2026-08-04T02:01:49.838781Z", - "shell.execute_reply": "2026-08-04T02:01:49.838537Z" + "iopub.execute_input": "2026-08-11T18:14:00.181807Z", + "iopub.status.busy": "2026-08-11T18:14:00.181697Z", + "iopub.status.idle": "2026-08-11T18:14:00.183489Z", + "shell.execute_reply": "2026-08-11T18:14:00.183141Z" } }, "outputs": [], @@ -553,10 +553,10 @@ "execution_count": 16, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:49.839688Z", - "iopub.status.busy": "2026-08-04T02:01:49.839633Z", - "iopub.status.idle": "2026-08-04T02:01:49.867487Z", - "shell.execute_reply": "2026-08-04T02:01:49.867055Z" + "iopub.execute_input": "2026-08-11T18:14:00.184714Z", + "iopub.status.busy": "2026-08-11T18:14:00.184639Z", + "iopub.status.idle": "2026-08-11T18:14:00.222897Z", + "shell.execute_reply": "2026-08-11T18:14:00.222555Z" } }, "outputs": [], @@ -573,10 +573,10 @@ "execution_count": 17, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:49.868502Z", - "iopub.status.busy": "2026-08-04T02:01:49.868449Z", - "iopub.status.idle": "2026-08-04T02:01:49.898245Z", - "shell.execute_reply": "2026-08-04T02:01:49.897927Z" + "iopub.execute_input": "2026-08-11T18:14:00.224889Z", + "iopub.status.busy": "2026-08-11T18:14:00.224779Z", + "iopub.status.idle": "2026-08-11T18:14:00.265665Z", + "shell.execute_reply": "2026-08-11T18:14:00.265387Z" } }, "outputs": [ @@ -584,7 +584,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/hfdrake/code/sectionate/sectionate/transports.py:565: UserWarning: This section is open (its first and last waypoints are far apart), so it does not enclose a polygon and the inward/outward sense of `positive_in` is undefined. Sectionate applies the left-of-transect convention instead: with `positive_in=True` positive transport points to the LEFT of the section as it is traversed from the first to the last waypoint (set `positive_in=False` to flip to right-of-transect). Verify the sign matches your expectations, e.g. by plotting the section's normal vectors.\n", + "/Users/hfdrake/code/sectionate/sectionate/transports.py:675: UserWarning: This section is open (its first and last waypoints are far apart), so it does not enclose a polygon and the inward/outward sense of `positive_in` is undefined. Sectionate applies the left-of-transect convention instead: with `positive_in=True` positive transport points to the LEFT of the section as it is traversed from the first to the last waypoint (set `positive_in=False` to flip to right-of-transect). Verify the sign matches your expectations, e.g. by plotting the section's normal vectors.\n", " warnings.warn(\n" ] } @@ -598,10 +598,10 @@ "execution_count": 18, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:49.899254Z", - "iopub.status.busy": "2026-08-04T02:01:49.899198Z", - "iopub.status.idle": "2026-08-04T02:01:50.002350Z", - "shell.execute_reply": "2026-08-04T02:01:50.001979Z" + "iopub.execute_input": "2026-08-11T18:14:00.266927Z", + "iopub.status.busy": "2026-08-11T18:14:00.266841Z", + "iopub.status.idle": "2026-08-11T18:14:00.427704Z", + "shell.execute_reply": "2026-08-11T18:14:00.427414Z" } }, "outputs": [ @@ -658,10 +658,10 @@ "execution_count": 19, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:50.003392Z", - "iopub.status.busy": "2026-08-04T02:01:50.003333Z", - "iopub.status.idle": "2026-08-04T02:01:50.004876Z", - "shell.execute_reply": "2026-08-04T02:01:50.004533Z" + "iopub.execute_input": "2026-08-11T18:14:00.429225Z", + "iopub.status.busy": "2026-08-11T18:14:00.429128Z", + "iopub.status.idle": "2026-08-11T18:14:00.430782Z", + "shell.execute_reply": "2026-08-11T18:14:00.430538Z" } }, "outputs": [], @@ -681,10 +681,10 @@ "execution_count": 20, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:50.005800Z", - "iopub.status.busy": "2026-08-04T02:01:50.005751Z", - "iopub.status.idle": "2026-08-04T02:01:50.008642Z", - "shell.execute_reply": "2026-08-04T02:01:50.008176Z" + "iopub.execute_input": "2026-08-11T18:14:00.432079Z", + "iopub.status.busy": "2026-08-11T18:14:00.432005Z", + "iopub.status.idle": "2026-08-11T18:14:00.435618Z", + "shell.execute_reply": "2026-08-11T18:14:00.435397Z" } }, "outputs": [], @@ -709,10 +709,10 @@ "execution_count": 21, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:50.009563Z", - "iopub.status.busy": "2026-08-04T02:01:50.009509Z", - "iopub.status.idle": "2026-08-04T02:01:50.084867Z", - "shell.execute_reply": "2026-08-04T02:01:50.084458Z" + "iopub.execute_input": "2026-08-11T18:14:00.436928Z", + "iopub.status.busy": "2026-08-11T18:14:00.436853Z", + "iopub.status.idle": "2026-08-11T18:14:00.542888Z", + "shell.execute_reply": "2026-08-11T18:14:00.542452Z" } }, "outputs": [ diff --git a/examples/3_Labrador_convergence_CM4p25.ipynb b/examples/3_Labrador_convergence_CM4p25.ipynb index 2c28820..dc00786 100644 --- a/examples/3_Labrador_convergence_CM4p25.ipynb +++ b/examples/3_Labrador_convergence_CM4p25.ipynb @@ -21,10 +21,10 @@ "execution_count": 1, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:50.972072Z", - "iopub.status.busy": "2026-08-04T02:01:50.971866Z", - "iopub.status.idle": "2026-08-04T02:01:51.710112Z", - "shell.execute_reply": "2026-08-04T02:01:51.709792Z" + "iopub.execute_input": "2026-08-11T18:14:02.030178Z", + "iopub.status.busy": "2026-08-11T18:14:02.029890Z", + "iopub.status.idle": "2026-08-11T18:14:03.371488Z", + "shell.execute_reply": "2026-08-11T18:14:03.371166Z" } }, "outputs": [ @@ -32,7 +32,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Sectionate version: 0.4.0rc2.dev3\n" + "Sectionate version: 0.4.0rc3.dev0\n" ] } ], @@ -60,10 +60,10 @@ "execution_count": 2, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:51.711277Z", - "iopub.status.busy": "2026-08-04T02:01:51.711166Z", - "iopub.status.idle": "2026-08-04T02:01:51.832845Z", - "shell.execute_reply": "2026-08-04T02:01:51.832479Z" + "iopub.execute_input": "2026-08-11T18:14:03.373236Z", + "iopub.status.busy": "2026-08-11T18:14:03.373032Z", + "iopub.status.idle": "2026-08-11T18:14:03.578535Z", + "shell.execute_reply": "2026-08-11T18:14:03.578228Z" } }, "outputs": [ @@ -71,7 +71,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/hfdrake/miniforge3/envs/docs_env_sectionate_fix-layer-interface-matching/lib/python3.14/site-packages/xgcm/grid.py:463: UserWarning: The north-fold (tripolar) boundary condition is experimental. Its API and numerical behavior may change in future releases, and it has not yet been validated across the full range of grid configurations. Please review results carefully and report any issues at https://github.com/xgcm/xgcm/issues.\n", + "/opt/anaconda3/envs/docs_env_sectionate_fix-fold-crossing-transports/lib/python3.14t/site-packages/xgcm/grid.py:463: UserWarning: The north-fold (tripolar) boundary condition is experimental. Its API and numerical behavior may change in future releases, and it has not yet been validated across the full range of grid configurations. Please review results carefully and report any issues at https://github.com/xgcm/xgcm/issues.\n", " warnings.warn(\n" ] } @@ -94,10 +94,10 @@ "execution_count": 3, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:51.833990Z", - "iopub.status.busy": "2026-08-04T02:01:51.833928Z", - "iopub.status.idle": "2026-08-04T02:01:51.835664Z", - "shell.execute_reply": "2026-08-04T02:01:51.835344Z" + "iopub.execute_input": "2026-08-11T18:14:03.580105Z", + "iopub.status.busy": "2026-08-11T18:14:03.580026Z", + "iopub.status.idle": "2026-08-11T18:14:03.581866Z", + "shell.execute_reply": "2026-08-11T18:14:03.581635Z" } }, "outputs": [], @@ -120,10 +120,10 @@ "execution_count": 4, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:51.836649Z", - "iopub.status.busy": "2026-08-04T02:01:51.836596Z", - "iopub.status.idle": "2026-08-04T02:01:51.864344Z", - "shell.execute_reply": "2026-08-04T02:01:51.864033Z" + "iopub.execute_input": "2026-08-11T18:14:03.583068Z", + "iopub.status.busy": "2026-08-11T18:14:03.583002Z", + "iopub.status.idle": "2026-08-11T18:14:03.620485Z", + "shell.execute_reply": "2026-08-11T18:14:03.620200Z" } }, "outputs": [], @@ -140,10 +140,10 @@ "execution_count": 5, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:51.865600Z", - "iopub.status.busy": "2026-08-04T02:01:51.865535Z", - "iopub.status.idle": "2026-08-04T02:01:51.980369Z", - "shell.execute_reply": "2026-08-04T02:01:51.979958Z" + "iopub.execute_input": "2026-08-11T18:14:03.621967Z", + "iopub.status.busy": "2026-08-11T18:14:03.621882Z", + "iopub.status.idle": "2026-08-11T18:14:03.775562Z", + "shell.execute_reply": "2026-08-11T18:14:03.775242Z" } }, "outputs": [ @@ -192,10 +192,10 @@ "execution_count": 6, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:51.981486Z", - "iopub.status.busy": "2026-08-04T02:01:51.981410Z", - "iopub.status.idle": "2026-08-04T02:01:52.020015Z", - "shell.execute_reply": "2026-08-04T02:01:52.019586Z" + "iopub.execute_input": "2026-08-11T18:14:03.776886Z", + "iopub.status.busy": "2026-08-11T18:14:03.776802Z", + "iopub.status.idle": "2026-08-11T18:14:03.824717Z", + "shell.execute_reply": "2026-08-11T18:14:03.824406Z" } }, "outputs": [], @@ -208,10 +208,10 @@ "execution_count": 7, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:52.021157Z", - "iopub.status.busy": "2026-08-04T02:01:52.021084Z", - "iopub.status.idle": "2026-08-04T02:01:52.062912Z", - "shell.execute_reply": "2026-08-04T02:01:52.062595Z" + "iopub.execute_input": "2026-08-11T18:14:03.826081Z", + "iopub.status.busy": "2026-08-11T18:14:03.825998Z", + "iopub.status.idle": "2026-08-11T18:14:03.877300Z", + "shell.execute_reply": "2026-08-11T18:14:03.877050Z" } }, "outputs": [], @@ -225,10 +225,10 @@ "execution_count": 8, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:52.064165Z", - "iopub.status.busy": "2026-08-04T02:01:52.064104Z", - "iopub.status.idle": "2026-08-04T02:01:52.141159Z", - "shell.execute_reply": "2026-08-04T02:01:52.140798Z" + "iopub.execute_input": "2026-08-11T18:14:03.878667Z", + "iopub.status.busy": "2026-08-11T18:14:03.878590Z", + "iopub.status.idle": "2026-08-11T18:14:03.981853Z", + "shell.execute_reply": "2026-08-11T18:14:03.981564Z" } }, "outputs": [ @@ -264,10 +264,10 @@ "execution_count": 9, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:52.142231Z", - "iopub.status.busy": "2026-08-04T02:01:52.142172Z", - "iopub.status.idle": "2026-08-04T02:01:52.278672Z", - "shell.execute_reply": "2026-08-04T02:01:52.278340Z" + "iopub.execute_input": "2026-08-11T18:14:03.983452Z", + "iopub.status.busy": "2026-08-11T18:14:03.983352Z", + "iopub.status.idle": "2026-08-11T18:14:04.195311Z", + "shell.execute_reply": "2026-08-11T18:14:04.194981Z" } }, "outputs": [ diff --git a/examples/4_sections_on_global_tripolar_grid.ipynb b/examples/4_sections_on_global_tripolar_grid.ipynb index a5b13f9..f4951ff 100644 --- a/examples/4_sections_on_global_tripolar_grid.ipynb +++ b/examples/4_sections_on_global_tripolar_grid.ipynb @@ -23,10 +23,10 @@ "execution_count": 1, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:53.171667Z", - "iopub.status.busy": "2026-08-04T02:01:53.171402Z", - "iopub.status.idle": "2026-08-04T02:01:55.635990Z", - "shell.execute_reply": "2026-08-04T02:01:55.635261Z" + "iopub.execute_input": "2026-08-11T18:14:05.328279Z", + "iopub.status.busy": "2026-08-11T18:14:05.327946Z", + "iopub.status.idle": "2026-08-11T18:14:11.213026Z", + "shell.execute_reply": "2026-08-11T18:14:11.212514Z" } }, "outputs": [ @@ -34,7 +34,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Sectionate version: 0.4.0rc2.dev3\n" + "Sectionate version: 0.4.0rc3.dev0\n" ] } ], @@ -66,10 +66,10 @@ "execution_count": 2, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:55.638398Z", - "iopub.status.busy": "2026-08-04T02:01:55.638098Z", - "iopub.status.idle": "2026-08-04T02:01:55.646099Z", - "shell.execute_reply": "2026-08-04T02:01:55.645540Z" + "iopub.execute_input": "2026-08-11T18:14:11.216200Z", + "iopub.status.busy": "2026-08-11T18:14:11.215700Z", + "iopub.status.idle": "2026-08-11T18:14:11.224542Z", + "shell.execute_reply": "2026-08-11T18:14:11.224169Z" } }, "outputs": [], @@ -143,10 +143,10 @@ "execution_count": 3, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:55.647677Z", - "iopub.status.busy": "2026-08-04T02:01:55.647564Z", - "iopub.status.idle": "2026-08-04T02:01:55.794379Z", - "shell.execute_reply": "2026-08-04T02:01:55.793983Z" + "iopub.execute_input": "2026-08-11T18:14:11.226445Z", + "iopub.status.busy": "2026-08-11T18:14:11.226314Z", + "iopub.status.idle": "2026-08-11T18:14:11.501873Z", + "shell.execute_reply": "2026-08-11T18:14:11.501499Z" } }, "outputs": [ @@ -154,7 +154,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/hfdrake/miniforge3/envs/docs_env_sectionate_fix-layer-interface-matching/lib/python3.14/site-packages/xgcm/grid.py:463: UserWarning: The north-fold (tripolar) boundary condition is experimental. Its API and numerical behavior may change in future releases, and it has not yet been validated across the full range of grid configurations. Please review results carefully and report any issues at https://github.com/xgcm/xgcm/issues.\n", + "/opt/anaconda3/envs/docs_env_sectionate_fix-fold-crossing-transports/lib/python3.14t/site-packages/xgcm/grid.py:463: UserWarning: The north-fold (tripolar) boundary condition is experimental. Its API and numerical behavior may change in future releases, and it has not yet been validated across the full range of grid configurations. Please review results carefully and report any issues at https://github.com/xgcm/xgcm/issues.\n", " warnings.warn(\n" ] } @@ -177,10 +177,10 @@ "execution_count": 4, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:55.795569Z", - "iopub.status.busy": "2026-08-04T02:01:55.795503Z", - "iopub.status.idle": "2026-08-04T02:01:55.798005Z", - "shell.execute_reply": "2026-08-04T02:01:55.797632Z" + "iopub.execute_input": "2026-08-11T18:14:11.504562Z", + "iopub.status.busy": "2026-08-11T18:14:11.504444Z", + "iopub.status.idle": "2026-08-11T18:14:11.507370Z", + "shell.execute_reply": "2026-08-11T18:14:11.507123Z" } }, "outputs": [], @@ -203,10 +203,10 @@ "execution_count": 5, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:55.799062Z", - "iopub.status.busy": "2026-08-04T02:01:55.799000Z", - "iopub.status.idle": "2026-08-04T02:01:56.913038Z", - "shell.execute_reply": "2026-08-04T02:01:56.912614Z" + "iopub.execute_input": "2026-08-11T18:14:11.508658Z", + "iopub.status.busy": "2026-08-11T18:14:11.508563Z", + "iopub.status.idle": "2026-08-11T18:14:13.875563Z", + "shell.execute_reply": "2026-08-11T18:14:13.875186Z" } }, "outputs": [ @@ -251,10 +251,10 @@ "execution_count": 6, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:56.915401Z", - "iopub.status.busy": "2026-08-04T02:01:56.915332Z", - "iopub.status.idle": "2026-08-04T02:01:57.818895Z", - "shell.execute_reply": "2026-08-04T02:01:57.818522Z" + "iopub.execute_input": "2026-08-11T18:14:13.887283Z", + "iopub.status.busy": "2026-08-11T18:14:13.887134Z", + "iopub.status.idle": "2026-08-11T18:14:15.277081Z", + "shell.execute_reply": "2026-08-11T18:14:15.276780Z" } }, "outputs": [ @@ -305,10 +305,10 @@ "execution_count": 7, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:57.821173Z", - "iopub.status.busy": "2026-08-04T02:01:57.821093Z", - "iopub.status.idle": "2026-08-04T02:01:57.822690Z", - "shell.execute_reply": "2026-08-04T02:01:57.822323Z" + "iopub.execute_input": "2026-08-11T18:14:15.280014Z", + "iopub.status.busy": "2026-08-11T18:14:15.279914Z", + "iopub.status.idle": "2026-08-11T18:14:15.281502Z", + "shell.execute_reply": "2026-08-11T18:14:15.281272Z" } }, "outputs": [], @@ -328,10 +328,10 @@ "execution_count": 8, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:57.823644Z", - "iopub.status.busy": "2026-08-04T02:01:57.823593Z", - "iopub.status.idle": "2026-08-04T02:01:58.205365Z", - "shell.execute_reply": "2026-08-04T02:01:58.204922Z" + "iopub.execute_input": "2026-08-11T18:14:15.282785Z", + "iopub.status.busy": "2026-08-11T18:14:15.282706Z", + "iopub.status.idle": "2026-08-11T18:14:15.835722Z", + "shell.execute_reply": "2026-08-11T18:14:15.835436Z" } }, "outputs": [ @@ -370,10 +370,10 @@ "execution_count": 9, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:58.207583Z", - "iopub.status.busy": "2026-08-04T02:01:58.207497Z", - "iopub.status.idle": "2026-08-04T02:01:58.606342Z", - "shell.execute_reply": "2026-08-04T02:01:58.605892Z" + "iopub.execute_input": "2026-08-11T18:14:15.838920Z", + "iopub.status.busy": "2026-08-11T18:14:15.838812Z", + "iopub.status.idle": "2026-08-11T18:14:16.431123Z", + "shell.execute_reply": "2026-08-11T18:14:16.430836Z" } }, "outputs": [ @@ -412,10 +412,10 @@ "execution_count": 10, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:58.608545Z", - "iopub.status.busy": "2026-08-04T02:01:58.608465Z", - "iopub.status.idle": "2026-08-04T02:01:59.012843Z", - "shell.execute_reply": "2026-08-04T02:01:59.012464Z" + "iopub.execute_input": "2026-08-11T18:14:16.434992Z", + "iopub.status.busy": "2026-08-11T18:14:16.434863Z", + "iopub.status.idle": "2026-08-11T18:14:17.049175Z", + "shell.execute_reply": "2026-08-11T18:14:17.048897Z" } }, "outputs": [ @@ -456,10 +456,10 @@ "execution_count": 11, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:59.015039Z", - "iopub.status.busy": "2026-08-04T02:01:59.014959Z", - "iopub.status.idle": "2026-08-04T02:01:59.577602Z", - "shell.execute_reply": "2026-08-04T02:01:59.577160Z" + "iopub.execute_input": "2026-08-11T18:14:17.052447Z", + "iopub.status.busy": "2026-08-11T18:14:17.052328Z", + "iopub.status.idle": "2026-08-11T18:14:17.924450Z", + "shell.execute_reply": "2026-08-11T18:14:17.924143Z" } }, "outputs": [ @@ -499,10 +499,10 @@ "execution_count": 12, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:59.579759Z", - "iopub.status.busy": "2026-08-04T02:01:59.579676Z", - "iopub.status.idle": "2026-08-04T02:01:59.972989Z", - "shell.execute_reply": "2026-08-04T02:01:59.972545Z" + "iopub.execute_input": "2026-08-11T18:14:17.928263Z", + "iopub.status.busy": "2026-08-11T18:14:17.928157Z", + "iopub.status.idle": "2026-08-11T18:14:18.534270Z", + "shell.execute_reply": "2026-08-11T18:14:18.533881Z" } }, "outputs": [ @@ -543,10 +543,10 @@ "execution_count": 13, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:01:59.975278Z", - "iopub.status.busy": "2026-08-04T02:01:59.975201Z", - "iopub.status.idle": "2026-08-04T02:02:00.688345Z", - "shell.execute_reply": "2026-08-04T02:02:00.687998Z" + "iopub.execute_input": "2026-08-11T18:14:18.537678Z", + "iopub.status.busy": "2026-08-11T18:14:18.537567Z", + "iopub.status.idle": "2026-08-11T18:14:19.576560Z", + "shell.execute_reply": "2026-08-11T18:14:19.576109Z" } }, "outputs": [ @@ -588,10 +588,10 @@ "execution_count": 14, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:02:00.690842Z", - "iopub.status.busy": "2026-08-04T02:02:00.690767Z", - "iopub.status.idle": "2026-08-04T02:02:01.046170Z", - "shell.execute_reply": "2026-08-04T02:02:01.045845Z" + "iopub.execute_input": "2026-08-11T18:14:19.580042Z", + "iopub.status.busy": "2026-08-11T18:14:19.579935Z", + "iopub.status.idle": "2026-08-11T18:14:20.114869Z", + "shell.execute_reply": "2026-08-11T18:14:20.114549Z" } }, "outputs": [ @@ -623,10 +623,10 @@ "execution_count": 15, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:02:01.048283Z", - "iopub.status.busy": "2026-08-04T02:02:01.048205Z", - "iopub.status.idle": "2026-08-04T02:02:01.418272Z", - "shell.execute_reply": "2026-08-04T02:02:01.417864Z" + "iopub.execute_input": "2026-08-11T18:14:20.118061Z", + "iopub.status.busy": "2026-08-11T18:14:20.117954Z", + "iopub.status.idle": "2026-08-11T18:14:20.677296Z", + "shell.execute_reply": "2026-08-11T18:14:20.676998Z" } }, "outputs": [ @@ -658,10 +658,10 @@ "execution_count": 16, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:02:01.420269Z", - "iopub.status.busy": "2026-08-04T02:02:01.420194Z", - "iopub.status.idle": "2026-08-04T02:02:01.811776Z", - "shell.execute_reply": "2026-08-04T02:02:01.811376Z" + "iopub.execute_input": "2026-08-11T18:14:20.680056Z", + "iopub.status.busy": "2026-08-11T18:14:20.679965Z", + "iopub.status.idle": "2026-08-11T18:14:21.252533Z", + "shell.execute_reply": "2026-08-11T18:14:21.252243Z" } }, "outputs": [ @@ -701,10 +701,10 @@ "execution_count": 17, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:02:01.813687Z", - "iopub.status.busy": "2026-08-04T02:02:01.813606Z", - "iopub.status.idle": "2026-08-04T02:02:02.168679Z", - "shell.execute_reply": "2026-08-04T02:02:02.168282Z" + "iopub.execute_input": "2026-08-11T18:14:21.256016Z", + "iopub.status.busy": "2026-08-11T18:14:21.255916Z", + "iopub.status.idle": "2026-08-11T18:14:21.776661Z", + "shell.execute_reply": "2026-08-11T18:14:21.776121Z" } }, "outputs": [ diff --git a/examples/5_MOC_transports_ECCOv4r4.ipynb b/examples/5_MOC_transports_ECCOv4r4.ipynb index 37f91c3..07cb5c9 100644 --- a/examples/5_MOC_transports_ECCOv4r4.ipynb +++ b/examples/5_MOC_transports_ECCOv4r4.ipynb @@ -33,10 +33,10 @@ "execution_count": 1, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:09:56.567055Z", - "iopub.status.busy": "2026-08-04T02:09:56.566859Z", - "iopub.status.idle": "2026-08-04T02:09:57.455411Z", - "shell.execute_reply": "2026-08-04T02:09:57.455061Z" + "iopub.execute_input": "2026-08-11T18:14:23.119158Z", + "iopub.status.busy": "2026-08-11T18:14:23.119018Z", + "iopub.status.idle": "2026-08-11T18:14:24.429117Z", + "shell.execute_reply": "2026-08-11T18:14:24.428846Z" } }, "outputs": [ @@ -44,7 +44,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Sectionate version: 0.4.0rc2.dev3\n" + "Sectionate version: 0.4.0rc3.dev0\n" ] } ], @@ -77,10 +77,10 @@ "execution_count": 2, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:09:57.469675Z", - "iopub.status.busy": "2026-08-04T02:09:57.469513Z", - "iopub.status.idle": "2026-08-04T02:09:58.262430Z", - "shell.execute_reply": "2026-08-04T02:09:58.262018Z" + "iopub.execute_input": "2026-08-11T18:14:24.430947Z", + "iopub.status.busy": "2026-08-11T18:14:24.430771Z", + "iopub.status.idle": "2026-08-11T18:14:25.946242Z", + "shell.execute_reply": "2026-08-11T18:14:25.945903Z" } }, "outputs": [ @@ -129,10 +129,10 @@ "execution_count": 3, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:09:58.263547Z", - "iopub.status.busy": "2026-08-04T02:09:58.263491Z", - "iopub.status.idle": "2026-08-04T02:09:58.265337Z", - "shell.execute_reply": "2026-08-04T02:09:58.265038Z" + "iopub.execute_input": "2026-08-11T18:14:25.948020Z", + "iopub.status.busy": "2026-08-11T18:14:25.947899Z", + "iopub.status.idle": "2026-08-11T18:14:25.950114Z", + "shell.execute_reply": "2026-08-11T18:14:25.949875Z" } }, "outputs": [], @@ -172,10 +172,10 @@ "execution_count": 4, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:09:58.266314Z", - "iopub.status.busy": "2026-08-04T02:09:58.266260Z", - "iopub.status.idle": "2026-08-04T02:10:02.515464Z", - "shell.execute_reply": "2026-08-04T02:10:02.514970Z" + "iopub.execute_input": "2026-08-11T18:14:25.951526Z", + "iopub.status.busy": "2026-08-11T18:14:25.951441Z", + "iopub.status.idle": "2026-08-11T18:14:31.857574Z", + "shell.execute_reply": "2026-08-11T18:14:31.857288Z" } }, "outputs": [ @@ -245,10 +245,10 @@ "execution_count": 5, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:10:02.518605Z", - "iopub.status.busy": "2026-08-04T02:10:02.518471Z", - "iopub.status.idle": "2026-08-04T02:10:17.559173Z", - "shell.execute_reply": "2026-08-04T02:10:17.558828Z" + "iopub.execute_input": "2026-08-11T18:14:31.865521Z", + "iopub.status.busy": "2026-08-11T18:14:31.865326Z", + "iopub.status.idle": "2026-08-11T18:14:49.067400Z", + "shell.execute_reply": "2026-08-11T18:14:49.067029Z" } }, "outputs": [ @@ -293,10 +293,10 @@ "execution_count": 6, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T02:10:17.560375Z", - "iopub.status.busy": "2026-08-04T02:10:17.560315Z", - "iopub.status.idle": "2026-08-04T02:10:17.727353Z", - "shell.execute_reply": "2026-08-04T02:10:17.726950Z" + "iopub.execute_input": "2026-08-11T18:14:49.070440Z", + "iopub.status.busy": "2026-08-11T18:14:49.070335Z", + "iopub.status.idle": "2026-08-11T18:14:49.306386Z", + "shell.execute_reply": "2026-08-11T18:14:49.306112Z" } }, "outputs": [ From 9b951d4a683fe55787db5efe88c2a3a5768bf550 Mon Sep 17 00:00:00 2001 From: Henri Drake Date: Tue, 11 Aug 2026 11:20:41 -0700 Subject: [PATCH 3/3] Note corner-identity resolution in the algorithm walkthrough The page already explained that a consecutive pair resolving to the same physical point emits no face. Add why twins matter for the converse: a fold crossing can leave one index representation of a seam corner and land next to the other, so the chain has to be resolved to index-adjacent steps before faces are named. Co-Authored-By: Claude Opus 5 (1M context) --- docs/source/algorithm.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/source/algorithm.md b/docs/source/algorithm.md index e82a811..1c64f86 100644 --- a/docs/source/algorithm.md +++ b/docs/source/algorithm.md @@ -239,6 +239,16 @@ staggering (`outer`, `right` or `left`) and is read from the grid rather than as "Up to" `N-1`, because a consecutive pair that resolves to the same physical point — the twin corners of a seam again — spans no cell and carries no flux, so it emits no face at all. +Twins matter here for a second reason. Naming the face between two corners assumes they are +neighbours *in index space*, and across a bipolar fold that can fail: the walk may leave one +index representation of a seam corner and land next to the *other*, a real physical edge whose +endpoints are far apart in `i`. Attributing that step from the source corner alone would name +the mirrored column's face, with the sign that goes with it. So before converting the chain, +`uvindices_from_qindices` resolves corner identity — multi-tile grids canonicalise every corner +through the outer-lattice topology, single-tile grids splice the skipped twin back into the +chain — after which every remaining step is an ordinary adjacent one, and the zero-length twin +edge it introduces drops out by the rule above rather than being counted twice. + `transports.convergent_transport` then accumulates the signed normal transport through those faces. For a **closed** section it works out the traversal orientation and signs everything so that positive means *into* the enclosed region; for an **open** section there is no inside, so