Skip to content

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.

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 beats
study.why("pair:002ea7a82e1060f1") # names the pair that beat it
study.save("vikram.study.json") # replays to the same sha256

archive="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")
>>> study
Study(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()
True

The 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 --sources
lunartrace verify vikram.study.json
lunartrace replay vikram.study.json

Inside 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, 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 None
2.0 9 9 9 1.0 10.0

No 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.

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.

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))
>>> 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 None
2 2420 2420 1605 0.663223 2.524974
25 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_result

Columns 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)
True

result.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.

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-study
uv 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.json

Use 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.

uv run --frozen pytest tests/test_docs_quickstart.py -q

The 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.

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-figure

This writes figure.svg and figure.pdf. Keep the matching projection and receipt with them, since the footer does not record admission.

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.