Accept either seam representation, and map velocity faces back to corner steps - #14
Draft
hdrake wants to merge 7 commits into
Draft
Accept either seam representation, and map velocity faces back to corner steps#14hdrake wants to merge 7 commits into
hdrake wants to merge 7 commits into
Conversation
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#47. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
A seam gives one physical corner more than one index, so a section crossing it can arrive written two ways: seam-explicit, carrying both indices, or twin-free, carrying one. `_insert_seam_twins` tested index adjacency before physical coincidence, so a pair that was *already* a zero-length twin edge -- the two indices of a fold-seam corner sit far apart in the index lattice, column i against column nx-i -- was sent down the twin-splicing path, found no twin to splice, and raised. Sections traced on a cell mask arrive in exactly that form: `regionate.boundaries` keeps both coincident corners at a seam junction and closes its loops with a coincident edge, deliberately, because zero-length edges emit no velocity face. So did sections reloaded from saved indices. Testing coincidence first passes them straight through to the drop, which is where they were always meant to go. Because a corner step at a seam carries no face, faces and corner steps are not one-to-one, and nothing said which face came from which step. `uvindices_from_qindices` now reports "q": the index into the caller's corner arrays that each face starts at, so face k spans corners q[k] and q[k]+1. It is strictly increasing and skips exactly the steps that carried no face. `convergent_transport` and `extract_tracer` carry it as a coordinate, so a result can be put back onto the section's corners -- for plotting, for slicing out a sub-section, or for locating a child section inside its parent. On multi-tile grids the last index of a run of repeated corners is now the one kept. Every index of a corner rewrites to the same canonical native one, so no emitted corner changes; it only decides which step "q" attributes a face to, and the face leaving a run is the real one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…seam-corner-identity
CLAUDE.md gains a "Seam representation contract" concept entry: input is liberal (either seam representation is accepted, coincidence tested before adjacency), output is strict (only real velocity faces), the corner and `sect` axes therefore differ in length on purpose, and "q" is what relates them. It also spells out that `lons_c`/`lats_c` stay 1:1 with `i_c`/`j_c` -- a repeated corner is kept, and it is the face between the pair that is dropped -- because the two are easy to confuse. `algorithm.md` says the same thing where it converts corners into faces, and notes "q" in the worked example. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
No figure and no numerical output changed -- none of the notebooks depends on how a seam crossing is written, and the faces of every section they trace are the same ones. Only the recorded version string and one line number inside a captured warning traceback differ. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Check out this pull request on See visual diffs & provide feedback on Jupyter Notebooks. Powered by ReviewNB |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Targets
topology-driven-neighbors(the branch behind MOM6-community#47), notmaster.Supersedes #13 and #11, which answered the same question in opposite directions and could not both be merged. This carries #13's work forward in full and takes nothing from #11 — see Why #11 is not folded in below.
The question
A seam gives one physical vorticity corner more than one index: the duplicated column of a symmetric (
'outer') periodic grid, a corner shared by two tiles, the mirrored columns of a bipolar fold seam. So a section crossing a seam can be written two ways — seam-explicit, carrying both indices with a step between them that spans no cell, or twin-free, carrying one. Both describe the same path.#13 normalised toward the seam-explicit form and dropped the zero-length faces. #11 normalised toward the twin-free form inside
grid_sectionand madeuvindices_from_qindicesraise on anything else. They conflict directly: #11 deletes the drop #13 depends on.This PR settles it as a contract instead of a representation:
qrelates the two — since faces and corner steps are not one-to-one.The bug this fixes
_insert_seam_twinstested index adjacency before physical coincidence. The two indices of a fold-seam corner sit far apart in the index lattice — columniagainst columnnx-i— so a pair that was already the zero-length twin edge looked like a step needing repair, found no twin to splice, and raised.Sections traced on a cell mask arrive in exactly that form.
regionate.boundarieskeeps both coincident corners at a seam junction and closes its loops with a coincident edge, on purpose, because zero-length edges emit no velocity face. Its own comment says so. Reloading a section from saved indices does the same.Measured, running regionate's suite against this branch's parent:
On that 7-column fold row the identity is
i ↔ 6−i, so(4,2)→(4,4)and(4,5)→(4,1)are both coincident pairs — the two frame hand-offs of a region joined to its mirror through the fold. Testing coincidence first passes them to the drop, which is where they were always meant to go. With the fix regionate is48 passed, 10 skipped, including its discrete-divergence-theorem test on a fold-straddling region.q: which corner step each face came fromBecause a step at a seam carries no face,
len(sect) ≠ len(i_c) − 1and nothing said which face came from where.uvindices_from_qindicesnow reportsq, the index into the caller's corner arrays that each face starts at:8 corners → 6 faces,
q = [0,1,3,4,5,6]; the gap atm=2is the seam hand-off.qis strictly increasing, facekspans cornersq[k]andq[k]+1, and the steps it skips are exactly the ones that carried no flux.convergent_transportandextract_tracercarry it as a coordinate on the samesectaxis, so a result can be put back onto the section's corners — to plot against them, to slice out the part belonging to one stretch, or to locate a child section inside its parent (whichregionate.BoundedRegioncurrently does by matching lon/lat floats).Note the two axes are different lengths on purpose.
lons_c/lats_care corner coordinates, 1:1 withi_c/j_cincluding any repeat; thelon/latonsectare velocity-point coordinates. What is dropped is the face between a repeated pair, never the corner.On multi-tile grids the last index of a run of repeated corners is now the one kept. Every index of a corner rewrites to the same canonical native one, so no emitted corner changes; it only decides which step
qattributes a face to, and the face leaving a run is the real one.Carried from #13
The whole of it:
_insert_seam_twins, the_pinched_fold_gridstreamfunction fixture, and its tests. Recapping what that fixed, since it is the numerically significant part — crossing a fold seam, the walk steps from one index representation straight to a corner adjacent to the other. The single-tile arithmetic reads the velocity column off the source corner, so it named the mirror image of the face actually crossed, with the opposite sign, and on a closed crossing emitted one face twice. On the real MOM6 tripolar grid a closed 80°N latitude circle whose true transport is zero returned1.26e-02.Why #11 is not folded in
Each item re-audited after merging
topology-driven-neighbors, which already carries #9 and #10:create_section_composite(grid=)— its own docstring says it exists so the output satisfies "the invariant... whichtransports.uvindices_from_qindicesrefuses". This PR does not add that refusal, so the parameter fixes nothing._multitile_padded_maps/_validate_reciprocity— not dead code here.build_neighbor_mapsreaches_multitile_padded_mapsfor multi-tile grids without tracer-center coordinates, andtwo_face_x_to_xintest_neighbor_maps_x_to_x_seamis exactly that shape (verified by instrumenting the call). Removing it would delete a supported, tested path.drop_repeated_corners/_collapse_single_tile_runs/_single_tile_face— a public API with no internal consumer once the precondition it served is gone.Tests
New
test_seam_representations.py:1e-12under the exactly non-divergent streamfunction — the divergence theorem across the seam;qcontract on periodic-seam, mask-traced, seam-free, and multi-tile sections.Verification
make html SPHINXOPTS="-W"clean;docs/make_algorithm_animation.pyreplay still matchesgrid_section(9 corners, 8 steps) and the committed artifacts are byte-identical.Found while testing, not fixed here
_check_segment_span(added by #10, already merged into this base) rejects a meridional segment undercurve="latitude circle", soregionate.GriddedRegion(..., curve="latitude circle")on an axis-aligned box raises. Confirmed against plaintopology-driven-neighbors, so it predates this branch. The intended migration is the"latitude and great circle"mode #10 added, which the error message itself names — a regionate-side change, not a sectionate one.Next
The end state agreed for this is one representation everywhere, via corner-node identity extended to single-tile grids:
_OuterTopologyfingerprints corners by their surrounding tracer cells, and a single tile's periodic/fold padding supplies those halos exactly asface_connectionsdoes for thirteen. That deletes_insert_seam_twinsand the single-tile coordinate tolerance outright, and lets regionate collapse its two boundary back-ends into one. It changes corner counts in saved sections and notebooks, so it belongs after MOM6-community#47 lands, coordinated with regionate MOM6-community#22. This PR's contract is unchanged by it — only the mechanism beneath.AI-assisted PR, disclosed per the project AI Usage Policy: commits carry
Co-Authored-By:trailers. 🤖 Drafted with Claude Code.