Quickstart
Install LunarTrace with pip install lunartrace, or use the locked
environment of a repository checkout. All pycon blocks below are
executable Python sessions, tested directly from this page. >>> / ... are prompts; omit
them when pasting into a script.
The later Site01 sections need the private PGDA Site01 witness, not browse imagery or
invented observations.
The one call
Section titled “The one call”Three lines after the import take you from a site, an event interval and a product type to a Study that can be checked, saved and replayed:
import lunartrace as lt
study = lt.temporal.study(site=(-69.3741, 32.32), event="2023-08-23T12:33Z/12:35Z", products="LRO/LROC/CDRNAC4", archive="vikram-2026-10-14")study.frontier("angles") # the 13 pairs nothing beatsstudy.why("pair:002ea7a82e1060f1") # names the pair that beat itstudy.save("vikram.study.json") # replays to the same sha256archive="vikram-2026-10-14" replays the archived pages of the 14 October 2026 run. Those
pages ship inside the package, so this call needs no network and gives the same answer every
time. A live call, with archive left out, fetches the Lunar ODE archive now. The archive
grows, so a live call can return more than the 108 products below and a different digest. A
live Study is a new observation, not a reproduction of the 14 October run.
>>> import lunartrace as lt>>> study = lt.temporal.study(site=(-69.3741, 32.32),... event="2023-08-23T12:33Z/12:35Z",... products="LRO/LROC/CDRNAC4",... archive="vikram-2026-10-14")>>> studyStudy(vikram:minute-envelope · retained · sha256:3d75a662… · 108 products · 2,340 pairs)>>> len(study.frontier("angles"))13>>> print(study.why("pair:002ea7a82e1060f1"))pair:002ea7a82e1060f1 is off the angles frontier: pair:0033128b0d75e56c is at least as good on every angle (Δsun 2.78° vs 6.79°, Δview 0.29° vs 0.83°, Δphase 2.67° vs 3.14°) and strictly better on one.>>> study.verify()TrueThe same run on the command line, which also writes the file and its retained sources:
lunartrace study --site -69.3741,32.32 --event 2023-08-23T12:33Z/12:35Z --products LRO/LROC/CDRNAC4 --archive vikram-2026-10-14 -o vikram.study.json --sourceslunartrace verify vikram.study.jsonlunartrace replay vikram.study.jsonInside the checkout, put uv run --frozen before lunartrace. The last command prints
REPLAY_MATCH when the saved record recompiles to the same digest. The Study is METHOD_EXECUTED:
it records that the method ran on the retained bytes. Temporal adjacency is not a
surface-change claim. The one-call Study covers frontiers, why, save, replay,
the notebook and the four limits.
Synthetic DIRECT example
Section titled “Synthetic DIRECT example”SYNTHETIC, not lunar measurements and not NUMERIC_REPRODUCED. A 3×3 grid with 5 m cells demonstrates the closed ROI boundary, τ inclusion and empty statistics. Every ROI vertex is a cell centre and every segment is one lattice step.
>>> import numpy as np>>> from affine import Affine>>> from rasterio.crs import CRS>>> from shapely.geometry import Polygon>>> from lunartrace.science import slope_quality as sq>>> transform = Affine(5, 0, 0, 0, -5, 15)>>> crs = CRS.from_string("+proj=stere +lat_0=-90 +lat_ts=-90 +lon_0=0 +R=1737400 +units=m +no_defs")>>> ring = [(0, 0), (1, 0), (2, 0), (2, 1), (2, 2), (1, 2), (0, 2), (0, 1)]>>> polygon = Polygon([transform @ (c + 0.5, r + 0.5) for c, r in ring])>>> slope = sq.Grid(np.full((3, 3), 10.0), transform, crs, "synthetic-slope")>>> rms = sq.Grid(np.full((3, 3), 1.0), transform, crs, "synthetic-rms")>>> rois = sq.RoiSet((polygon,), "synthetic-roi")>>> result = sq.evaluate(slope, rms, rois, taus=(0.5, 2.0))>>> for row in result.rows:... if row.roi == "union":... print(row.tau, row.roi_cells, row.joint_cells, row.included_cells, row.included_fraction, row.slope_mean)0.5 9 9 0 0.0 None2.0 9 9 9 1.0 10.0No included cells means null mean/quantiles; fraction is 0.0 when the jointly
valid denominator is positive, null when it is zero. Counts are not physical
area. An in-memory sha256 is caller-supplied identity; evaluate does not prove
that synthetic labels or arbitrary caller hashes came from frozen file bytes.
Real Site01: DIRECT, STUDY and CLI
Section titled “Real Site01: DIRECT, STUDY and CLI”For the command-line STUDY run with observed result.json figures, see the
Site01 walkthrough.
Observed local private-source output, CPython 3.13.5 on Windows, locked
environment, source baseline 172aa976925fcbc022814adcfe274ebeba270e3c. The source
manifest identifies 11 role-labelled blobs, including the frozen README and
surface products; STUDY resolution verifies every role, not only the two rasters.
The store is taken from --store, or from LUNARTRACE_SOURCE_STORE when --store is
not given. There is no built-in host default. Set LUNARTRACE_SOURCE_STORE to an authorized
replica if needed.
If those blobs are unavailable, run only the synthetic section and inspect the committed real projection as a dated result, not a fresh execution. The test reports this as a real-source skip, never as numeric reproduction. Missing data is not zero and the runner never fetches or fabricates a substitute.
Verify custody and stage the shapefile
Section titled “Verify custody and stage the shapefile”CAS files have hash names. OGR requires .shp/.shx/.dbf/.prj files sharing a stem,
so copy only those small verified sidecars into a temporary directory. The source
rasters remain in CAS, unmodified. Temporary output is cleaned up below.
>>> import hashlib>>> import json>>> import os>>> import shutil>>> import subprocess>>> import sys>>> from pathlib import Path>>> from tempfile import TemporaryDirectory>>> from lunartrace.science import slope_quality as sq>>> from lunartrace.science.study import load_manifest, resolve_inputs, run_study>>> manifest_path = Path("research/sources/pgda78-site01.manifest.json")>>> store = Path(os.environ["LUNARTRACE_SOURCE_STORE"])>>> inputs = resolve_inputs(load_manifest(manifest_path), store)>>> print(len(inputs))11>>> workspace = TemporaryDirectory(prefix="lt-docs-")>>> work = Path(workspace.name)>>> shp = work / "roi.shp">>> for role, suffix in [("roi_shp", ".shp"), ("roi_shx", ".shx"), ("roi_dbf", ".dbf"), ("roi_prj", ".prj")]:... _ = shutil.copyfile(inputs[role].path, shp.with_suffix(suffix))DIRECT: in memory and from files
Section titled “DIRECT: in memory and from files”>>> slope = sq.load_grid(inputs["slope"].path, inputs["slope"].sha256)>>> rms = sq.load_grid(inputs["slope_rms"].path, inputs["slope_rms"].sha256)>>> rois = sq.load_rois(shp, inputs["roi_shp"].sha256)>>> result = sq.evaluate(slope, rms, rois)>>> print(result.shape, len(result.rows))(3200, 3200) 24>>> for row in result.rows:... if row.roi == "union":... mean = "None" if row.slope_mean is None else f"{row.slope_mean:.6f}"... print(f"{row.tau:g} {row.roi_cells} {row.joint_cells} {row.included_cells} {row.included_fraction:.6f} {mean}")0.5 2420 2420 0 0.000000 None2 2420 2420 1605 0.663223 2.52497425 2420 2420 2420 1.000000 2.526202>>> result_bytes = result.canonical_bytes()>>> digest = hashlib.sha256(result_bytes).hexdigest()>>> print(digest)f0065fafcefc7263172c8446b456d0f51cbf0f1627e364981eb870c0ca2cc5a0>>> del slope, rms, rois, result>>> frozen = {role: inputs[role].sha256 for role in ("slope", "slope_rms", "roi_shp")}>>> files_result = sq.run_paths(inputs["slope"].path, inputs["slope_rms"].path, shp, frozen=frozen)>>> print(files_result.canonical_bytes() == result_bytes)True>>> del files_resultColumns are τ (degrees), ROI cells, jointly valid cells, included cells,
included/joint fraction, mean slope (degrees). τ=2° includes 1605/2420 jointly
valid cells; this is not a fraction of lunar area or a safety result. run_paths
checks the supplied three hashes; STUDY additionally verifies every manifest role.
Delete large grids before subsequent runs on a low-RAM host.
STUDY: deterministic result plus execution receipt
Section titled “STUDY: deterministic result plus execution receipt”>>> receipt = run_study(manifest_path, store, work / "python-study")>>> print(receipt["status"])METHOD_EXECUTED>>> print(receipt["result_sha256"])f0065fafcefc7263172c8446b456d0f51cbf0f1627e364981eb870c0ca2cc5a0>>> print((work / "python-study" / "result.json").read_bytes() == result_bytes)Trueresult.json is canonical science. receipt.json records manifest/input hashes,
git HEAD/dirty state, environment, lock hash and execution times. Its bytes change
between runs. Fresh execution is METHOD_EXECUTED. The separately recorded
Site01 independent oracle agreement supports NUMERIC_REPRODUCED, while
scientific admission remains NOT_ADMITTED. Matching this documented hash does
not replace an independent oracle or admission decision.
Actual module CLI and projection
Section titled “Actual module CLI and projection”The following session runs the native CLI in a child interpreter, rather than
mocking it. check=True makes an unsuccessful command fail the example.
>>> command = [sys.executable, "-I", "-m", "lunartrace.science.study", "run", "--manifest", str(manifest_path), "--store", str(store), "--out", str(work / "cli-study")]>>> completed = subprocess.run(command, check=True, capture_output=True, text=True)>>> print(completed.stdout.split()[0])result_sha256=f0065fafcefc7263172c8446b456d0f51cbf0f1627e364981eb870c0ca2cc5a0>>> print((work / "cli-study" / "result.json").read_bytes() == result_bytes)True>>> projection_path = work / "projection.json">>> command = [sys.executable, "-I", "-m", "lunartrace.science.study", "project", "--result", str(work / "cli-study" / "result.json"), "--manifest", str(manifest_path), "--standing", "METHOD_EXECUTED", "--out", str(projection_path)]>>> completed = subprocess.run(command, check=True, capture_output=True, text=True)>>> projection = json.loads(projection_path.read_text(encoding="utf-8"))>>> print(projection["schema"], projection["standing"], projection["source_witness_id"])lunartrace.projection.slope_quality/v0 METHOD_EXECUTED pgda78-site01>>> workspace.cleanup()Equivalent copyable shell command, from the repository root on this host:
uv run --frozen python -I -m lunartrace.science.study run --manifest research/sources/pgda78-site01.manifest.json --store "$LUNARTRACE_SOURCE_STORE" --out .build/site01-studyuv run --frozen python -I -m lunartrace.science.study project --result .build/site01-study/result.json --manifest research/sources/pgda78-site01.manifest.json --standing METHOD_EXECUTED --out .build/site01-study/projection.jsonUse a fresh output directory to preserve an earlier receipt; run overwrites its
named result/receipt files. project trusts the caller’s permitted standing
choice: schema validity is not evidence for elevating it. Source data is not
redistributed; keep the repository private. See CLI and method.
Execute this page
Section titled “Execute this page”uv run --frozen pytest tests/test_docs_quickstart.py -qThe existing runner extracts this page’s pycon blocks and executes them with Python
doctest. It runs the synthetic section everywhere and explicitly skips the real
section only when source blobs are absent. An absent source store is a skip, not
a passing real-source reproduction. Run serially on a low-RAM host.
Optional: the numeric Figure
Section titled “Optional: the numeric Figure”uv run --frozen python -I examples/polar-source-quality/make_figure.py --result .build/site01-study/result.json --manifest research/sources/pgda78-site01.manifest.json --out .build/site01-figureThis writes figure.svg and figure.pdf. Keep the matching projection and receipt with
them, since the footer does not record admission.
Keep outputs private
Section titled “Keep outputs private”Source rasters, derived grids, receipts and figures derived from them are local and private. Source rights are UNVERIFIED. Delete large grids before running again on a low-RAM host.
Next steps
Section titled “Next steps”- Workflows: notebook reader, Figure, the paper build and the browser projection seam.
- CLI reference and Python usage.
- Provenance: what the witness, CAS and receipt do and do not establish.
- Method: the cell rules behind every number above.