Always take the shortest path between section waypoints - #10
Conversation
Sectionate now resolves every section segment to the shortest path between its two waypoints, for both `curve="great circle"` and `curve="latitude circle"`. Raw longitudes are no longer read as a request to travel the long way round: a latitude-circle segment written 0 -> 270 runs 90 degrees west, matching what a great circle already did. The old "each segment must span less than 180 degrees" ValueError is gone. `_check_segment_span` now raises a single, shared ambiguity error for both curve types, and only for genuinely ill-posed segments: - endpoints exactly half a circle apart, where the two ways round are equally short so the shortest path is not unique (antipodal for a great circle, +/-180 degrees of longitude for a latitude circle); - for "latitude circle" only, oblique endpoints that differ in both latitude and longitude, since no circle of constant latitude passes through them. Latitude-circle segments must be zonal (shared latitude) or meridional (shared longitude); a meridional segment is the connector used to join zonal arcs at different latitudes and walks straight down the meridian. Previously an oblique latitude-circle segment was traced as an L-shaped path -- zonal along the starting latitude, then meridional -- which was direction-dependent and, when it crossed the periodic seam, degenerated into a staircase of zero-length seam steps. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`curve` now takes three values instead of two: - "great circle" (default) -- unchanged, every segment follows the geodesic. - "latitude circle" -- every segment follows a parallel. A segment whose endpoints do not share a latitude lies on no circle of constant latitude, so it now raises. Previously meridional segments were quietly accepted here on the grounds that a meridian is unambiguous; they are not latitude circles, and accepting them hid a real failure (below). - "latitude and great circle" (new) -- decided segment by segment: segments whose endpoints share a latitude follow the parallel, all others follow the geodesic. This is what a section that is zonal in places and joined up by meridional or slanted legs elsewhere actually wants. This fixes a bug. The constant-latitude `progress` metric measures progress purely in longitude, so along a meridian it is flat: the walk admits no neighbour, falls through to the fallback branch, and on a real grid can fail to converge. `grid_section(grid, [0., 0.], [80., 60.], curve="latitude circle")` on ECCO LLC90 raised `RuntimeError: Should have reached the endpoint by now.` in that direction while 60->80 traced fine -- a direction-dependent failure on a section that never should have been routed to those metrics. Under "latitude circle" the segment is now refused with a clear message; under "latitude and great circle" it routes to the geodesic and traces identically in both directions. The per-segment choice is made once, in `infer_grid_path_from_geo` (which is called once per segment), from the requested waypoints rather than from the grid corners they snap to, and threaded down to the metric selection. `_is_constant_latitude` is the single classification behind both the legality check and the metric choice, so the two cannot disagree. Also, from review of the shortest-path change: - The segment tolerance was 1e-9 degrees, 0.11 mm. It is now a *classification* tolerance, and at that size a latitude that has been through float32 -- which is how ECCO and many models store corner coordinates -- reads as a different latitude and turns a zonal segment oblique. Split into two named constants: `CONSTANT_LATITUDE_ATOL_DEG` (1e-6 deg, ~11 cm, five orders of magnitude below any grid cell) and `HALF_CIRCLE_ATOL_DEG` (1e-9 deg), which measure different things. - The shared half-a-circle error quoted a bare "separation ~180.0000 degrees" for two points 222 km apart at 89N, because along a parallel the number is degrees of longitude and not degrees of arc. Still one error, but it now names the measure it is reporting. - The `180 - atol` guard is described for what it does: it catches endpoints written exactly half a circle apart. A hair short of that (0 -> 179.9999999 east, 0 -> 180.0000001 west) resolves by round-off with no error, so the docstring says to subdivide rather than to rely on the check. - The contract was restated in full in five places. It is now stated once, in `grid_section`, and cross-referenced from the rest; nothing points a user at a private name, since there is no rendered API page for one to land on. - Dropped the comment describing the L-shaped oblique latitude-circle path, which no longer reaches that code. Tests: all three options over zonal, meridional, oblique and exactly-half-a- circle segments; a 2-degree global fixture where the two metrics visibly differ (0->120 at 40N holds 40N for 61 points, while the geodesic bows to 60N over 83) -- the existing 7x5 fixture has corner rows 40 degrees apart and cannot show this; float32-derived waypoints classifying as constant-latitude; and the ECCO LLC90 meridional segment tracing identically in both directions. `test_oblique_latitude_circle_segments_reversible` is renamed: its legs are zonal and meridional, so under these rules it was never oblique. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three
|
| value | behaviour |
|---|---|
"great circle" |
unchanged — every segment follows the geodesic |
"latitude circle" |
every segment follows a parallel; a segment whose endpoints do not share a latitude raises |
"latitude and great circle" (new) |
decided per segment — parallels where the endpoints share a latitude, geodesics everywhere else |
This replaces the oblique-only rejection the first version shipped. "latitude circle" now means
what it says, including for meridional segments, which were previously waved through on the
grounds that a meridian is unambiguous.
That accepting-meridians behaviour was hiding a real failure. The constant-latitude progress
metric measures progress purely in longitude, so along a meridian it is flat: the walk admits no
neighbour and falls through to a fallback. On ECCO LLC90,
grid_section(grid, [0., 0.], [80., 60.], curve="latitude circle") raised
RuntimeError: Should have reached the endpoint by now. in that direction while 60 -> 80 traced
fine — a direction-dependent failure. This is pre-existing on the base branch, not introduced
here. Under "latitude circle" the segment is now refused with a clear message; under
"latitude and great circle" it routes to the geodesic and traces identically in both directions
(49 points each way, exact reverses).
Review findings fixed alongside:
- The classification tolerance was 1e-9 degrees — 0.11 mm — and it was a rejection threshold.
float(np.float32(26.4))differs from26.4by 3.8e-07 degrees (about 4 cm), and ECCO stores
geolat_cas float32, so a user reading one latitude off the grid and typing the other got a
hard error. It is nowCONSTANT_LATITUDE_ATOL_DEG = 1e-6(~11 cm, five orders of magnitude
below any grid cell) and it classifies rather than rejects. The half-circle comparison keeps its
own tight1e-9as a separately named constant — the two are not the same physical quantity and
should never have shared one. - The merged ambiguity message was wrong for latitude circles. At 89°N,
0 -> 180reported
"half a circle apart (separation ~180.0000 degrees)" for two points 222 km apart, because the
separation is degrees of longitude, not arc. It still is one message for all options, but it now
names the measure it reports. - The near-180 guard was oversold.
0 -> 179.999999999raises while0 -> 179.9999999runs
east and0 -> 180.0000001runs west — a 2e-07 degree move flips the section to the other side
of the planet with no error. The docs now say plainly that it catches endpoints typed exactly
half a circle apart and that anything short of it is settled by round-off. - The contract was stated in full in five places; it is now stated once, in
grid_section.
test_oblique_latitude_circle_segments_reversiblewas renamed — its legs are zonal and
meridional, so under these rules it is not oblique at all.
On the discriminating test: the previous check that a latitude circle and a great circle agree
was run at latitude 0, where a latitude circle is a great circle — true but vacuous. The suite
now uses a 2° global grid where 0 -> 120 at lat 40 holds the parallel in 61 points while the
geodesic bows to 60°N in 81. The old 7×5 fixture has rows 40° apart and cannot tell the metrics
apart at any latitude.
Full suite: 85 passed, 0 skips.
The literal "latitude and great circle" reads a little oddly beside two options that each name a
single curve; "latitude circle where possible" or "mixed" would also work. It is a one-line
change to CURVES plus the test literals if you would rather rename before merge.
Written by Claude Code.
Two conflicts, both additive documentation: - `create_section_composite`'s parameter list: #10 documented `curve`, this branch documented `grid`. Both kept. - `CLAUDE.md`'s `section.py` bullet: the base now states the three-curve contract from #10; this branch states the repeated-corner invariant. The base's wording is authoritative on curves and carries hdrake's own edits, so the invariant sentences are spliced into it. Two tests on this branch encoded #10-era path lengths that the corner dropping changes: `test_latitude_circle_takes_shortest_path_west` (the seam vertex is one corner under two indices, so the path starts at index 6 rather than carrying both) and `test_parallel_is_held_where_the_geodesic_bows` (the geodesic crosses the periodic seam diagonally, 81 points rather than 83). Expectations updated and the reasons recorded in each docstring. Suite: 96 passed, 0 skips. Transports re-verified against the unmodified base: still exactly one recorded difference, `mom6_fold.zonal_periodic.n_corners: 308 -> 307`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Addresses
r3752599198,r3752605844andr3752736092from the review of MOM6-community#47 (MOM6-community#47, review4899521952). Targetstopology-driven-neighbors.Shortest path, always
Every segment follows the shortest path between its two waypoints. Raw longitudes are never read as an instruction to go the long way round, so a segment written
0 -> 270along a parallel runs 90° west. The old "each segment must span less than 180 degrees"ValueErroris gone.The walk already did the right thing — the constant-latitude progress metric is
sin²(Δlon/2), periodic in 360° and monotonic in|Δlon|up to 180° — only_check_segment_spanrefused to let such a segment through. This also fixes segments that were wrongly rejected before:350 -> 10(20° east across the prime meridian) and-170 -> 170(20° west across the dateline) now work.Three
curveoptions"great circle""latitude circle""latitude and great circle"(new)"latitude circle"now means what it says, meridional segments included. They were previously waved through on the grounds that a meridian is unambiguous, and that was hiding a real failure: the constant-latitude metric is flat along a meridian, so the walk admits no neighbour and falls through to a fallback. On ECCO LLC90,grid_section(grid, [0., 0.], [80., 60.], curve="latitude circle")raisedRuntimeError: Should have reached the endpoint by now.in that direction while60 -> 80traced fine. That failure is pre-existing on the base branch, not introduced here. It is now either refused with a clear message or, under the combined option, routed to the geodesic and traced identically both ways (49 points each, exact reverses).There is one ambiguity error, shared by all options: endpoints written exactly half a circle apart, where neither way round is shorter. It names the measure it reports — degrees of longitude along the parallel, or degrees of arc.
Tolerances
Two separately named constants replace the single
SEGMENT_ATOL_DEG, because they were never the same quantity:CONSTANT_LATITUDE_ATOL_DEG = 1e-6(~11 cm) classifies whether a segment follows a parallel. The old 1e-9 (0.11 mm) rejectedfloat(np.float32(26.4))against26.4— 4 cm apart — and ECCO storesgeolat_cas float32.HALF_CIRCLE_ATOL_DEG = 1e-9guards the ambiguity check. The docs now say plainly that it catches endpoints typed exactly half a circle apart and nothing else:0 -> 179.9999999runs east and0 -> 180.0000001runs west, with no warning either side.Notes
The earlier claim that a latitude circle and a great circle produce identical paths was checked at latitude 0, where a latitude circle is a great circle — true but vacuous. The suite now discriminates on a 2° global grid:
0 -> 120at lat 40 holds the parallel in 61 points while the geodesic bows to 60°N. The 7×5 fixture has rows 40° apart and cannot show this at any latitude.The literal
"latitude and great circle"reads oddly beside two options that each name a single curve;"latitude circle where possible"or"mixed"would also do. One-line change toCURVESplus the test literals if you want it renamed before merge.Tests
85 passed, 0 skips, indocs_env_sectionate_pr47-lat-circle-shortest-path. Covers all three options against zonal, meridional, oblique and exactly-180 segments; the lat-40 discriminator; float32 waypoint classification; and a meridional ECCO LLC90 segment tracing both directions.test_oblique_latitude_circle_segments_reversiblewas renamed — its legs are zonal and meridional.Both notebooks that pass
curveuse only constant-latitude waypoints, so no notebook output changes; they are re-executed on the integrated branch.🤖 Generated with Claude Code