Save and replay
A saved Study is one JSON file. Its bytes are exactly the canonical form of the record, so the
SHA-256 of the file is stable, and the record’s own study_digest can be recomputed from it
at any time.
study.save(path) writes the file and returns its path. For the Vikram minute-envelope run the
file is byte for byte the study-minute.json committed with the run of 14 October 2026.
>>> import hashlib>>> import lunartrace as lt>>> study = lt.temporal.study(site="vikram", event="2023-08-23T12:33Z/12:35Z",... products="LRO/LROC/CDRNAC4", archive="vikram-2026-10-14")>>> path = study.save("vikram.study.json")>>> hashlib.sha256(path.read_bytes()).hexdigest()[:16]'a97a8044d2b52841'Verify
Section titled “Verify”load reads a saved file and checks it. verify() recomputes the digest from the record and
returns True, or raises IntegrityError. Change one digit of a value and the check fails:
>>> import lunartrace as lt>>> study = lt.temporal.study(site="vikram", event="vikram:minute-envelope",... products="LRO/LROC/CDRNAC4", archive="vikram-2026-10-14")>>> path = study.save("vikram.study.json")>>> lt.temporal.load("vikram.study.json").verify()True>>> data = path.read_bytes().replace(b'"pre":30', b'"pre":31', 1)>>> _ = path.with_name("edited.study.json").write_bytes(data)>>> lt.temporal.load("edited.study.json")Traceback (most recent call last): ...lunartrace.temporal.IntegrityError: study_digest sha256:3d75a662ce75f00cd34dd9ba288012ad471c73b1e56ecf0abe36896d0b199dd0 does not match the record (sha256:...)IntegrityError is a ValueError. The digest covers the whole record except the digest
itself. It shows that the record is the one that was compiled. It does not show that the source
is genuine; see the limits.
Replay
Section titled “Replay”Replay recompiles the Study from the retained pages and gives you a new Study to compare. It
needs the pages, so save with sources=True. That also writes a <stem>.sources/ directory
beside the file, with the fetch receipt and every raw ODE response:
>>> import lunartrace as lt>>> study = lt.temporal.study(site="vikram", event="vikram:minute-envelope",... products="LRO/LROC/CDRNAC4", archive="vikram-2026-10-14")>>> path = study.save("vikram.study.json", sources=True)>>> import pathlib>>> sorted(p.name for p in pathlib.Path("vikram.study.sources").iterdir())['fetch-receipt.json', 'raw']>>> sorted(p.name for p in pathlib.Path("vikram.study.sources/raw").iterdir())['count-after.json', 'count.json', 'page-000.json', 'page-001.json']>>> loaded = lt.temporal.load("vikram.study.json")>>> again = loaded.replay()>>> again.digest == loaded.digest == study.digestTrueload finds <stem>.sources/ on its own. Pass sources= to point at another directory.
A file saved without sources cannot replay, and replay() says what is missing:
>>> import lunartrace as lt>>> study = lt.temporal.study(site="vikram", event="vikram:minute-envelope",... products="LRO/LROC/CDRNAC4", archive="vikram-2026-10-14")>>> _ = study.save("bare.study.json")>>> lt.temporal.load("bare.study.json").replay()Traceback (most recent call last): ...FileNotFoundError: no retained sources for this Study; save it with sources=True, or load it with a <stem>.sources/ directory beside the fileBefore it compiles, replay checks every page against the SHA-256 in the receipt. A page that
changed on disk raises ValueError naming the file.
What is in the file
Section titled “What is in the file”study.anatomy() lays the record out as seven sections, in the order a reader asks the
questions. It adds nothing to the record; each section is a view over keys the record already
has.
>>> import lunartrace as lt>>> study = lt.temporal.study(site="vikram", event="vikram:minute-envelope",... products="LRO/LROC/CDRNAC4", archive="vikram-2026-10-14")>>> list(study.anatomy())['what_ran', 'from_which_bytes', 'around_when', 'on_what', 'why_these', 'where_it_stops', 'has_anything_moved']>>> study.anatomy()["what_ran"]["method"]'lunartrace.temporal_prepost/v1'>>> study.anatomy()["has_anything_moved"] == study.digestTrueA live Study
Section titled “A live Study”A study fetched live keeps its pages in memory. save(..., sources=True) writes them, and from
then on it replays like any other:
import lunartrace as lt
live = lt.temporal.study(site=(-69.3741, 32.32), # no archive: fetches Lunar ODE now event="2023-08-23T12:33Z/12:35Z", products="LRO/LROC/CDRNAC4")live.save("live.study.json", sources=True)lt.temporal.load("live.study.json").replay().digest == live.digest # TrueWhat replay shows is that the same bytes compile to the same record. It does not show that the archive still says the same thing today.