Mixed-order support in the geo kernels (issue #116)#122
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N32Mx4rWhbTK8xXm7Mt2ax
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N32Mx4rWhbTK8xXm7Mt2ax
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N32Mx4rWhbTK8xXm7Mt2ax
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N32Mx4rWhbTK8xXm7Mt2ax
why? i understand that a single order 29 entry makes little sense... but it still makes some sense (what box / polygon contains that point)? furthermore, a group of points fed into mort2bbox/mort2polygon does cover and area and is well defined. |
Merging this PR will not alter performance
Comparing Footnotes
|
kinda feel like this maybe should be a flag? Do we have a function handles the mix use case-- and do we need one? |
|
🤖 from Claude (review) Blocking (CI red — stale base, not a defect in the #116 code). Root cause: the branch is based on Evidence: Fix at fold time: rebase (or merge) onto current |
|
🤖 from Claude (review) Downstream coordination note (non-blocking) — refines the "downstreams verified" claim in Q under the
With the new mixed→raise, that first line now raises mortie's message before the moczarr-specific guard message is reached. End-state is preserved (mixed input → (Spot-checked the other callsites — |
|
🤖 from Claude Folded the two review findings. Finding 1 (merge stale base) — done, but the red test is NOT fixed by the merge; root cause differs from the review's diagnosis. Merged However, the merge does not turn
Per repo convention I did not "fix" a pre-existing Question for @espg: the spec-page tie-break test needs re-pinning for the Finding 2 (moczarr |
|
🤖 from Claude Two review points addressed. On On
Concrete options (pick one):
Which would you like? I've left Generated by Claude Code |
espg
left a comment
There was a problem hiding this comment.
🤖 from Claude (review)
Phase-5 review (commit 20cd212) — removes the point-word ValueError from mort2bbox/mort2polygon so points yield their containing order-29 cell geometry.
Correctness: sound, no defect found.
orders_ofmaps suffix 48..=63 → order 29, so points group with order-29 area words and decode through the samemort2norm→nested-cell path. The oraclemort2bbox(_point()) == mort2bbox(_area(29))is valid and non-circular: both derive from the same lat/lon viageo2mort, and the equality genuinely checks that the point's suffix bits don't perturb the decoded cell vs the area word. Depth-29 agreement is independently pinned bytest_matches_kernel_depth_decode.- Edge cases raised in review are clean: the empty sentinel
0decodes as order 0 withis_point(0)==False, so the removed raise never guarded it — no new silent-wrong path. Empty-array input never entered the oldwords.size and np.any(is_point(...))guard either, so its behavior is unchanged. Southern base cells (bit 63) are exercised by the-45,-120fixture. is_pointis no longer used insidetools.pybut remains public (exported in__init__.py, directly tested) — not dead code.- Docstrings are correctly updated; the spec §1/§4 "no area claim" language for points is consistent with this change (the point carries no area claim; the returned box is its containing cell's), so nothing stale there.
One diff-scoped test-strengthening and one cosmetic nit inline. Nothing blocking.
Generated by Claude Code
| """Points mixed with area words scatter correctly through the | ||
| group-by-order dispatch: each point element yields its containing | ||
| order-29 cell geometry at its input position.""" | ||
| words = np.concatenate([_area(6), _point()]) |
There was a problem hiding this comment.
🤖 from Claude (review)
Point-scatter is only exercised with contiguous group blocks: np.concatenate([_area(6), _point()]) puts the two order-6 words at positions [0,1] and the two order-29 points at [2,3]. Because the order-6 and order-29 groups are already contiguous ascending-order blocks, a buggy scatter that just concatenates per-order group results (ignoring original positions) would still pass this test — so it doesn't actually pin scatter-back-to-position for the point path.
The pure-area oracle test_mort2bbox_matches_uniform gets this right by using shuffled/interleaved _mixed_area(), but that helper excludes points, so the point path never gets the same positional rigor. Suggest interleaving points among area words (e.g. shuffle, or [_point()[:1], _area(6), _point()[1:], _area(19)]) and asserting each element equals its containing-cell/uniform bbox at its input index. Low severity — the underlying scatter mechanism is shared and covered for area words — but this is the one test that's supposed to lock the point behavior.
Generated by Claude Code
| assert mort2polygon(words)[0] == mort2polygon(int(words[0])) | ||
|
|
||
|
|
||
| class TestPointPostures: |
There was a problem hiding this comment.
🤖 from Claude (review)
Nit (non-blocking): the class name TestPointPostures is stale after this phase. It made sense when the tests asserted raise "postures"; the class now asserts containing-cell geometry. Consider renaming (e.g. TestPointContainingCell) so the intent reads from the name. Cosmetic only.
Generated by Claude Code
espg
left a comment
There was a problem hiding this comment.
🤖 from Claude (review)
Adversarial review of the newest phase (20cd212 — point words yield containing-cell geometry). Verdict: clean, no defects found.
What I checked and confirmed:
- Demotion correctness — point words (suffix 48..=63) are classified order 29 by
orders_ofand flow through the order-29 group;mort2bbox/mort2polygon/mort2geoon a point return exactly the geometry of the order-29 AREA word at the same location. Verified across 505 random + edge locations (poles, ±180 antimeridian): 0 bbox / 0 polygon mismatches. No off-by-one, no wrong/empty geometry. - Scatter through dispatch — points mixed with area words (including a point and a genuine order-29 area word coexisting in the same order-29 group, an untested combination) each land at the correct input position with the correct per-element geometry.
- Test replacement — the old point-raise assertions in
TestPointPosturesare properly replaced with positive oracles (point geometry == same-location order-29 area geometry), scalar and array postures both covered. All 23 tests intest_mixed_order.pypass locally. - Consistency —
mort2geo(point)lands on the same order-29 cell center as the area word, so geo/bbox/polygon treat a point as its containing cell uniformly. - Docstrings —
mort2bbox/mort2polygondocstrings andRaisessections updated to match the new (non-raising) contract; params no longer say "area words only". Terse, matches surrounding style.
One non-blocking nit inline (oracle location breadth). One observation for awareness: this deliberately swaps a loud raise for a silently-plausible cell box when a caller passes a point word by mistake — sanctioned by the request on this PR, and is_point remains exported for callers that need to distinguish. is_point is no longer referenced inside tools.py after the guard removal, but it stays a legitimate public introspection helper (in __all__, covered by TestIsPoint) — not dead code.
Generated by Claude Code
| assert mort2bbox(_point()) == mort2bbox(_area(29)) | ||
|
|
||
| def test_polygon_points_yield_containing_cell(self): | ||
| assert mort2polygon(_point()) == mort2polygon(_area(29)) |
There was a problem hiding this comment.
🤖 from Claude (review)
Nit (non-blocking): the point-demotion oracle mort2bbox(_point()) == mort2bbox(_area(29)) only exercises the two module-level locations _LATS/_LONS (45,45) and (-45,-120). The load-bearing invariant here is that the Rust kernel decodes a point word (suffix 48..=63) to the same nested cell as the order-29 area word at that location — worth hardening with a few edge locations (poles, ±180 antimeridian, a base-cell 7-11 southern column already partly covered).
I independently swept 505 random + edge locations (poles and antimeridian included) and found 0 bbox/0 polygon mismatches, so the invariant does generalize — this is a test-coverage hardening suggestion, not a defect. mort2geo(point) likewise lands on the same order-29 cell center, so geo/bbox/polygon treat a point as its containing cell consistently.
Generated by Claude Code
Closes #116
Mixed-order support in the geo kernels, per the implementation plan on the issue (the "Implementation plan (espg-requested)" comment): python-side group-by-order dispatch, zero rust changes.
Phases (one commit each)
orders_of(words) -> uint8[n]andis_point(words) -> bool[n], pure-numpy suffix decodes per the spec page §1 suffix table (0..=27order == suffix;28..=47order-28/29 area in parent-first preorderr = t28*5 + (t29 present ? t29+1 : 0), so(suffix-28) % 5 == 0is the order-28 parent slot;48..=63order-29 point). Golden-tested against the table (every band suffix, both hemispheres) and cross-checked elementwise against the rust kernel's depth decode so the numpy table and the kernel cannot drift.infer_order_from_mortonsemantics fix: uniform input still returns the scalar order (unchanged, kernel-validated); mixed input now raisesValueErrornaming the distinct orders and pointing atorders_of(previously the first element's order was returned silently).mort2geo/mort2polygon/mort2bbox:orders_of→np.uniquemasks → the existing uniform kernel per group → scatter back to input positions. Points group with order 29 (a point's location is exactly whatmort2georeturns); inmort2polygon/mort2bboxa point now yields the geometry of its containing order-29 cell — identical to the order-29 area word at the same location — so a single point has a well-defined box/polygon and a group of points covers a well-defined area element by element.step=1/step=2rings, singleton-group scatter posture), the point containing-cell postures (scalar / uniform / mixed, oracled against the same-location order-29 area word), and theorders_of→order2resresolution-ladder cross-check.mort2bbox/mort2polygonper espg's review. A point word decodes to the identical HEALPix cell as the order-29 area word at the same location (verified: same nested id + depth 29), so points now flow through the order-29 group and yield that containing cell's bbox / ring instead of raising. Tests flipped from raise-assertions to containing-cell oracles.Acceptance evidence
mainfailure (test_spec_page.py::TestDecimalParseTieBreak::test_order29_string_parses_to_area_word, issue main is red: test_spec_page tie-break test predates #120's p-suffix rendering #123 — decimalp-suffix parse, untouched by this PR): 706 passed / 11 skipped / 1 inherited failure.test_mixed_order.pyis 23/23.np.unique(orders).size == 1short-circuits to the pre-existing code path untouched).TestGroupDispatchOracleasserts, per kernel, that the mixed-array result equals the per-order uniform kernel call scattered back to input positions — including a singleton-group case that exercises the length-1 bare-dict/bare-ring return posture.TestPointPosturesoracles a point's bbox/ring against the same-location order-29 area word.TestOrdersOfpins every suffix in the28..=47preorder band and the48..=63point band onto real order-29 prefix+body words in both hemispheres, plus order==suffix for 0–27, plus elementwise agreement with_rustie.rust_mort2nesteddepths.Notes on existing-test changes
Two
test_coverage.pyassertions calledinfer_order_from_morton(moc)on mixed-order MOCs — under the old first-element behavior these were asserting on whichever cell happened to be first. They now useorders_of(moc)and assert over the true per-element orders (strictly stronger). This is exactly the migration the new error message prescribes.Questions for review
mort2normkeeps its mixed-orders raise — open question raised by espg (here); answered on the thread with concrete options.mort2normreturns a single scalar(norm, parent, order)by contract, so mixed input is structurally unanswerable there without a return-type change (unlike the geo kernels, which return per-element arrays). The mixed use-case is already served per-element bymort2geo+orders_of. Awaiting espg's pick between (a) keep the raise + point atmort2geo/orders_of[recommended], (b) a new per-elementmort2norm_each, or (c) a flag — so not implemented here.orders_of/is_pointdo not validate words (pure bit arithmetic; the empty sentinel0decodes as order 0). Docstrings say so and point atvalidate_morton. Validating would cost a kernel round-trip and diverge from the plan's "pure numpy bit ops".mort2bbox/mort2polygonnow yield their containing order-29 cell geometry (Phase 5, per espg's review). A point and the order-29 area word at the same location decode to the identical cell, somort2bbox(point) == mort2bbox(area29)and likewise formort2polygon; a group of points scatters element-by-element to each point's containing cell. This replaces the earlier raise — the containing box/polygon of a point is well defined, as espg noted.main(e.g.F841intools.py::_normalize_antimeridian_polygon) are left untouched; the new/changed files are clean under the CI flags (--select=E,F,W,I --ignore=E501).🤖 Generated with Claude Code
https://claude.ai/code/session_01N32Mx4rWhbTK8xXm7Mt2ax