mortie specification & conventions (v1.0)
This page is the normative record of the mortie 1.x stability contract: the packed-word encodings (the spatial morton word and the temporal toc word), the decimal string grammar, the morton-hive store layout, the coverage-MOC serializations, and the zarr DGGS convention block for morton-declared stores. Everything marked contract here is frozen for the 1.x major-version series; anything not marked contract is informative.
Design rationale — why each decision was made, with trade studies and
ratification records — lives in zagg's
docs/design/sparse_coverage.md
(the decisions registry, D-numbered entries). That document cites this page;
this page is the spec. Grammars and constants are normative here only —
duplicated normative text drifts.
Contents:
- The packed 64-bit morton word
- Decimal string representation
- Resolution table
- Order-29 points: encoding-carried kind and the decimal-parse tie-break
- Zarr DGGS convention block
- Morton-hive store layout
- Coverage MOC serializations
- Rank-space (x, y) deinterleave
- Latitude convention: authalic on WGS84
- Frozen for 1.x
- The packed 64-bit toc word
1. The packed 64-bit morton word
Contract. A morton index is one unsigned 64-bit word encoding a HEALPix
NESTED cell (or an order-29 point) with its order carried intrinsically.
Source of truth in code: mortie-core/src/decimal_morton.rs (MAX_ORDER = 29,
BODY_TUPLES = 27), re-exported as mortie_rustie::decimal_morton.
Bit layout (MSB → LSB)
[ 4-bit prefix ][ 54-bit body (27 x 2-bit) ][ 6-bit suffix ]
63 .. 60 59 .. 6 5 .. 0
- prefix (bits 63–60) — the HEALPix base cell stored as
base_id + 1, so the 12 base cells occupy1..=12.0is the empty/null sentinel;13..=15are invalid. The+1shift is monotonic, so a raw unsigned sort is preserved as a Z-order curve. - body (bits 59–6) — 27 two-bit tuples, one per order
1..=27. Order 1 occupies the highest tuple (bits 59–58), order 27 the lowest (bits 7–6). The stored value is0..=3, interpreted as1..=4(a decode-time+1, matching the decimal grammar in §2). - suffix (bits 5–0) — one plain unsigned integer
0..=63, a preorder numbering of the path tail past tuple 27:
| suffix | meaning |
|---|---|
0..=27 |
variable-length area element; the order is the suffix value (0 = base-cell-only). 27 = order-27 with empty tail. |
28..=47 |
order-28/29 area cells in parent-first preorder: r = t28*5 + (t29_present ? t29 + 1 : 0), suffix = 28 + r, with t28/t29 the stored 0..=3 tuple values. Each t28 owns a 5-block: [t28] then its four [t28,t29] children. |
48..=63 |
order-29 point, max-encoded (no area claim): r2 = t28*4 + t29, suffix = 48 + r2. A point sorts after every area cell sharing its body (highest suffix range) — the finest, last thing there. |
- Canonical zero-fill: every bit below an element's order is zero-filled, so two encodings of the same cell are bit-equal — integer equality, hashing, dedup, and the raw sort all work on the word directly.
- Unsigned storage: the word is stored and exchanged as
uint64. The signed "negative = southern" form is a presentation detail of the decimal string (§2), never a storage form. Reinterpreting the word asint64is an error: base cells 7–11 set bit 63 and would read back negative. - Z-order: a raw unsigned sort of packed words is a Z-order (Morton) curve traversal, parent-before-children across the whole order range 0–29.
Kind: area vs point
A decoded word is either an area element (a real cell with spatial extent, orders 0–29) or an order-29 point (a location cast to maximum resolution with no area claim — e.g. a raw lat/lon conversion). The kind is carried by the suffix range, not by any external flag — the load-bearing convention §4 states normatively — and the only place the two kinds can collide is the order-29 decimal string, resolved by the §4 parse tie-break.
2. Decimal string representation
Contract. The decimal string is the render/interchange external form of a
morton index (paths, logs, inventories, display). Packed uint64 words are
the storage and compute form; output types are never data-dependent:
- strings for display/interchange at every order;
- packed
uint64for storage and compute; - legacy signed
i64only via the explicit, cappedto_legacy_i64()escape hatch.
Grammar:
morton-decimal = ["-"] base-digit *order-digit [kind-suffix]
base-digit = "1" / "2" / "3" / "4" / "5" / "6"
order-digit = "1" / "2" / "3" / "4"
kind-suffix = "p" ; POINT ids only; render/interchange form only
- Sign + base digit form a constant-width component with 12 values
(
1..6/-1..-6; the sign renders the southern base cells). One order-digit follows per order; digits are1-4, never0. The string length minus the sign/base component is the order. - String prefix = spatial ancestor at every level:
-31123is a descendant of-311, and lexicographic grouping under a fixed-width prefix is spatial grouping. - The base component grammar is
-?[1-6]— reserved wherever names must be distinguishable from morton components (see §6.5). - Kind suffix
p(espg-ruled in review, 2026-07-21): the render form MAY carry a terminalpon point ids (e.g.-62…21p), and only there — apis legal solely on a full order-29 string, because points exist only at order 29. Paths never carry it: points don't live in paths, and the §6 path grammar is unchanged. Rationale: no letter occurs anywhere in the base grammar (sign, base digit, digits1-4), so a letter suffix is maximally distinguishable, greppable, and inert in shells, markup, and URLs;*and a terminal.were considered and rejected (glob/markdown hazards; trailing-dot stripping).
Order-29 kind marking and the unmarked tie-break
With the kind suffix, the decimal round-trip is lossless for both
kinds — the normative contract: an area word renders unmarked and parses
back to itself; a point word renders p-marked and parses back to itself.
The p emit/accept is implemented in
PR #121 (issue #120), with
golden vectors. The residual ambiguity
is only the unmarked order-29 string — path components and legacy
renders — which denotes both kinds and parses as the area word (the
normative §4 tie-break; every pre-suffix string is an area context, so the
rule is fully backward compatible).
3. Resolution table
Informative (the formulas are contract; the rendered values are derived).
nside = 2^order; both columns derive from one Earth model — the exact
HEALPix sphere at mean radius R = 6371.0088 km. Every order-k HEALPix
cell has identical area 4πR² / (12 · 4^order) (HEALPix is equal-area by
construction); the cell scale is the square root of that area — the RMS
cell spacing sqrt(4πR² / (12 · 4^order)). The table below is regenerated
from these formulas and pinned by mortie/tests/test_spec_page.py so it
cannot drift.
Note — code and page unified. These are the normative, sphere-derived
values, and mortie.orders.order2res now derives from the same sphere:
order2res(order) = sqrt(4πR² / (12 · 4^order)) with the single
mortie.orders.EARTH_RADIUS_KM = 6371.0088 constant. Its consumers
(res2display and the buffer-pad computation in
tests/test_coverage_boundary.py) therefore read the cell-scale column
below directly. This replaced the historical flat constant
111 km/deg × 58.6323 × 0.5^order (an implied sphere R ≈ 6366 km), a
behavioral change of ~0.2% at every order, per
mortie #119.
| order | nside | cell scale | cell area |
|---|---|---|---|
| 0 | 1 | 6,519.623 km | 4.25055e+07 km2 |
| 1 | 2 | 3,259.812 km | 1.06264e+07 km2 |
| 2 | 4 | 1,629.906 km | 2.65659e+06 km2 |
| 3 | 8 | 814.953 km | 664,148 km2 |
| 4 | 16 | 407.476 km | 166,037 km2 |
| 5 | 32 | 203.738 km | 41,509.3 km2 |
| 6 | 64 | 101.869 km | 10,377.3 km2 |
| 7 | 128 | 50.935 km | 2,594.33 km2 |
| 8 | 256 | 25.467 km | 648.582 km2 |
| 9 | 512 | 12.734 km | 162.146 km2 |
| 10 | 1024 | 6.367 km | 40.5364 km2 |
| 11 | 2048 | 3.183 km | 10.1341 km2 |
| 12 | 4096 | 1.592 km | 2.53352 km2 |
| 13 | 8192 | 795.852 m | 633,381 m2 |
| 14 | 16384 | 397.926 m | 158,345 m2 |
| 15 | 32768 | 198.963 m | 39,586.3 m2 |
| 16 | 65536 | 99.482 m | 9,896.58 m2 |
| 17 | 131072 | 49.741 m | 2,474.15 m2 |
| 18 | 262144 | 24.870 m | 618.536 m2 |
| 19 | 524288 | 12.435 m | 154.634 m2 |
| 20 | 1048576 | 6.218 m | 38.6585 m2 |
| 21 | 2097152 | 3.109 m | 9.66463 m2 |
| 22 | 4194304 | 1.554 m | 2.41616 m2 |
| 23 | 8388608 | 77.720 cm | 0.604039 m2 |
| 24 | 16777216 | 38.860 cm | 0.15101 m2 |
| 25 | 33554432 | 19.430 cm | 0.0377525 m2 |
| 26 | 67108864 | 9.715 cm | 0.00943811 m2 |
| 27 | 134217728 | 4.857 cm | 0.00235953 m2 |
| 28 | 268435456 | 2.429 cm | 0.000589882 m2 |
| 29 | 536870912 | 1.214 cm | 0.000147471 m2 |
4. Order-29 points
Contract. Kind is carried by the encoding itself — never by store or array metadata. The packed word's suffix region (§1) is the kind:
- suffix
0..=47decodes as an area word — an exact cell at its encoded order, at every order 0–29. An order-29 area word is a genuine order-29 cell; nothing is unrepresentable. - suffix
48..=63decodes as an order-29 point — a location with no area claim, full stop.
There is no resolution/kind field in the §5 attrs block and readers never
consult a declaration: two encodings, two meanings, one word. Mixed content
— exact cells at any orders alongside order-29 points — is well-formed in a
single coordinate by construction, because each word carries its own kind.
(Provenance: espg-ratified 2026-07-21 on the PR #118 review, superseding
the drafted declaration-based designs.)
The decimal-parse tie-break (contract)
Packed words never collide across kinds — the suffix ranges are disjoint. The one ambiguity in the convention is the decimal repr at order 29: a full order-29 string (base component + 29 digits) denotes both the order-29 area cell and the max-encoded point on the same path, because the string carries path only, never kind (§2). Strings at orders 0–28 denote area cells alone (points exist only at order 29). The normative tie-break:
A
p-marked string yields the POINT word; an unmarked string always yields the AREA word. For an order-29 path with final two (stored) tuple valuest28,t29: unmarked ⇒ the area word (suffix28 + t28·5 + t29 + 1);p-marked ⇒ the point word (suffix48 + t28·4 + t29). Round-trip identity holds for both kinds; the unmarked-string rule is the tie-break for the one truly ambiguous form (§2), and since every pre-suffix string is unmarked it is fully backward compatible.
In channels that strip or cannot carry the suffix (paths above all),
point-ness does not survive the string: those channels carry the packed
word when kind matters. The unmarked tie-break is what mortie's parser has
always implemented, golden-pinned by mortie/tests/test_spec_page.py; the
p emission/acceptance is implemented in
PR #121 (issue #120), with
golden vectors.
Parse-side API (normative surface)
The tie-break above is a parse-side contract, so it is worth stating in
the terms a parse caller sees. The public entry points, all implementing the
same kernel (decimal_morton::from_decimal_repr):
| Entry point | Shape |
|---|---|
mortie.decimal_to_word(s, dtype=np.uint64) |
scalar; numpy-only (no pandas import), dtype selects np.uint64 / int / MortonIndexScalar |
mortie.decimals_to_words(arr) |
vectorized; the inverse of MortonIndexArray.to_decimal(), shape-preserving, uint64 out |
MortonIndexArray.from_decimal(arr) |
the same parse as an ExtensionArray constructor, the inverse of .to_decimal() |
What a parse-side caller must know: an unmarked order-29 id parses to the
area word, so a point word does not round-trip through an unmarked string.
Emit renders point words p-marked, so word → to_decimal → from_decimal
is the identity end to end — but any channel that strips the marker (a
path component, a legacy render, a hand-typed id) returns the area word for
what may have been a point. At orders 0–28 there is no ambiguity to resolve.
Pinned by mortie/tests/test_decimal_parse.py.
Points at coarser levels (informative)
Membership of a point in a coarser cell is the ordinary truncation
(coarsen / clip2order: drop the path tail below the target order) — a
transient cast computed where needed, never a stored re-encoding.
Viewer-side float64 casts (informative)
IEEE-754 float64 (JavaScript Number, plain JSON parsers) is
integer-exact only to 2^53, which covers NESTED ids only through order 24.
A display layer in such a runtime (e.g. the gridlook viewer) may
transiently cast point ids to order ≤ 24 for Number safety — an
implementation detail of that display layer, not encoding semantics; other
viewers and future runtimes need no such cast. Area cells are never
coarsened this way: coarsening an area cell changes the labelled thing
(that is aggregation, cf. zagg D24) — servers fabricate or aggregate
instead.
5. Zarr DGGS convention block
Contract. A zarr store whose cell coordinate is packed morton words declares, on the group holding the cell-indexed arrays:
{
"zarr_conventions": [
{
"schema_url": "https://github.com/espg/mortie/blob/main/docs/specification.md#dggs-attrs",
"spec_url": "https://github.com/espg/mortie/blob/main/docs/specification.md",
"uuid": "3e22156d-ea9e-4e01-95fe-e3809a4b41e7",
"name": "morton-dggs",
"description": "Packed-u64 morton (HEALPix) DGGS convention"
}
],
"dggs": {
"name": "morton",
"coordinate": "morton",
"latitude": "authalic-wgs84",
"...": "grid parameters (refinement level, ellipsoid, ...)"
}
}
name: "morton"andcoordinate: "morton"— the grid name is distinct, nevername: "healpix"+indexing_scheme: "morton". A scheme-blind reader that recognizeshealpixbut ignores the indexing scheme would silently decode morton words as NESTED ids and mis-place every cell; an unknown grid name makes it hard-reject with a diagnostic instead. This matches moczarr's xdggs registration (grid_name: "morton").- There is no kind/
resolutionfield: point-vs-area kind is carried by the word encoding itself (§4), never by attrs. - Latitude convention — the grid-parameter block records the latitude
convention of §9 under the key
latitude, whose two tokens are"authalic-wgs84"(the default convention) and"geodetic-spherical"(the legacy escape). A writer at this spec version MUST record it; see §9 for what an absent marker means and for the reader obligation. Readers must refuse to compose covers across conventions. - Convention identity — the
zarr_conventionsentry above is the self-declared convention record (the zarr-conventions mechanism supports self-declared entries). The UUID3e22156d-ea9e-4e01-95fe-e3809a4b41e7is minted once and permanent; readers may key on it.zarr_conventionsis a list: a future upstream dggs-registry entry coexists alongside this one rather than replacing it.
6. Morton-hive store layout
Contract. The morton-hive layout stores one self-describing zarr per
spatial shard under a morton digit tree. The convention is versioned by the
manifest's spec string (morton-hive/1, /2, /3); every frozen
grammar remains valid forever for stores declaring its version — readers
discriminate by the spec string, never by sniffing names.
6.1 Common structure (all versions)
{store_root}/
morton_hive.json <- static manifest, versioned `spec` string
coverage.moc <- optional root coverage MOC (§7.3)
{sign+base}/{d1}/{d2}/.../ <- one decimal digit per path level
{leaf}.zarr/ <- vanilla zarr v3 leaf (naming per version)
<declared sidecars> <- stats record, sub-shardmap (per version)
- Digit tree: path components are the decimal-string components of §2 —
the constant-width
{sign+base}first, then one digit ([1-4]) per order. Shards live at mixed orders, so every order is a legal node. A manifestpath_groupingparameter (default1) declares how many digits each component chunks; readers chunk the digit string per the manifest, never by assumption. - Grouping remainder (contract): the
{sign+base}component stands alone and never participates in grouping —path_groupingchunks only the order-digit string that follows it. When the order is not a multiple ofpath_grouping, the leading digit components are full-width and the final component carries the remainder (never remainder-first). This is the only split that keeps a coarser shard's grouped prefix a directory-prefix of its finer descendants, so shared ancestor directories survive across mixed shard orders. Worked example — order 8 atpath_grouping: 3chunks the eight order-digits33142241as3+3+2(331/422/41), leaving{sign+base}4as its own leading component:
4/331/422/41/433142241.zarr <- id 433142241, order 8, path_grouping: 3
- Node invariant: below a product root, a node contains only digit
children,
*.zarrobjects, and the declared leaf-adjacent sidecar names — nothing else, ever. The walker's child classification depends on the name set being closed. - Termination: object stores have no empty prefixes and S3 LIST is strongly consistent, so a delimiter-LIST returning no digit children is a definitive "nothing finer exists". Absence is trustworthy.
- Commit stamp: presence is not trustworthy without one. A leaf's root
metadata is finalized last (a root-group attrs update carrying the
stamp); a
.zarr/prefix whose root metadata lacks the stamp is debris — incomplete, ignorable, safe to overwrite on retry. - Overview flagging: ancestor-node zarrs must carry
role: overviewattrs; the role is never inferred from tree position (a shallow zarr may be coarse source in a sparse region). - Manifest:
morton_hive.jsonat the (product) root is the reader's bootstrap; with it every shard path is computable arithmetically with zero requests. Itsspecstring versions the convention.
6.2 morton-hive/1 — bare leaves
Leaf basename is the full morton id: {full_id}.zarr (e.g.
.../1/2/3/-31123.zarr/). Self-describing without parsing its path.
6.3 morton-hive/2 — time-windowed leaves
A /2 store's manifest carries a temporal block declaring time
encoding/units/epoch/calendar, the membership timestamp field, the window
schedule, and the append policy. A /1 store is a /2 store with
schedule: none.
Leaf naming:
{full_id}_{window}.zarr <- windowed leaf (schedule != none)
{full_id}.zarr <- bare leaf (schedule == none only)
- Separator is
_; it never appears in morton ids nor in window labels, so the split is unambiguous. Parse rule: split on the first_. - One schedule per store; at most one leaf per (id, window); bare and windowed source leaves never mix in one store.
Window schedules and label grammar (lexicographic order = chronological order within a store):
| schedule | label grammar | example label | window |
|---|---|---|---|
none (default) |
(no label) | — | unbounded; re-run replaces the leaf |
yearly |
YYYY |
2025 |
[2025-01-01T00:00Z, 2026-01-01T00:00Z) |
monthly |
YYYYMM |
202511 |
calendar month |
daily |
YYYYMMDD |
20251103 |
calendar day |
quarterly (grammar-reserved, not implemented) |
YYYYQ[1-4] |
2025Q3 |
calendar quarter |
| explicit list | opaque, [0-9A-Za-z-]{1,32} |
melt-2019 |
declared per label in the manifest |
- Boundaries are UTC calendar terms, half-open
[start, end), regardless of the store's native time encoding (the temporal block declares the conversion). An observation stream straddling a boundary contributes to both windows. - Explicit labels are opaque: the manifest maps each label to its
[start, end); readers never parse semantics out of a custom label. The charset excludes_by construction. - The reserved token
all(§6.4) is excluded from the window-label grammar going forward: no store written under any spec version may declare an explicit window labeledall. In/2the token has no structural role — theschedule: noneleaf is the bare{full_id}.zarr, notall.zarr(that is a/3construct) — so the reservation is a forward-going constraint on new writers, not a retroactive narrowing of the frozen/2opaque grammar. A pre-existing/2store whose manifest happened to declare analllabel stays readable under its frozen grammar:/2labels are opaque and manifest-resolved, soallthere is just another custom label. - Per-shard stats sidecars (when written):
stats_{window}.json, orstats.jsonunderschedule: none.
6.4 morton-hive/3 — window-only leaf naming
Completes the axis separation: product = root prefix (§6.5), space = digit path, time = basename — each identity axis in exactly one place.
{window}.zarr <- leaf, basename = time window alone
all.zarr <- schedule: none (reserved token)
- The morton id no longer appears in the basename; it is recoverable
arithmetically from the digit path and recorded in the leaf's stamp
attrs / stats sidecar (
shard_key). allis a reserved token: it names the/3schedule: noneleaf (reads as "all time"), cannot collide with the digit-shaped generative labels, and is excluded from the window-label grammar going forward (no store, any spec version, may declare an explicit window labeledall). The token is structural only in/3; §6.3 records how the same forward-going reservation applies to/2, which has noallleaf.- Sidecar naming aligns to the leaf:
{window}.stats.json/all.stats.json. /1and/2stores keep their frozen grammars (§6.2, §6.3) forever; readers discriminate by the manifestspecstring.
6.5 Product roots and the product-name grammar
A multi-product store is a directory of stores: each product lives under
its own human-readable root prefix {name}/, and a product subtree is a
complete, unmodified morton-hive store (bare-named manifest, MOC, digit
tree). A bare single-product store (manifest at the store root) remains
fully valid; readers distinguish the two root forms by content — a
manifest at the root ⇒ bare store; name-shaped prefixes ⇒ product
directory.
Product-name grammar (contract):
product-name = 1*192( lowercase-alphanum / "-" / "_" ) ; [a-z0-9_-]{1,192}
with the base-component exclusion: a product name must not match the
morton base-component grammar -?[1-6] (§2), so the walker's child
classification stays unambiguous. Names are URL-safe by construction (no
percent-encoding, no case-folding hazards).
- Length 1–192 characters. The charset is single-byte ASCII, so 192
chars = 192 bytes. Derivation: the POSIX filename-component ceiling is 255
bytes; a D23 downloader that materializes the tree locally shares the
product's path component with a 13-character immutable-provenance
decoration (
{name}+{catalog-hash}/— a+plus a 12-hex catalog fingerprint), leaving a hard ceiling of255 − 13 = 242. The 192 cap sits 50 characters under that ceiling and keeps total-path budgets comfortable (~400 chars of the 1,024-byte S3 total-key budget, and macOSPATH_MAX, at order-24 digit depth under realistic prefixes).
6.6 Repr reminder for paths
Hive paths embed decimal components (§2). The §2 non-injectivity note applies: leaf ids at order 29 could not distinguish point from area — hive stores avoid the ambiguity structurally (shards live at coarse orders, and in-store kind rides the packed words themselves, §4) — and any parsed path string resolves by the §4 tie-break; no implementation may round-trip point-ness through a path string.
7. Coverage MOC serializations
Contract (spec: "morton-moc/1"). Coverage is declared in three tiers;
all three share the envelope's encoding discriminator:
"ranges" | "bitmap" | "full".
7.1 Stamp envelope: the tier-0 morton box
The leaf commit stamp carries the shard's morton box — the canonical
≤ 4-member MOC covering its occupied cells — padded to exactly four
decimal-string slots; the pad sentinel is JSON null. encoding:
"full" declares a fully occupied subtree (the shard id itself is the exact
MOC; no bitmap sidecar is written).
7.2 Leaf bitmap sidecar (coverage.moc inside the leaf)
Exact cell-order occupancy for one shard subtree:
- The raw bitmap has
4^depthbits (depth = cell_order - shard_order); raw size is exactlyceil(4^depth / 8)bytes. - Bit convention (contract, golden-vector-pinned): bit
iis the i-th shard-subtree cell in ascending packed-word order — equivalently, the base-4 rank of the cell's digit tail with digits1..4mapped to0..3— packed MSB-first within each byte (np.packbitsorder). - The payload is one zstd stream over the raw bitmap. The zstd stream format is contract; the compression level is non-normative (any level decodes identically).
- A decoder must reject a payload whose decompressed size is not exactly the raw bitmap size — never zero-pad or truncate (a partial cell set is a false negative).
7.3 Root coverage MOC (coverage.moc at the store/product root)
A JSON envelope with encoding: "ranges" listing shard-order coverage.
Required keys (a reader validates the set, never infers it):
spec—"morton-moc/1"(§7 header), tying the envelope to this serialization;encoding—"ranges"for this tier;order— integer, the shard order of every cell inranges(the common order the decimal endpoints share);ranges— the list of[first, last]runs below.
source, generated_at, and optional time_range are informative carrier
fields. Field semantics:
- A range is an inclusive
[first, last]run of same-order cells within one base cell, consecutive in base-4 digit-tail rank (the same ascending packed-word order as §7.2) — range ordering is contract (golden-vector-pinned). - Endpoints are decimal strings (§2), never JSON numbers: packed words
exceed 2^53 and would be silently mangled by float-based JSON parsers.
They are always unmarked area words at the shard order, never
p-marked: points live only at order 29 and cannot be range endpoints. - The carrier fields above are informative cache metadata; the ranges are a regenerable cache of the leaf-stamp truth.
8. Rank-space (x, y) deinterleave
Contract. A depth-d subtree holds 4^d cells whose ascending
packed-word order is a Z-order (morton) curve over a 2^d × 2^d block. The
rank of a cell — its position 0..4^d − 1 within the subtree, the same
base-4 digit-tail rank the §7.2 bitmaps index by — maps to a face-local
(x, y) pair by pure bit deinterleave. Source of truth in code:
mortie/rank_xy.py (rank_to_xy / xy_to_rank; the pure-numpy mask
ladder retained there is the executable reference and golden-vector
generator, while the shipped kernel is the thin Rust binding in
src_rust/src/rank_xy.rs over the vendored healpix crate's z-order
curve — equivalence is test-pinned elementwise across depths).
- Bit parity:
xis the gather of the rank's even bits (bit 0, 2, 4, …),ythe gather of its odd bits (bit 1, 3, 5, …). Equivalentlyrank = interleave(x, y)withxsupplying the LSB. - Orientation: the origin
(0, 0)is the subtree's south corner;xincreases toward the north-east edge,ytoward the north-west edge — the HEALPix face-coordinate frame. - Subtree-locality: the input is rank-space, never a packed morton
word (§1). A word carries base cell, order, and kind; strip the shard
prefix down to the digit-tail rank first. Whole-word
(base, x, y)decomposition is deliberately out of scope here and reserved for a future layer that composes word → (prefix, rank) → this transform. - healpy equivalence (the community convention this attaches to): for
nside = 2^d,rank_to_xy(r, d) == healpy.pix2xyf(nside, r, nest=True)[:2]andxy_to_rank(x, y, d) == healpy.xyf2pix(nside, x, y, 0, nest=True)— i.e. HEALPix C++pix2xyf/xyf2pixrestricted to one face. The golden vectors inmortie/tests/test_rank_xy.pypin this at depths 6 and 8.
8.1 Worked example (depth 2)
rank = 6 = 0b0110: even bits (b0, b2) = (0, 1) give x = 0b10 = 2; odd
bits (b1, b3) = (1, 0) give y = 0b01 = 1. The full 4 × 4 block, x
left→right and y bottom→top (origin at the lower-left / south corner):
y=3 | 10 11 14 15
y=2 | 8 9 12 13
y=1 | 2 3 6 7
y=0 | 0 1 4 5
x=0 x=1 x=2 x=3
Reading the ranks 0, 1, 2, 3, … traces the familiar Z (self-similar
across depths): child tuples order as (x, y) = (0,0), (1,0), (0,1), (1,1)
at every level.
9. Latitude convention: authalic on WGS84
Contract. Geographic coordinates crossing the mortie API are WGS84
geodetic latitude / longitude in degrees. Since issue #186, the default
convention latitude="authalic" converts geodetic latitude to authalic
latitude before the spherical HEALPix kernel (ingress) and back after it
(egress), making every cell equal-area on the WGS84 ellipsoid by
construction. Longitude never converts. The legacy convention
latitude="geodetic-spherical" (geodetic latitude fed to the spherical
kernel as-is — the pre-0.10 behavior) remains available as an explicit
escape on every crossing.
Reference ellipsoid (pinned, normative). The conversion depends only on the WGS84 defining constants, fixed across all WGS84 realizations — never on the geoid or the epoch:
a = 6378137 (semi-major axis, metres; exact)
1/f = 298.257223563 (inverse flattening; exact decimal)
e^2 = f (2 - f) (derived — compute it, do not parse a decimal)
a and 1/f are the two normative inputs; e^2 is derived from them
and an implementation MUST compute it rather than parse a printed decimal.
Evaluating f = 1/298.257223563 then f (2 - f) in binary64 yields
0.0066943799901413165 (shortest round-tripping decimal; the value
src_rust/src/authalic.rs actually uses). The correctly rounded 16-digit
decimal of the exact real, 0.006694379990141317, parses to the next
double up — a 1-ulp difference, ~1e-19 rad in the series and six orders
inside the error bound below, but enough that quoting it as the number to
reproduce would be wrong. Implementations may legitimately differ in the
last ulp here depending on evaluation order.
The mapping (normative). Both directions are 5-harmonic trigonometric
series with coefficients that are exact rationals in powers of e^2 through
e^10. The closed form they approximate is Snyder 1987 eqs. 3-11/3-12:
q(phi) and beta = asin(q(phi) / q(pi/2)). Snyder's eq. 3-18 is the
inverse series (authalic → geodetic) — its published e^2/e^4/e^6
terms are reproduced exactly by I1/I2/I3 below. The forward series
F1..F5 is this project's reversion/perturbation of the closed form, and
the e^8/e^10 extensions in both directions are likewise derived here
rather than published by Snyder:
beta = phi + F1 sin(2 phi) + F2 sin(4 phi) + ... + F5 sin(10 phi)
phi = beta + I1 sin(2 beta) + I2 sin(4 beta) + ... + I5 sin(10 beta)
F1 = -(1/3) e^2 - (31/180) e^4 - (59/560) e^6 - (42811/604800) e^8
- (605399/11975040) e^10
F2 = (17/360) e^4 + (61/1260) e^6 + (76969/1814400) e^8
+ (215431/5987520) e^10
F3 = -(383/45360) e^6 - (3347/259200) e^8 - (1751791/119750400) e^10
F4 = (6007/3628800) e^8 + (201293/59875200) e^10
F5 = -(5839/17107200) e^10
I1 = (1/3) e^2 + (31/180) e^4 + (517/5040) e^6 + (120389/1814400) e^8
+ (1362253/29937600) e^10
I2 = (23/360) e^4 + (251/3780) e^6 + (102287/1814400) e^8
+ (450739/9979200) e^10
I3 = (761/45360) e^6 + (47561/1814400) e^8 + (434501/14968800) e^10
I4 = (6059/1209600) e^8 + (625511/59875200) e^10
I5 = (48017/29937600) e^10
phi (geodetic) and beta (authalic) are in radians throughout this
block — the series is only valid in radians, while the degree-facing API of
the Contract paragraph converts on both sides (forward_rad(lat.to_radians())
.to_degrees()). Feeding degrees straight into sin(2 phi) silently produces
a different partition. The coefficients are named by position here and by
harmonic in the implementation: F1..F5 are FWD_S2..FWD_S10 and I1..I5
are INV_S2..INV_S10 in src_rust/src/authalic.rs.
The derivation, high-precision reference values, and the offline generator
are mortie/tests/generate_authalic_reference.py →
mortie/tests/data/authalic_reference.json; the runtime implementation is
src_rust/src/authalic.rs.
Error bound (normative). Truncation error of the series against the
60-digit closed form, measured on a 0.01-degree grid: 6.2e-15 rad forward,
8.2e-15 rad inverse. The documented conversion bound is <= 1e-13 rad
(~0.6 µm on the ground) per direction (f64 evaluation rounding included);
unit tests enforce <= 1e-14 rad against the committed references. The
equator and the poles are exact fixed points; the divergence between the two
conventions peaks near 45° latitude at |beta - phi| ~= 0.12830°
(~14.26 km along the meridian).
The bound is each implementation's conversion-accuracy obligation, not a
statement about mortie alone: an implementation conforms if each direction
lands within <= 1e-13 rad of the reference values in
mortie/tests/data/authalic_reference.json. What it does not promise is
identical cell assignment everywhere — §10 freezes the coefficients but not
the evaluation (mortie uses a Chebyshev recurrence with one sin_cos and
degree↔radian conversions; a different-but-conformant series differs by
ulps). Two conformant implementations therefore agree on the cell id of any
coordinate farther than the combined bound (~1e-13 rad, ~0.6 µm) from a cell
boundary, and may legitimately disagree for coordinates closer than that.
Edge cases (normative). Reproducing the cell ids requires reproducing three rules alongside the series:
- Pole clamp, conditional. For input in
[-90, 90]the converted value is clamped back into[-90, 90](the inverse series overshoots ±90 by an ulp near the pole). Input outside[-90, 90]is deliberately not clamped and passes through converted-but-out-of-range, so an invalid latitude cannot silently become a valid pole cell — the legacy convention passes it through unconverted for the same reason. - Non-finite propagates unchanged. NaN → NaN and ±inf → ±inf; the raw series would return NaN for the infinities, so implementations short- circuit non-finite input before evaluating it.
- Exact fixed points at 0 and ±90. These hold structurally, for any
coefficients, since
sin(2k·0) = sin(2k·(π/2)) = 0.
Non-correspondence (normative). Cell ids produced under the two
conventions are non-corresponding partitions of the sphere: the same
(lat, lon, order) generally hashes to different morton words, and the same
word decodes to different geodetic coordinates. Datasets MUST NOT mix
conventions, and set operations (occupancy AND/OR across covers) are
meaningful only within one convention.
Store-level metadata therefore carries the convention, in the latitude key
of the §5 dggs block, with exactly two tokens:
"latitude": "authalic-wgs84" the default convention of this section
"latitude": "geodetic-spherical" the legacy escape
A writer at this spec version or later MUST record one of them — the marker is not optional now that authalic is the default, because the most likely producer of an unmarked store is a new writer that took the default and never set the attr, and misreading that as legacy is silent (~0.128° of misplacement, no error). Absent ⇒ legacy geodetic-as-spherical applies only to stores predating this spec version; a reader SHOULD treat an absent marker on a store at this spec version or later as an error rather than assume either convention. This is mortie's recommended vocabulary; a consuming project's own attrs spec (zagg's, for its stores) remains authoritative for that project.
Symmetry rule. The conversion applies at every geodetic lat/lon
crossing or nowhere: ingress converts forward before the kernel, egress
applies the inverse on the way out. In mortie the ingress surfaces are point
binning, polygon and linestring coverage (flat and MOC), WKB ingest (single
and batched), the ring predicates, the prefix-trie geodetic entry points,
and the dataframe/arrow skins; the egress surfaces are cell centres,
boundaries, bounding boxes, polygon/WKB/WKT emit, and dissolve outlines.
The rule, not the enumeration, is normative: any surface that accepts or
returns geodetic lat/lon converts, and every such surface exposes the same
latitude= escape. Below the API boundary the kernel operates purely in the
authalic frame — spherical primitives never convert, and nothing converts
twice.
10. Frozen for 1.x
The 1.x contract guarantees, immutable within the major version:
- the §1 bit layout, order range 0–29, canonical zero-fill, unsigned storage, and the raw-sort Z-order property;
- the §2 decimal grammar, its render/interchange status, and the emit conventions
(strings display /
uint64storage / capped legacyi64escape hatch); - the §4 encoding-carried kind convention (suffix
0..=47= area, exact at every order;48..=63= order-29 point) and the decimal parse rules (p-marked string ⇒ point word; unmarked string ⇒ area word — the tie-break; thepkind suffix is render/interchange-only and never appears in paths); - the §5 convention identity (UUID) and the
name: "morton"/coordinate: "morton"declaration; - the §6 hive grammars —
/1,/2,/3each frozen for stores declaring itsspecstring — including the_split rule, the window-label grammars, theallreserved token (structural in/3; a forward-going window-label exclusion across all spec versions, §6.3/§6.4), the product-name grammar (charset, base-component exclusion, and the 1–192 character length cap), and the node invariant; - the §7 coverage contracts: the 4-slot null-padded box, the
encodingdiscriminator values, the bitmap bit convention, and the root-MOC range ordering (zstd level and other codec parameters stay non-normative); - the §8 rank-space deinterleave: bit parity (x = even bits, y = odd bits),
the south-corner orientation, and the healpy
pix2xyfequivalence; - the §9 latitude convention: the pinned WGS84 constants, the series
coefficients and their
<= 1e-13rad bound, the"authalic"/"geodetic-spherical"parameter vocabulary with authalic as the default, the store-attrlatitudekey and its two tokens ("authalic-wgs84"/"geodetic-spherical", §5/§9) — the wire-visible half that external readers key on — and the non-correspondence rule (never mix conventions in one dataset); - the §11 toc word: the bit layout (flag at bit 31, polarity 1 =
timestamp), the 2³¹/2³² start/end quanta, the 1850 epoch on the
leap-free GPS-aligned timescale with its UTC-boundary convention
(pre-1972 zero offset; per-step offsets frozen once a step is in the
table — leap-table appends are additive),
TOC_MAX_NSapplied to both encoders, the outward-rounding encode law (floored start, strictly-greater end ceiling), the decode semantics (exclusive envelope end), the valid-domain characterization and the garbage-in-garbage-out posture, the unsigned sort order and its tie-breaks, the merge law with its valid-domain scope and no-identity-element rule, the window-predicate conservatism directions, and the §11.8 conformance vectors (their UTC renderings scoped to the shipped leap table, per §11.8).
Extensions (new schedules, new spec versions, new encodings) are additive
under new discriminator values; existing stores never reparse under new
rules.
11. The packed 64-bit toc word
Contract. A toc index (temporal order coverage) is one unsigned 64-bit
word encoding either an exact nanosecond timestamp or a quantized,
conservative time range — the temporal sibling of the §1 morton word:
self-describing, sortable as a plain unsigned integer, and closed under a
semilattice merge. Source of truth in code: src_rust/src/toc.rs (the word
kernels this section specifies — encode, decode, merge, sort order, window
predicates) and mortie/_toc.py (the UTC/GPS timescale boundary). Decision
provenance: the issue #175 decision
ledger (the 1 ns base quantum
and 32/31
split;
the flag position, polarity, and
name)
and the design record on
englacial/zagg#410.
External stores cite this section as the word grammar behind their own
declarations — e.g. zagg's zagg-toc/1 attrs blocks store the grammar
revision token mortie-toc/1. This section is the class of citation such a
token is meant to resolve to; where a given store's documentation pointer
actually points is that store's own release-pinned choice, and zagg's swaps
here once this section ships (tracked on
issue #193). Informative
either way — the token vocabulary is the citing store's own. Two layers sit
above this grammar and are out of scope here: the set algebra over covers
of words (toc_normalize / toc_and, src_rust/src/toc/set_ops.rs) and
the typed Toc object (issue #198). Both operate on words this section
defines; neither changes one.
Naming note (informative): "toc" echoes tick/tock and T-MOC, but this is not an IVOA T-MOC and does not conform to the IVOA MOC 2.0 recommendation — different epoch, timescale, and cell model.
11.1 Timescale and epoch
Contract. Internal time is u64 nanoseconds since 1850-01-01T00:00:00 on a continuous, leap-free, GPS-aligned timescale: the scale ticks in SI seconds exactly with GPS time, and leap seconds exist only at the UTC conversion boundary, never inside the scale.
- GPS interop is a pure constant offset.
GPS_EPOCH_NS = 47,486 × 86,400 × 10⁹ = 4,102,790,400,000,000,000— the GPS epoch 1980-01-06T00:00:00 as internal ns (47,486 proleptic-Gregorian days past 1850-01-01).internal = gps_ns + GPS_EPOCH_NS, exactly. - The UTC boundary (
from_datetime64/to_datetime64inmortie/_toc.py): from 1972 on, the offset from naive UTC day-count time isGPS − UTC = TAI − UTC − 19seconds, from the static leap-second table inmortie/_toc.py— zero at the GPS epoch, +18 s from the 2017-01-01 step, which is the last step in the table (none further is scheduled). The table's authoritative external source is the IERS leap-second announcements — Bulletin C: every post-1972 row transcribes one announced step (TAI − UTCminus 19 s), so an implementation without Python in reach can reproduce the identical table from Bulletin C alone. Before 1972 the proleptic convention is zero offset (naive day-count seconds, no leap adjustment), pinning the epoch identity 1850-01-01T00:00:00 → 0 ns exactly. Cost, stated as shipped: the mapping steps back 9 s across the 1972-01-01 boundary, so the last 9 SI seconds of 1971 alias into early 1972; the UTC conversion is exact and invertible from 1972 on. Internal instants falling inside an inserted leap second render into the following UTC second (datetime64cannot express 23:59:60). - Leap-table appends are additive. When the IERS announces a step, the table gains a row; the conversion for every instant before a newly appended step is frozen and never moves, and internal-scale words never re-encode — the internal scale, not UTC, is what words persist in.
- Span ceiling.
TOC_MAX_NS = 2⁶³ − 2³² = 9,223,372,032,559,808,512is the exclusive ceiling on internal times — 2³² ns (~4.3 s) below the 2⁶³ ns mark; the last valid instant renders, under the leap table as of the 2017-01-01 step, as UTC 2142-04-11T23:46:54.559808511 (a derived rendering, per the additive rule above; the internal ns ceiling itself is fixed): the range end codee = (t ≫ 32) + 1must fit 31 bits. The ceiling is applied to both encoders so that every encodable word is mergeable — a timestamp in the last 2³² ns would encode fine but its merge envelope would overflow the end field, so it is rejected up front rather than wrapping silently.
11.2 Bit layout (MSB → LSB)
[ start: 32 bits, 2^31 ns units ][ flag: 1 bit ][ low: 31 bits ]
63 .. 32 31 30 .. 0
- flag (bit 31) — the variant discriminator: 1 = timestamp, 0 = range.
- flag = 1 (timestamp) — the word is the instant
t_nswith the flag bit spliced in at position 31: bits 63–32 holdt_ns ≫ 31, bits 30–0 holdt_ns & (2³¹ − 1). The splice is monotone: unsigned word order over timestamps is exactly ns order. - flag = 0 (range) — bits 63–32 hold the start code
s(units ofQ_START_NS = 2³¹ ns, ~2.15 s, floored), bits 30–0 hold the end codee(units ofQ_END_NS = 2³² ns, ~4.29 s, ceiled). The encoded envelope is the half-open interval[s · 2³¹, e · 2³²)ns. - Unsigned storage: the word is stored and exchanged as
uint64. Reinterpreting it asint64is an error: any timestamp at or past internal ns2⁶²(and any range with start code ≥ 2³¹) sets bit 63 and would read back negative.
11.3 Encoding
Contract. Two encoders, total over their stated domains and erroring outside them (never wrapping):
- Timestamp (
time2toc): domain0 ≤ t_ns < TOC_MAX_NS. The word is the §11.2 splice oft_ns. - Range (
span2toc): the input is a real closed interval[start_ns, end_ns]withstart_ns ≤ end_ns < TOC_MAX_NS. The codes are
text
s = start_ns >> 31 (floor onto the 2^31 ns grid)
e = (end_ns >> 32) + 1 (strictly-greater ceiling onto the 2^32 ns grid)
The end ceiling is strictly greater uniformly — including when
end_ns sits exactly on the 2³² ns grid — so the half-open envelope
[s · 2³¹, e · 2³²) always properly contains end_ns.
The conservative direction is law: encoding only ever widens — the envelope contains the real interval, never the reverse, and a real interval is never narrowed. Every derived operation below (merge, window predicates) preserves this direction.
The grammar also provides an instant an exact form (time2toc), so no
instant need be widened into a range. Taking that form is writer
discipline, not an encoder property: span2toc(t, t) is in domain and
returns a range word (span2toc(0, 0) is the range word 1), and nothing
here rejects it. A citing store that wants decoded (t, t) to mean
"instant" states the obligation itself — zagg's zagg-toc/1 §8.1 makes it
a MUST.
The all-zero word is unreachable. No encoder output is 0: the epoch
instant encodes as 0x8000_0000 (the flag bit sits at position 31, not at
the bottom of the word), and every range word has end code e ≥ 1, so the
smallest range word is 1. External conventions may therefore reserve 0
as a fill/absence sentinel (zagg's zagg-toc/1 §8.2 does); mortie itself
assigns 0 no meaning — under §11.4 it is simply out of domain.
Reserving 0 is free of encoder collisions, not of predicate hits. 0
is unreachable by both encoders, so a reserved sentinel can never be
confused with a written word; but the operations of §11.7 are total
(§11.4), and 0 decodes as the empty range envelope [0, 0). That
envelope intersects nothing, so toc_overlaps(0, ·, ·) is false for every
window — the property zagg's §8.2 argues over. It is, however, vacuously
inside any window anchored at the epoch, so toc_contains(0, 0, q_end) is
true: a store that reserves 0 must mask its fills before a
containment query rather than rely on the sentinel selecting nothing.
11.4 Decoding, validity, and the garbage posture
Contract. Decoding (toc2time) is variant-dispatched on the flag bit:
- a timestamp yields its exact instant twice:
(t, t); - a range yields its half-open envelope bounds
(s · 2³¹, e · 2³²)— the end bound is exclusive, strictly greater than every instant the range covers.
Valid domain. A word is valid exactly when it is encoder-reachable:
- a timestamp word is valid iff its decoded instant
t < TOC_MAX_NS— equivalently, iff its high fieldword ≫ 32 ≤ 2³² − 3; - a range word is valid iff its envelope is nonempty:
s · 2³¹ < e · 2³², equivalentlys ≤ 2e − 1(which forcese ≥ 1).
Every encoder output satisfies these bounds, and every word satisfying them is producible by the corresponding encoder — the characterization is exact.
Garbage in, garbage out. Decoding and every derived operation are total: an out-of-domain bit pattern decodes, merges, sorts, and windows without complaint, and no guarantee of this section survives it. The operations remain deterministic on junk (no panic, no wrap error), but their results carry no semantics — see the §11.6 merge scoping for the one place this is load-bearing.
11.5 Ordering and equality
Contract. Unsigned u64 order over toc words is order by
conservative encoded start — both variants place their start information
in the high 32 bits (word ≫ 32 is t ≫ 31 for a timestamp and s for a
range), so a raw unsigned sort needs no comparator and no decode. Within a
tied start quantum the tie-breaks are, in order:
- ranges (flag 0) sort before timestamps (flag 1) — correct, since a range's floored start is ≤ any timestamp inside that quantum;
- among ranges: shorter first, by end code;
- among timestamps: exact ns order.
Equality is bit equality. The encoding is canonical — one word per
(variant, field values) — so integer equality, hashing, and dedup work on
the word directly. The timestamp encoding is injective in t_ns; the
range encoding is injective in the envelope, not the real interval:
distinct real intervals quantizing to the same codes share one word, and
word equality asserts envelope equality only.
11.6 The merge law
Contract. The merge (toc_merge) is the semilattice join of two
words:
- bitwise-equal inputs return that word unchanged — required for idempotence, because merging two equal timestamps must NOT produce their range envelope;
- any other pair takes each word's conservative codes — a range's
(s, e)verbatim; a timestamp's merge envelope iss = t ≫ 31(its high field) ande = (t ≫ 32) + 1— and emits the range word(min(s_a, s_b) ≪ 32) | max(e_a, e_b). Two unequal valid words always merge to a range word.
The join is closed on the valid domain. For valid inputs the merged
codes again satisfy §11.4's range condition — min s ≤ s_a ≤ 2e_a − 1 ≤
2·max e − 1, with max e ≥ 1 — so the merged word is itself a valid word,
and in particular is never 0 (§11.3). Closure is what lets a reduction
re-merge its own intermediate results inside the stated scope below.
On the valid domain (§11.4) the join is exactly associative, commutative,
and idempotent over the fixed epoch-anchored lattice, so a reduction over
any multiset of valid words yields a bit-identical u64 under any fold
tree — parallel, segmented, or sequential. The merged envelope contains
every input instant and every input envelope (conservatism direction
preserved, never narrowed). The join has no identity element: a
reduction over zero words is an error, never a sentinel — toc_reduce
refuses an empty array, and tocs_reduce refuses an empty group. An
empty batch (no words and no groups) asks for no reduction at all and
is accepted, returning no words.
Scope of the law (the [PR
192](https://github.com/espg/mortie/pull/192) finding, stated
normatively): the fold-tree bit-identity guarantee covers
encoder-produced (valid) words only. Out-of-domain patterns are
garbage in, garbage out — an invalid "timestamp" past TOC_MAX_NS can
even merge to a word with the timestamp flag set, and past that point two
different fold trees may disagree, each deterministically. Implementations
are not required to detect junk; they are required not to panic on it.
11.7 Window predicates
Contract. Both predicates test a word's conservative encoded
bounds against a half-open query window [q_start, q_end) in internal
ns; a timestamp is treated as the one-ns envelope [t, t + 1). An
inverted window is an error; an empty window (q_start == q_end)
overlaps and contains nothing.
The edge slack is asymmetric, because §11.3 rounds the two ends onto
different grids: a range word's envelope starts at most Q_START_NS − 1 =
2³¹ − 1 ns (~2.15 s) before the real interval's start (floored start;
zero when that start sits on the 2³¹ ns grid) and ends between 1 ns and
Q_END_NS = 2³² ns (~4.29 s) past the interval's last covered instant
(strictly-greater ceiling; the full 2³² ns when the end sits on the 2³² ns
grid). Timestamp words carry no slack. So the edge misreports below reach
< 2³¹ ns on the leading side and ≤ 2³² ns on the trailing side — a
reader budgeting "one quantum" symmetrically under-budgets the trailing
edge by 2×.
- overlaps (
toc_overlaps): true iff the envelope intersects the window. It may over-report near window edges — a range whose envelope grazes the window without its real interval doing so — and only within the slack: a spurious hit requires real content ending later thanq_start − 2³²ns (trailing slack reaching back into the window) or starting earlier thanq_end + 2³¹ns (leading slack reaching forward). It never under-reports: every word whose real time content intersects the window tests true. - contains (
toc_contains): true iff the envelope fits inside the window. It never over-reports (the real interval lies inside the envelope, so envelope-in-window implies interval-in-window) and may under-report by the same slack: a word whose real interval starts less than 2³¹ ns afterq_start, or ends less than 2³² ns beforeq_end, can test false though its real content fits.
The two conservatism directions are law, and are what external readers key selection semantics on (zagg §8.1's "conservative superset" clause cites them).
11.8 Conformance vectors
Contract (golden, test-pinned). The words below are normative byte
values: an independent implementation of this grammar must reproduce every
row exactly. Inputs are internal ns (§11.1); start / end are the
toc2time decode (end exclusive for ranges, per §11.4); the UTC column
renders the decoded start through the §11.1 boundary convention
(to_datetime64). The two merge rows join the rows named in their inputs.
The table is regenerated from the live kernels by
mortie/tests/test_spec_toc.py and compared literally, so the page and the
code cannot drift apart; the §11.6 algebraic laws are pinned at volume by
the fixture tests in src_rust/src/toc.rs.
What is frozen, exactly. The word and ns columns and the decode
they express are the normative, immutable part. The start (UTC) column is
derived: it renders the decoded start through the §11.1 boundary
convention under the leap table as of the 2017-01-01 step, the table
mortie ships. §11.1's additive rule governs the column: when the table
gains a row (the IERS announces each step roughly six months ahead via
Bulletin C, and a future step may be positive or negative), renderings of
instants after that step move (of the rows below, only the 2142 one is
after any future step, and the §11.1 sentence rendering that same instant
is scoped identically), while every word, ns, and decode value stays
byte-for-byte fixed. The regeneration test is what surfaces such an append:
it fails, and the UTC cell is deliberately re-rendered.
| value | inputs | word (hex) | word (decimal) | start (ns) | end (ns) | start (UTC) |
|---|---|---|---|---|---|---|
| timestamp: epoch | time2toc(0) |
0x0000000080000000 |
2147483648 | 0 | 0 | 1850-01-01T00:00:00.000000000 |
| timestamp: GPS epoch | time2toc(4102790400000000000) |
0x71E014439BE20000 |
8205580801679687680 | 4102790400000000000 | 4102790400000000000 | 1980-01-06T00:00:00.000000000 |
| timestamp: 2018-01-01 UTC | time2toc(5301590418000000000) |
0x932610CAE9813400 |
10603180836377408512 | 5301590418000000000 | 5301590418000000000 | 2018-01-01T00:00:00.000000000 |
| timestamp: last valid instant | time2toc(9223372032559808511) |
0xFFFFFFFDFFFFFFFF |
18446744065119617023 | 9223372032559808511 | 9223372032559808511 | 2142-04-11T23:46:54.559808511 |
| range: straddles the 2^32 grid | span2toc(12384901888, 13384901888) |
0x0000000500000004 |
21474836484 | 10737418240 | 17179869184 | 1850-01-01T00:00:10.737418240 |
| range: end exactly on the 2^32 grid | span2toc(29064771072, 30064771072) |
0x0000000D00000008 |
55834574856 | 27917287424 | 34359738368 | 1850-01-01T00:00:27.917287424 |
| merge: the two ranges above | toc_merge(21474836484, 55834574856) |
0x0000000500000008 |
21474836488 | 10737418240 | 34359738368 | 1850-01-01T00:00:10.737418240 |
| merge: the two epoch timestamps above | toc_merge(2147483648, 8205580801679687680) |
0x0000000038F00A22 |
955255330 | 0 | 4102790401679687680 | 1850-01-01T00:00:00.000000000 |
Reading the rows against the layout (§11.2): the epoch instant is the flag
bit alone; the 2018 timestamp's +18 s carries the §11.1 leap offset; the
on-grid range's end code is 8, never 7 — the strictly-greater ceiling
(§11.3); the range⊔range merge takes min of start codes and max of end
codes; and the timestamp⊔timestamp merge is a range word (flag 0) whose
envelope contains both instants, while §11.6's equal-input rule keeps a
timestamp merged with itself a timestamp.