Skip to content

Temporal coverage: packed timestamp|range dual-type time word (design transfer from zagg#410)Β #175

Description

@espg

πŸ€– from Claude (filed at espg's direction, 2026-08-08 β€” design transfer from englacial/zagg#410)

What this is

mortie grows from spatial indexing to a coverage library: spatial (HEALPix/morton, existing) plus temporal. The temporal cell type was designed on zagg#410 β€” see the superseding design comment and the T-MOC research that ruled out the hierarchical-cell alternative β€” and lands here because:

  1. Dependency topology: zagg writes the field, moczarr reads it, and mortie is the dependency both already share.
  2. Kernel synergy: a mortie spatial MOC is already, internally, a sorted list of disjoint u64 ranges (the depth-29 linearization in src_rust/src/moc.rs); a temporal coverage is the same object over a 1-D axis, and the range-walk machinery (cf. the Batch MOC set operations: mocs_and / mocs_intersect and the 1xN broadcast familyΒ #173 mocs_intersect predicate) generalizes directly.
  3. It fits mortie's constraints exactly: pure bit manipulation, zero new dependencies, numpy-only surface.

zagg#410 keeps the consumer side (the per-centroid companion array, its fold law, the config knob, normative spec text + fixtures). This issue is where the type itself gets planned and built.

The type, as decided on zagg#410

One u64, a tagged union of an exact timestamp and a conservative time range:

variant layout (bit 0 = LSB)
timestamp bits 63..1 = nanoseconds since epoch (2^63 ns β‰ˆ 292 yr); bit 0 = 0
range bits 63..32 = start, 32 bits at 2^31 ns (β‰ˆ2.15 s); bits 31..1 = end, 31 bits at 2^32 ns (β‰ˆ4.29 s); bit 0 = 1
  • Bit split 32/31 β€” decided (espg, 2026-08-08, on #410): spatial overview folds merge same-pass centroids constantly, so short-span ranges are the bulk of the range population, and their stored width is pure quantization slop β€” worst case q_start + q_end β‰ˆ 6.4 s under 32/31 vs β‰ˆ 9.7 s under the 33/30 alternative (typical: ~3.2 s vs ~4.8 s). The tighter end bound wins over the finer sort granularity.
  • Outward rounding: start floored, end ceiled to their grids β€” a range is always a conservative superset of its members (the same honesty contract common_ancestor gives the spatial location companion).
  • Sort: discriminator in the LSB, shared field high (the zuniq trick). A timestamp's top 32 bits are ns >> 31 β€” the same 2^31 ns units as a range's start field β€” so plain unsigned sort orders timestamps and ranges chronologically by start, interleaved, no comparator.
  • Merge: min on start codes, max on end codes, timestamps cast outward on first merge. A semilattice join on a fixed epoch-anchored lattice: exactly associative, commutative, idempotent β€” bit-identical under any fold tree, with error bounded at one quantum per end regardless of fold depth.
  • Epoch/timescale β€” decided: nanoseconds since 1850-01-01T00:00:00 on a continuous, leap-free, GPS-aligned scale (equals GPS time + one fixed constant after 1980; proleptic before). Verified endpoints: CMIP historical starts 1850 (the binding constraint), NCEP/NCAR R1 starts 1948; ceiling β‰ˆ 2142 covers standard ScenarioMIP to 2100. Documented exclusions: CMIP extended-to-2300 scenarios, pre-1850 reanalyses. Leap seconds appear only at the UTC boundary (datetime64/pandas import-export), via the static leap table.
  • Rejected alternatives (recorded on #410 with the kill arguments): hierarchical T-MOC-style cell (unbounded merge coarsening, measured), floating-point absolute time (precision inverted β€” worst on newest data), unit-scale fields and calendar tuples (sort/path-independence/radix-waste failures). Held in reserve: uniform start + float duration (finer everywhere, but ULP-level merge path-dependence).

Open questions

  1. Base quantum: 1 ns vs 2 ns. 2 ns doubles the span to 585 yr (1850β†’2434), covering CMIP extended scenarios, at zero physical cost for ICESat-2 (float64 delta_time carries only ~50 ns at mission magnitudes). Lean: plain ns, exclusions documented; flip only if 2300-scenario output is declared in scope. Must be settled before the epoch constant freezes.
  2. Module and naming. A new mortie/temporal.py (mirrored by src_rust/src/temporal.rs)? Names for the type's operations (time_encode/time_merge/…? a TimeWord framing?) β€” should read as one family with the morton word ops.
  3. Phase-1 scope. Scalar encode/decode/merge + vectorized numpy batch forms (encode from datetime64[ns], merge two arrays, reduce an array), UTC↔internal conversion helpers with the leap table. Interval-set coverage algebra (union/intersect over sorted arrays of these) is explicitly deferred β€” espg parked it pending the scalar type; when it lands it should share the range-walk kernels with the spatial set ops.
  4. Conformance surface. Which invariants are normative and fixture-pinned: sort-order property, semilattice laws (associativity/commutativity/idempotence), outward-conservatism, round-trip bounds, the epoch/scale constants.

Sequencing

Blocked on nothing in mortie; gated only on the tail of the zagg#410 discussion (the zagg-side fold law and spec text develop in parallel there). The batch entry points, when they come, land in mortie/batch.py per #170.

Refs: englacial/zagg#410 (design record), englacial/zagg#414 (the compression-factor change the leaf-precision story depends on), #173 (the range-walk kernels this shares), #170 (batch.py).

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions