Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d22268a
phase 1 of issue #198
espg Aug 18, 2026
147dc78
fold review: cluster the randomized word generator so merges occur (i…
espg Aug 18, 2026
c0e9b88
fold review: assert sortedness by comparison, not uint64 diff (issue …
espg Aug 18, 2026
3bfce7a
fold review: scope the canonical-form laws to encoder-produced words …
espg Aug 18, 2026
58f9f08
fold review: state the ascending-starts invariant on the absorption w…
espg Aug 18, 2026
f69b49f
fold review: pin merged bounds against the input envelopes (issue #198)
espg Aug 18, 2026
d2545be
fold review: add the top-of-span golden for the full end field (issue…
espg Aug 18, 2026
ad18705
phase 2 of issue #198
espg Aug 18, 2026
df9db84
fold review: pin the junk intersection's canonicality, not determinis…
espg Aug 18, 2026
d30e6a4
fold review: guard the randomized and-laws against a vacuous generato…
espg Aug 18, 2026
0174dbd
fold review: pin a stamp surviving on an output piece's end (issue #198)
espg Aug 18, 2026
8eff3b8
fold review: add the top-of-span golden to the and path (issue #198)
espg Aug 18, 2026
ca2bc20
fold review: build only the stamp half each call site uses (issue #198)
espg Aug 18, 2026
e7ce41e
Merge remote-tracking branch 'origin/main' into claude/198-toc-object
espg Aug 19, 2026
f3b209d
phase 3 of issue #198
espg Aug 19, 2026
7cebfb6
fold review: canonicalize protocol operands in _words (issue #198)
espg Aug 19, 2026
ac6faf1
fold review: ceil the repr's end bound outward (issue #198)
espg Aug 19, 2026
92b1673
fold review: gate the words branch on 1-D shape (issue #198)
espg Aug 19, 2026
61d68d6
fold review: pin the array_equal operand structurally (issue #198)
espg Aug 19, 2026
2a3a12c
fold review: disambiguate the window kernels of the same name (issue …
espg Aug 19, 2026
7ce585b
fold review: accept every spelling of the empty cover (issue #198)
espg Aug 19, 2026
a993ec7
phase 4 of issue #198
espg Aug 19, 2026
bea27cb
fold review: repoint the Rust comments at the renamed module (issue #…
espg Aug 19, 2026
07000e9
fold review: pin the shim roster to the 0.9.9 released surface (issue…
espg Aug 19, 2026
420f746
fold review: announce the toc set-algebra kernels in the changelog (i…
espg Aug 19, 2026
8c51792
fold review: complete the notebook's flat-surface list (issue #198)
espg Aug 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,70 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- **BREAKING: `mortie.toc` is the `Toc` constructor, not a submodule** (issue
#198). `mortie/toc.py` is now `mortie/_toc.py`, which frees the `mortie.toc`
name for a callable — the same move issue #196 made for `mortie.moc`.
**Statement-form `import mortie.toc` and `from mortie.toc import x` break at
this rename** — the module does not exist any more, and no import-system
shim is possible because a callable cannot also be a module. The flat
package names are unchanged and are the supported spelling:
`mortie.time2toc`, `mortie.span2toc`, `mortie.toc2time`, `mortie.toc_merge`,
`mortie.toc_reduce`, `mortie.tocs_reduce`, `mortie.toc_is_range`,
`mortie.toc_overlaps`, `mortie.toc_contains`, `mortie.from_datetime64`,
`mortie.to_datetime64`, `mortie.from_gps_ns`, `mortie.to_gps_ns` — the four
grid/epoch constants, which previously lived only on the submodule, are now
flat too: `mortie.Q_START_NS`, `mortie.Q_END_NS`, `mortie.TOC_MAX_NS`,
`mortie.GPS_EPOCH_NS` — and `mortie.toc_normalize` / `mortie.toc_and` are
**new in this release, flat from the start**: they never had a submodule
spelling, so they are not in the shim's roster and
`mortie.toc.toc_and` was never reachable. Attribute access to the old names
(`mortie.toc.toc_merge`, `mortie.toc.Q_START_NS`) still resolves through a
migration shim for **one minor version**, emitting a `DeprecationWarning`
on each access (deduplication is left to the standard warnings filters);
the attributes then drop.

- **`Toc`: a time-first temporal coverage object** (issue #198).
`toc("2020-01-01", "2021-06-01")` builds a temporal coverage from ISO
strings or `datetime64` instants and pairs (via `time2toc` / `span2toc`,
broadcasting), a `uint64` toc word array, or anything exposing the new
`__toc_words__()` interchange dunder. The canonical form is a word **set**
— `toc_normalize`'s sorted maximal merges, kept eagerly and stored
read-only, so `==` and `hash()` are well defined and gappy coverage keeps
its gaps (the constructor docstring states the one-way lossy-toward-
coverage act: subsumed instants are absorbed and not recoverable from the
cover). Methods are `overlaps`, `contains`, and `intersection` / `&` —
**every public method a single delegation to `toc_and`**, the one set
operation the issue #177 call-site audit ruled in, pinned mechanically by
the same delegation test machinery as `Moc` (now shared in
`mortie/tests/delegation.py`); union is construction
(`Toc(np.append(a.words, b.words))`) and difference/xor deliberately do
not ship. The predicates are documented as envelope algebra with a
conservative-direction table; `repr` prints the span/instant counts, the
outward-rounded UTC extent, and the covered duration. See
[docs/api/toc_object.md](docs/api/toc_object.md).

- **Toc set algebra: `toc_normalize` and `toc_and`** (issues #177 / #198).
The two entries the issue #177 call-site audit ruled in, both new public
flat names. `toc_normalize(words)` is the **canonical cover form**: the
sorted word set with the same decoded coverage as the input — ranges
coalesce iff their decoded half-open envelopes overlap or abut exactly (a
surviving gap is never bridged, however small, because outward rounding
only shrinks apparent gaps), a timestamp a range subsumes is absorbed, and
a free instant survives bit-identical. `toc_and(a, b)` is the **one set
operation** over that form: both operands canonicalized, then a sorted
sweep emitting `[max(starts), min(ends))`, with a timestamp surviving iff
genuinely covered on both sides. Conservative directions: normalize is
coverage-identical with no rounding arm anywhere (merged bounds are
min/max of on-grid values); intersection is **exact by grid closure** —
the max of two starts stays on the 2^31 ns start grid and the min of two
ends on the 2^32 ns end grid — and never under-covers the true
intersection, over-covering only by the operands' own inherited quantum.
Union needs no operator (concatenate, then `toc_normalize`); **difference
and xor deliberately do not ship**, because conservative covers
*under*-cover on subtraction and no audited call site exists. Both release
the GIL and carry `toc_merge`'s scope — an arbitrary bit pattern is
garbage in, garbage out, deterministically.

- **BREAKING: `mortie.moc` is the `Moc` constructor, not a submodule** (issue
#196). `mortie/moc.py` is now `mortie/_moc.py`, which frees the `mortie.moc`
name for a callable. **Statement-form `import mortie.moc` and
Expand Down
29 changes: 24 additions & 5 deletions docs/api/toc.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# mortie.toc
# mortie toc kernel

The toc word — temporal order coverage (issue #175): one `uint64` packing
either an exact nanosecond timestamp or a conservative time range, sortable
Expand All @@ -9,25 +9,44 @@ IVOA T-MOC. These flat-array elementwise ops are the type's scalar surface
(the same relationship [the MOC kernel](moc.md) has to its ops over one
cover), plus one ragged operator — `tocs_reduce`, the segmented sibling of
`toc_reduce` (issue #177), kept here because it folds the word type itself
rather than operating over covers. The many-*cover* plurals still land in
[mortie.batch](batch.md), and wait on the interval-set algebra, which stays
deferred for want of a consumer. The names stay flat on the package
rather than operating over covers. `toc_normalize` and `toc_and` are the
set-algebra entries the issue #177 call-site audit ruled in: the canonical
cover form and the one set operation over it. The many-*cover* plurals still
land in [mortie.batch](batch.md). The names stay flat on the package
(`mortie.time2toc`, ...).

These are the **kernel layer**: words in, words out, no wrapping cost, and
nothing here is deprecated. The **object layer** over them is
[mortie.Toc](toc_object.md), where every public method is a single delegation
to a function on this page.

!!! warning "`mortie.toc` is no longer a module (issue #198)"

The implementation moved to `mortie/_toc.py` so that `mortie.toc` could
become the `Toc` constructor — the same move issue #196 made for
`mortie.moc`. `import mortie.toc` and `from mortie.toc import …`
**break**; the flat package names (`mortie.time2toc`,
`mortie.toc_merge`, …, and now `mortie.Q_START_NS`, `mortie.Q_END_NS`,
`mortie.TOC_MAX_NS`, `mortie.GPS_EPOCH_NS`) are unchanged and are the
supported spelling. `mortie.toc.toc_merge`-style attribute access still
resolves for one minor version, with a `DeprecationWarning`.

Worked example:
[examples/toc_temporal_coverage.ipynb](https://github.com/espg/mortie/blob/HEAD/examples/toc_temporal_coverage.ipynb)
walks the type end-to-end on synthetic data — encoding, the conservative
merge, sorting without a comparator, the window predicates at a quantum
boundary, and the UTC/GPS round-trip
([run it on Binder](https://mybinder.org/v2/gh/espg/mortie/HEAD?labpath=examples%2Ftoc_temporal_coverage.ipynb)).

::: mortie.toc
::: mortie._toc
options:
members:
- time2toc
- span2toc
- toc2time
- toc_merge
- toc_normalize
- toc_and
- toc_reduce
- tocs_reduce
- toc_is_range
Expand Down
56 changes: 56 additions & 0 deletions docs/api/toc_object.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# mortie.Toc — the temporal coverage object

`mortie.toc(...)` builds a `Toc`: a temporal coverage as an object, so that
gappy time coverage reads as time.

```python
from mortie import toc

when = toc("2020-01-01", "2021-06-01")
assert store_toc.overlaps(when)
sliver = store_toc & when # the canonical cover of the overlap
```

## The two-layer rule

mortie's temporal-coverage surface is two layers and stays that way, the same
split [the spatial object](moc_object.md) documents:

- **The kernel functions are the array/batch layer.** The free `toc_*`
functions on [the toc kernel page](toc.md) are words in, words out,
unchanged and un-deprecated, and the segmented `tocs_reduce` stays
function-shaped permanently. Array-first consumers keep calling these
directly, at zero wrapping cost.
- **The object is ergonomics.** `Toc` is a thin view over the canonical
`uint64` word set — `toc_normalize`'s sorted maximal merges — never a new
representation: **every public method is a single delegation to a kernel
function** (all three delegate to `toc_and`, the one set operation the
issue #177 call-site audit ruled in). The array stays the interchange
format — `Toc.__toc_words__()` hands the canonical words back, and any
object exposing that dunder is accepted wherever a `Toc` is.

## The canonical form is a word set

A store observed in campaigns has *gappy* coverage: one merged envelope
papers over the gaps exactly where they are most informative, so the
canonical form keeps k disjoint spans (plus free instants, bit-identical).
Normalization is **lossy toward coverage, one way**: a timestamp subsumed by
a range's decoded span is absorbed at construction, and a cover can be
rebuilt from the sibling word arrays it came from — never the arrays from a
cover. Union needs no method (construction normalizes, so
`Toc(np.append(a.words, b.words))` is the union), and the difference /
symmetric-difference directions deliberately do not ship: conservative
covers under-cover on subtraction, and no audited call site exists.

Two naming notes. `Toc.overlaps` / `Toc.contains` compare two whole covers
and answer once; the un-deprecated kernel predicates `toc_overlaps` /
`toc_contains` of the same names take a `[q_start_ns, q_end_ns)` query
window and answer elementwise, per word — a different question. And the
predicates are *envelope* algebra, not data algebra: the
conservative-direction table in the module docstring below says which way
each answer can err near a span edge (the quanta are ~2–4 s).

::: mortie.toc_object
options:
members:
- Toc
42 changes: 24 additions & 18 deletions examples/toc_temporal_coverage.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"import mortie\n",
"from mortie import toc\n",
"\n",
"mortie.__version__\n"
]
Expand Down Expand Up @@ -124,10 +123,10 @@
}
],
"source": [
"print(f'Q_START_NS = {toc.Q_START_NS:>21,} ns ({toc.Q_START_NS / 1e9:.3f} s)')\n",
"print(f'Q_END_NS = {toc.Q_END_NS:>21,} ns ({toc.Q_END_NS / 1e9:.3f} s)')\n",
"print(f'TOC_MAX_NS = {toc.TOC_MAX_NS:>21,} ns (ceiling '\n",
" f'{mortie.to_datetime64(toc.TOC_MAX_NS - 1)})')\n",
"print(f'Q_START_NS = {mortie.Q_START_NS:>21,} ns ({mortie.Q_START_NS / 1e9:.3f} s)')\n",
"print(f'Q_END_NS = {mortie.Q_END_NS:>21,} ns ({mortie.Q_END_NS / 1e9:.3f} s)')\n",
"print(f'TOC_MAX_NS = {mortie.TOC_MAX_NS:>21,} ns (ceiling '\n",
" f'{mortie.to_datetime64(mortie.TOC_MAX_NS - 1)})')\n",
"\n",
"# The epoch identity: 1850-01-01T00:00:00 UTC is internal ns 0, exactly.\n",
"assert mortie.from_datetime64('1850-01-01T00:00:00') == 0\n"
Expand Down Expand Up @@ -341,9 +340,9 @@
"print(f'real [{seg_start:,} .. {seg_end:,}]')\n",
"print(f'envelope [{env_start:,} .. {env_end:,})')\n",
"print(f'slack start {(seg_start - env_start) / 1e9:.3f} s '\n",
" f'(< {toc.Q_START_NS / 1e9:.3f} s) '\n",
" f'(< {mortie.Q_START_NS / 1e9:.3f} s) '\n",
" f'end {(env_end - seg_end) / 1e9:.3f} s '\n",
" f'(<= {toc.Q_END_NS / 1e9:.3f} s)')\n",
" f'(<= {mortie.Q_END_NS / 1e9:.3f} s)')\n",
"\n",
"assert env_start <= seg_start and env_end > seg_end\n"
]
Expand Down Expand Up @@ -690,8 +689,8 @@
}
],
"source": [
"qs = (base // toc.Q_START_NS) * toc.Q_START_NS # 2^31-aligned\n",
"grid = ((qs + 3600 * 10**9) // toc.Q_END_NS) * toc.Q_END_NS # a 2^32 grid line\n",
"qs = (base // mortie.Q_START_NS) * mortie.Q_START_NS # 2^31-aligned\n",
"grid = ((qs + 3600 * 10**9) // mortie.Q_END_NS) * mortie.Q_END_NS # a 2^32 grid line\n",
"qe = grid + 1 # 1 ns past it\n",
"\n",
"# (1) real interval inside the window, envelope end spills past qe\n",
Expand All @@ -709,7 +708,7 @@
"assert mortie.toc_overlaps(a, qs, qe) and not mortie.toc_contains(a, qs, qe)\n",
"assert mortie.toc_overlaps(b, qs, qe)\n",
"print(f'\\nboth errors are bounded by one quantum: '\n",
" f'{toc.Q_END_NS / 1e9:.3f} s')\n"
" f'{mortie.Q_END_NS / 1e9:.3f} s')\n"
]
},
{
Expand Down Expand Up @@ -856,11 +855,16 @@
"## Notes\n",
"\n",
"- **The API is flat on the package.** `mortie.time2toc`, `span2toc`,\n",
" `toc2time`, `toc_merge`, `toc_reduce`, `toc_is_range`, `toc_overlaps`,\n",
" `toc_contains`, `from_datetime64`, `to_datetime64`, `from_gps_ns`,\n",
" `to_gps_ns`; the constants live on the module (`mortie.toc.Q_START_NS`,\n",
" `Q_END_NS`, `TOC_MAX_NS`, `GPS_EPOCH_NS`). Rendered reference:\n",
" `toc2time`, `toc_merge`, `toc_reduce`, `tocs_reduce`, `toc_is_range`,\n",
" `toc_overlaps`, `toc_contains`, `toc_normalize`, `toc_and`,\n",
" `from_datetime64`, `to_datetime64`, `from_gps_ns`, `to_gps_ns`; the\n",
" constants live flat on the package (`mortie.Q_START_NS`, `Q_END_NS`,\n",
" `TOC_MAX_NS`, `GPS_EPOCH_NS`). Rendered reference:\n",
" [docs/api/toc.md](../docs/api/toc.md).\n",
"- **`mortie.toc` is the `Toc` constructor now, not a module**\n",
" ([#198](https://github.com/espg/mortie/issues/198)): the kernel moved to\n",
" `mortie/_toc.py`, and `mortie.toc(...)` builds the object layer over these\n",
" words — see [docs/api/toc_object.md](../docs/api/toc_object.md).\n",
"- **Range ends are exclusive.** `toc2time` returns a half-open envelope; a\n",
" timestamp returns `(t, t)`, the one closed case.\n",
"- **Ceiling.** Both encoders reject times at or past `TOC_MAX_NS`\n",
Expand All @@ -871,10 +875,12 @@
" seconds), which is what pins the epoch identity\n",
" `from_datetime64('1850-01-01') == 0`; conversion is exact and invertible\n",
" from 1972 on. See the `from_datetime64` docstring.\n",
"- **Not an IVOA T-MOC**, and the interval-set algebra over collections of\n",
" words (unions of disjoint ranges, ragged per-cell temporal covers) is\n",
" deferred to [#177](https://github.com/espg/mortie/issues/177) — this\n",
" notebook is the flat-array elementwise surface only.\n"
"- **Not an IVOA T-MOC.** Of the interval-set algebra over collections of\n",
" words ([#177](https://github.com/espg/mortie/issues/177)), the entries the\n",
" call-site audit ruled in now ship as `toc_normalize` (the canonical cover\n",
" form) and `toc_and` (the one set operation); difference and xor\n",
" deliberately do not — conservative covers under-cover on subtraction.\n",
" This notebook is the flat-array elementwise surface only.\n"
]
}
],
Expand Down
3 changes: 2 additions & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ nav:
- coverage: api/coverage.md
- moc kernel: api/moc.md
- Moc object: api/moc_object.md
- toc: api/toc.md
- toc kernel: api/toc.md
- Toc object: api/toc_object.md
- prefix_trie: api/prefix_trie.md
- geometry: api/geometry.md
- batch: api/batch.md
Expand Down
57 changes: 42 additions & 15 deletions mortie/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,32 @@
split_base_cells,
)

# toc word -- temporal order coverage (issue #175; the module is
# mortie/_toc.py since issue #198 freed the `toc` name for the Toc
# constructor, but the names stay flat on the package either way -- the four
# grid/epoch constants included, now that the submodule spelling is gone).
from ._toc import (
GPS_EPOCH_NS,
Q_END_NS,
Q_START_NS,
TOC_MAX_NS,
from_datetime64,
from_gps_ns,
span2toc,
time2toc,
to_datetime64,
to_gps_ns,
toc2time,
toc_and,
toc_contains,
toc_is_range,
toc_merge,
toc_normalize,
toc_overlaps,
toc_reduce,
tocs_reduce,
)

# Bulk (plural) twins of the scalar operators, consolidated by arity out of
# coverage / geometry / moc / orders (issue #170). The flat package names below
# are unchanged -- only the submodule they live in moved.
Expand Down Expand Up @@ -120,21 +146,14 @@
xy_to_rank,
)

# toc word -- temporal order coverage (issue #175)
from .toc import (
from_datetime64,
from_gps_ns,
span2toc,
time2toc,
to_datetime64,
to_gps_ns,
toc2time,
toc_contains,
toc_is_range,
toc_merge,
toc_overlaps,
toc_reduce,
tocs_reduce,
# The temporal object layer over the toc kernel above (issue #198), sibling of
# `Moc`: `Toc` wraps the canonical normalized word set, and `toc` is a
# callable namespace rather than a submodule -- `toc("2020-01-01", ...)`
# builds a `Toc`, and `toc.toc_merge`-style attribute access is the
# deprecation shim for the `mortie/toc.py` -> `mortie/_toc.py` rename.
from .toc_object import (
Toc,
toc,
)

__all__ = [
Expand Down Expand Up @@ -186,6 +205,8 @@
'split_base_cells',
'Moc',
'moc',
'Toc',
'toc',
'linestring_coverage',
'from_wkb',
'from_wkbs',
Expand All @@ -207,6 +228,8 @@
'span2toc',
'toc2time',
'toc_merge',
'toc_normalize',
'toc_and',
'toc_reduce',
'toc_is_range',
'toc_overlaps',
Expand All @@ -216,6 +239,10 @@
'to_datetime64',
'from_gps_ns',
'to_gps_ns',
'Q_START_NS',
'Q_END_NS',
'TOC_MAX_NS',
'GPS_EPOCH_NS',
]

# morton_index datatype (phase 5) + Arrow interop (phase 4) for issue #35. The
Expand Down
Loading
Loading