High-resolution morton location channel for ragged/CSR fields#165
Conversation
|
🤖 from Claude (review) Fresh-context adversarial self-review of the full phases 1–5 diff (8 finder angles + verification). Findings folded in d93efe8; the rest are recorded here with dispositions. Folded (commit d93efe8)
Verified clean (empirically, not just by reading)
Standing findings (not folded — for review)
Generated by Claude Code |
|
🤖 from Claude Status: phases 1–5 complete, adversarial-review findings folded (d93efe8), CI green on each phase head, full suite 1146 passed / 24 skipped locally. Applying
Generated by Claude Code |
|
🤖 from Claude Reconciled with Generated by Claude Code |
|
branch conflicts need to be resolved. mortie espg/mortie#100 landed and is included in mortie 0.8.5 , so phase 6 is unblocked. |
|
🤖 from Claude Status note (recorded from espg's direction, for the next routine pass): phase 6 is unblocked — espg merged espg/mortie#100 (numpy-level point-kind For whoever advances it: (1) verify 0.8.5 has propagated to PyPI before bumping the floor ( |
…tion-channel # Conflicts: # src/zagg/processing/worker.py
|
🤖 from Claude Both items from your comment are done: Reconcile — merge commit d7a869c brings in Phase 6 — commit 6fc4794: mortie floor bumped to Full suite on the reconciled tree with mortie 0.8.5: 1336 passed, 26 skipped; ruff/format clean on touched files (registry.py N818 still the known pre-existing local-only hit). PR body updated: phase 6 ticked, Generated by Claude Code |
…tion-channel # Conflicts: # pyproject.toml
Closes #87
Implements the high-resolution morton location channel per the approved plan (plan comment, amended by the start-with-(a) directive). All six phases are complete — phase 6 landed after mortie 0.8.5 shipped the numpy-level point-encode surface (espg/mortie#100, sign-off) — so this now
Closes #87.What / approach
A
kind: raggedfield can declarelocation: leaf_idto carry a per-centroid high-resolution location:HealpixGrid.assignnow emits point-kind order-29 morton words (mortieKind::Point, distinct from an area cell), the t-digest reducer collapses each centroid's member points to their deepest enclosing cell viamortie.common_ancestor, and the resulting(k,)uint64 location vector is stored as a CSR companion array ({field}/{shard}/locations) sharingoffsets/cell_idswith the float32 digest — one logical multivariable field, two physical arrays, each channel lossless in its own dtype (the 2^53 float ceiling vs ~2^61.6 order-29 words rules out any interleaved float layout; see the layout discussion).Dependency change: mortie floor
>=0.8.3→>=0.8.5(phase 6; the 0.8.5 release carries bothcommon_ancestorand the numpy-levelgeo2mort(..., points=True)from espg/mortie#100 — bump signed off in espg's comment).Point and area words share path prefixes, so
clip2ordercoarsening — and every existing dense output — is bit-identical (regression-tested). Value-only ragged fields write byte-identical CSR output (byte-level store-snapshot guard).Per-phase map:
grids/healpix.py—assignemits point-kind words (initially viaMortonIndexArray.from_latlon(points=True)+ themorton_wordsadapter; phase 6 swapped this to the numpy-level surface). Coarsen-equivalence + point-kind tests.stats/tdigest.py—build_tdigest(values, delta, locations=None)co-sorts locations with values and reduces each centroid's members viacommon_ancestor(_centroid_ancestors);merge_tdigests(..., locations1=, locations2=)folds mixed-order per-centroid locations the same way. Value-only calls return the bare digest unchanged.config.py/processing/aggregate.py/processing/worker.py—location:validated (ragged-only, function-only, cell-resolution-only, HEALPix-grid-only, known column, reducer must accept alocationskwarg);get_output_signaturegains alocationkey (output_field_signatureincludes it only when set, so existing shard-map signatures stay byte-identical);calculate_cell_statisticspasses the cell'sleaf_idslice and accepts the(payload, locations)pair; the ragged sink delivers(values_list, cell_ids, locations_list)triples for located fields, 2-tuples otherwise.csr.py/processing/write.py/readers/tdigest_tensor.py—write_csr(..., locations_list=)writes{field}/locations(uint64) sharing offsets (per-cell length equality enforced);read_csr(..., locations=True)opt-in read-back;read_locationsreader yields(morton_index, cell_id, locations)aligned withread_raw_values, reading only the three arrays it needs; byte-identity snapshot test proves value-only stores are unchanged key-for-key and byte-for-byte.configs/atl03_tdigest_located_healpix.yaml— shipped located template (differs from the value-only template only bylocation: leaf_id, asserted in test) + end-to-end test: synthetic obs → point-kind assign → located digest with forced merges → CSR →read_locations→ containment holds for every contributing observation; docstrings updated (stats/tdigest.py,csr.py).pyproject.toml+grids/healpix.py— mortie floor>=0.8.5;assignswapped from the pandas-ExtensionArray wrapper to the numpy-levelgeo2mort(lats, lons, order=29, points=True)(word-identical, verified; benchmark below). The explicitorder=stays as the drift self-check (point encoding is order-29-only, so mortie raises ifHEALPIX_REF_ORDERever moves).Post-phase, the adversarial self-review's findings were folded in d93efe8 (guards for unvalidated configs, uint64 strictness, reader IO, helper dedupe, new-code mypy/codespell) — see the review summary for the full fold/standing list.
Reconciles with
main: merge 2853844 (post-#70;tests/test_config.pyresolved additively) and merge d7a869c (post-#152). The #152 merge intersects the located seam once: the new streaming buffered path (StreamingAggregator, issue #148) does not thread per-cell locations, sovalidate_streamingnow rejects located fields with a clear error instead of silently dropping the channel (tested); the pooled path is untouched. Located streaming support is a natural follow-up (the locatedmerge_tdigestslaw already exists) — flagged under "Questions for review".Phases
assignlocation:schema attribute + aggregation plumbinggeo2mort(..., points=True)swap + mortie>=0.8.5floor (6fc4794)Point-encode surface (phase 6 benchmark)
1M random points, mortie 0.8.5, same host as the phase-1 numbers:
geo2mort(area)geo2mort(..., points=True)(numpy-level)from_latlon(points=True)+morton_wordsunwrapnp.asarray/to_numpyunwrapThe swap removes the pandas wrapper entirely: ~11% faster than the adapter path, word-identical output (verified over 10k points), and no private-attribute reliance — the remaining 1.13x over area is the point kernel itself.
How tested
cells_of/shards_ofbit-identical toclip2orderon area words over 5000 random points; a lone point survivescommon_ancestorunchanged (tests/test_grids.py); phase-6 swap word-identical to the wrapper path.tests/test_tdigest.py, 2 new classes).calculate_cell_statistics; ragged sink triple throughprocess_shard; unlocated 2-tuple contract untouched; validation matrix forlocation:incl. reducer-signature and params-collision rejection (tests/test_processing.py,tests/test_config.py); streaming rejection of located fields (tests/test_streaming.py).locationskeys and every shared key is byte-identical to the value-only write (tests/test_csr.py); reader alignment withread_raw_values(tests/test_readers.py).tests/test_processing.py::TestLocatedRaggedAggregation::test_end_to_end_located_write_then_read).uv run pytest -v, mortie 0.8.5 resolved viauv sync).Questions for review
aggregation.streaming: currently rejected atvalidate_streaming(clear error) because the buffered per-cell state doesn't carry locations. The locatedmerge_tdigestslaw exists, so threading it throughStreamingAggregatoris a small follow-up if 88S-scale located products are wanted — say the word and it becomes its own issue/PR.common_ancestorcost: a located build is ~+24% per cell (Python loop, ~1 µs/centroid). A segmented rust-side reduce in mortie (common_ancestor(words, offsets)) would erase it — candidate for a follow-up mortie ask now that numpy-level point-kind geo2mort: geo2mort(..., points=True) espg/mortie#100 has landed.main(not fixed here):ruff checkfails N818 onsrc/zagg/registry.py:64(UnknownCapability), andruff format --checkwould reformat 6 files — both reproduce on a cleanmaincheckout with the synced env (CI's ruff job is green, so it's local-vs-CI ruff version drift); flagged rather than fixed per repo conventions.config.pymodule size: already past the ~1000-line guidance before this change; this PR adds ~75 lines of validation. A split (e.g. validation intoconfig_validate.py) should be its own refactor PR if wanted._aggregate_chunk_cellsreturn arity grew from 4 to 5 (ragged_locationsadded). In-repo callers and stubs updated (incl. re-verifying against 88S stress shard: pins, 900s timeout, chunk-boundary extraction, a-priori read plan, streaming tdigest (issue #148) #152's restructured worker); flagging in case anything external monkeypatches it.