Skip to content

lunartrace.science.raster_projection

Emitter for the lunartrace.projection.slope_raster/v0 cross-client contract.

The contract model lives in lunartrace.projection. This module turns the SAME frozen inputs that lunartrace.slope_quality/v1 reads into one JSON manifest plus raw sidecar .bin layers (little-endian, row-major, row 0 at the affine origin). Inclusion layers are computed only through lunartrace.science.slope_quality’s own validity and inclusion functions, so a client can count included cells without re-deriving the rule.

Usage (real inputs, output is gitignored):

python -I -m lunartrace.science.raster_projection emit \
--manifest research/sources/pgda78-site01.manifest.json \
--store <store>/lt-source-store --out build/projections
python -I -m lunartrace.science.raster_projection synthetic-fixture --out contracts/fixtures

Authority and units: the emitter writes the lunartrace.projection.slope_raster/v0 contract. Its model lives in lunartrace.projection, and this module does not define a separate schema. Slope and slope RMS layers are float32 in degrees, little-endian, row-major, with NaN as nodata. Inclusion layers are one uint8 per cell, with value 0 for excluded, 1 for included and 255 for nodata (no valid joint sample). The grid is referenced to its affine transform and a CRS given as a PROJ string. Validity and inclusion are computed by lunartrace.science.slope_quality, so the emitter adds no rule of its own. Standing is recorded as supplied by the caller, except that NUMERIC_REPRODUCED is refused unless a rerun reproduces a committed result hash (verify_reproduction), and SCIENCE_VERTICAL_QUALIFIED is always refused. The synthetic fixture is SYNTHETIC test data with no source provenance and must not be read as a scientific result.

class

EncodedLayer

One raster layer: its contract metadata and its encoded bytes.

NameTypeDescription
specdict[str, object]Layer metadata (kind, unit, dtype, nodata, byte_order, layout, uri) plus sha256 and byte_length once encode_layers has set them.
databytesThe raw sidecar bytes written to uri.

function

def float32_le(values: npt.NDArray[np.float64], what: str) -> bytes

Encode samples as little-endian float32 bytes in row-major order.

NameTypeDescription
valuesnumpy.ndarray of float64Grid samples. NaN is allowed and is preserved as nodata.
whatstrLayer name used in the error message.
  • bytes — values cast to <f4 and laid out C-contiguously.
  • lunartrace.science.slope_quality.MethodInputError — If casting to float32 changes any sample (other than NaN, which compares equal).

The check is round-trip exactness, so no sample is rounded silently.

function

def inclusion_codes(
slope: sq.FloatGrid,
rms: sq.FloatGrid,
tau: float,
) -> npt.NDArray[np.uint8]

Compute per-cell inclusion codes for one RMS threshold.

NameTypeDescription
slopenumpy.ndarray of float64Slope grid in degrees.
rmsnumpy.ndarray of float64Slope RMS grid in degrees, compared against tau.
taufloatRMS threshold in degrees.
  • numpy.ndarray of uint8 — Same shape as slope. Codes: INCLUSION_NODATA (255) where the cell is not jointly valid, INCLUSION_EXCLUDED (0) where it is valid but excluded, and INCLUSION_INCLUDED (1) where it is included.

Joint validity is lunartrace.science.slope_quality.valid_slope and lunartrace.science.slope_quality.valid_err. Inclusion is lunartrace.science.slope_quality.inclusion_mask, which implements valid∧(σrms≤τ)\text{valid} \wedge (\sigma_{\mathrm{rms}} \le \tau). Missing values are never coded as included.

function

def encode_layers(
slope: sq.Grid,
slope_rms: sq.Grid,
taus: Sequence[float],
stem: str,
) -> list[EncodedLayer]

Encode the slope, slope RMS and per-tau inclusion layers with their metadata.

NameTypeDescription
slopelunartrace.science.slope_quality.GridSlope grid in degrees.
slope_rmslunartrace.science.slope_quality.GridSlope RMS grid in degrees, on the same lattice as slope.
tausSequence[float]RMS thresholds in degrees; validated by slope_quality.check_taus.
stemstrFile-name prefix for the sidecar uri values.
  • list[EncodedLayer] — Layers in this order: slope (unit deg), slope_rms (unit deg), then one inclusion layer per tau (unit None, dtype uint8). Each spec carries sha256 and byte_length of its bytes.
  • lunartrace.science.slope_quality.MethodInputError — If the grids are incompatible, the taus are invalid, or a float32 narrowing is lossy.

function

def write_projection(
out_dir: Path,
stem: str,
slope: sq.Grid,
slope_rms: sq.Grid,
taus: Sequence[float],
*,
standing: Standing,
source_witness_id: str,
citation: str,
crs_proj4: str,
) -> Path

Validate the projection, then write <stem>.projection.json and its sidecars.

NameTypeDescription
out_dirpathlib.PathOutput directory, created if missing. Existing files with the same names are overwritten.
stemstrFile-name prefix for the projection document and its .bin sidecars.
slopelunartrace.science.slope_quality.GridSlope grid in degrees. Its transform defines the grid’s affine placement.
slope_rmslunartrace.science.slope_quality.GridSlope RMS grid in degrees.
tausSequence[float]RMS thresholds in degrees.
standingStandingStanding label, recorded as given (see lunartrace.projection.Standing).
source_witness_idstrIdentifier of the source witness.
citationstrCitation text carried by the projection.
crs_proj4strCRS as a PROJ string.
  • pathlib.Path — Path to the written <stem>.projection.json.
  • lunartrace.science.slope_quality.MethodInputError — From layer encoding (see encode_layers).
  • pydantic.ValidationError — If the projection model rejects the document. The validation runs before any file is written.

Sidecar files are written first, then the JSON document, using LF line endings.

function

def emit_from_manifest(
manifest_path: Path,
store: Path,
out_dir: Path,
taus: Sequence[float] = sq.DEFAULT_TAUS,
standing: Standing = 'METHOD_EXECUTED',
reproduces: Path | None = None,
) -> Path

Emit slope-raster projection files from SOURCE_FROZEN CAS inputs.

NameTypeDescription
manifest_pathpathlib.PathA SOURCE_FROZEN source-witness manifest.
storepathlib.PathContent-addressed store holding the manifest’s blobs.
out_dirpathlib.PathOutput directory for the projection and its sidecars.
tausSequence[float]RMS thresholds in degrees. Defaults to slope_quality.DEFAULT_TAUS. Default sq.DEFAULT_TAUS.
standingStandingStanding label to record. Defaults to "METHOD_EXECUTED", which is an execution label and not a scientific standing. "NUMERIC_REPRODUCED" is recorded only when reproduces is given and the rerun matches it. "SCIENCE_VERTICAL_QUALIFIED" is never recorded by this function. Default 'METHOD_EXECUTED'.
reproducespathlib.Path or NoneA committed slope_quality/v0 projection to reproduce. Required for "NUMERIC_REPRODUCED". See verify_reproduction. Default None.
  • pathlib.Path — Path to the written <witness id>.slope-raster.projection.json.
  • lunartrace.science.slope_quality.MethodInputError — If the manifest or its blobs fail checks, the manifest has no string id, or the requested standing is not earned by a passing reproduction.

Every blob is SHA-256-verified before it is read. The slope and slope RMS grids are loaded with their expected digests.

function

def verify_reproduction(manifest_path: Path, store: Path, reference: Path) -> str

Rerun slope_quality/v1 on the manifest’s inputs and check it against reference.

NameTypeDescription
manifest_pathpathlib.PathA SOURCE_FROZEN source-witness manifest.
storepathlib.PathContent-addressed store holding the manifest’s blobs.
referencepathlib.PathA committed lunartrace.projection.slope_quality/v0 document whose source_witness_id is the manifest’s id.
  • str — The matching hex SHA-256 of the rerun’s canonical result bytes.
  • lunartrace.science.slope_quality.MethodInputError — If the reference names another witness or has no result hash, or if the rerun’s hash differs from it.

The thresholds are taken from the reference’s own rows, so the rerun asks the same question the reference answered. This is the comparison that earns NUMERIC_REPRODUCED; it is not a scientific admission.

function

def synthetic_grids() -> tuple[sq.Grid, sq.Grid]

Build the SYNTHETIC 3-row x 4-column slope and slope RMS grids used for the fixture.

  • tuple[lunartrace.science.slope_quality.Grid, lunartrace.science.slope_quality.Grid] — (slope, slope_rms) sharing one affine transform (cell size 5 m) and the synthetic CRS SYNTHETIC_PROJ4 (a south polar stereographic on a 1737400 m sphere, in metres).

Slope has a NaN (nodata) and an out-of-domain 95 degrees. Slope RMS has a NaN and a non-positive 0. Values are exact in float32. These grids are SYNTHETIC, with no source provenance, and are not source data.

function

def write_synthetic_fixture(out_dir: Path) -> Path

Write the committed SYNTHETIC fixture projection to out_dir.

NameTypeDescription
out_dirpathlib.PathOutput directory, created if missing.
  • pathlib.Path — Path to synthetic-4x3.slope-raster.projection.json.

Uses synthetic_grids, the default taus, standing "METHOD_EXECUTED", the witness id synthetic-raster-4x3 and a citation that states there is no source. The output is a test fixture, not a scientific result.

function

def main(argv: Sequence[str] | None = None) -> int

Command-line entry point with the emit and synthetic-fixture subcommands.

NameTypeDescription
argvSequence[str] or NoneArguments without the program name. If None, sys.argv is used. Default None.
  • int — 0 on success. Method and input errors propagate as exceptions; argparse exits on usage errors.

emit takes --manifest, --store, --out, optional repeated --tau, --standing (default METHOD_EXECUTED) and --reproduces, which NUMERIC_REPRODUCED requires. synthetic-fixture takes --out.