Skip to content

lunartrace.science.slope_quality

Primary GDAL implementation of MethodContract lunartrace.slope_quality/v1.

Spec: contracts/methods/slope-quality-v1.md. This path uses GDAL through rasterio (raster I/O and polygon rasterisation), pyogrio (OGR vector I/O) and shapely. The independent oracle in research/oracles uses none of them.

Purpose For each region of interest (ROI) polygon, and for their union, count the cells that pass the spec validity and inclusion rules at each threshold tau applied to the slope RMS grid, and summarise the included slope values (quantiles, mean, histogram).

Authority and standing This module implements a frozen method contract. Its output is a numeric reproduction of that contract with standing NUMERIC_REPRODUCED. It is not a scientific admission of any result.

Units and CRS Grids and ROI shapefiles must use the lunar south polar stereographic projection on a sphere of radius MOON_RADIUS_M (metres), so grid pixel sizes and ROI coordinates are in metres. Slope values are in degrees and are valid on [0, 90]. tau is compared directly with the slope RMS grid values, with no unit conversion.

Missing values Nodata cells become NaN on load. NaN fails every validity and inclusion test, so a missing value is never treated as zero. Missing cells still belong to the ROI mask and are counted in roi_cells but never in the valid, joint or included counts.

Closed boundary rule realisation (with amendment A1 self-touching rings): the ROI vertices lie on cell centres and every edge is an 8-connected one-cell step, so any cell centre not on the boundary lies at least 5/sqrt(2) m (about 3.54 m) from it. Buffering each polygon by _BOUNDARY_BUFFER_M = 0.5 m then makes GDAL’s centre-inside test equal “inside or on the boundary”. Polygons that violate the lattice/step precondition fail closed instead of being rasterised approximately.

class

MethodInputError

Inputs violate the method contract; the method fails closed.

Raised for a sha256 pin mismatch, a raster or shapefile that is not in the required CRS, a band count or shape or transform mismatch, a non north-up grid, an ROI that is not a single polygon, an ROI with holes, an ROI off the cell-centre lattice, an ROI edge longer than one cell, or invalid taus.

class

Grid

A single-band raster loaded as float64 with its georeferencing and source hash.

NameTypeDescription
valuesFloatGrid(rows, cols) float64 array. Missing cells are NaN, never zero.
transformaffine.AffineAffine transform from pixel to projected coordinates in metres.
crsrasterio.crs.CRSThe lunar south polar stereographic CRS.
sha256strHex SHA-256 digest of the source file’s bytes.

class

RoiSet

ROI polygons read from a shapefile, in file order, with the shapefile’s hash.

NameTypeDescription
polygonstuple of shapely.geometry.PolygonOne polygon per shapefile record. Holes are rejected at load time.
sha256strHex SHA-256 digest of the .shp file’s bytes.

function

def sha256_file(path: Path) -> str

Return the hex SHA-256 digest of a file’s bytes, read in 1 MiB chunks.

function

def load_grid(path: Path, expected_sha256: str | None = None) -> Grid

Load a single-band raster as a Grid.

NameTypeDescription
pathpathlib.PathRaster file readable by rasterio. It must have exactly one band and a CRS.
expected_sha256strFrozen hex digest. When given, the file is hashed first and a mismatch raises before the raster is opened. Default None.
  • Grid — float64 values, the affine transform, the CRS and the file digest.
  • MethodInputError — On a digest mismatch, a band count other than one, a missing CRS, or a CRS that is not the lunar south polar stereographic CRS.

Values equal to the dataset’s nodata value are replaced with NaN. If the nodata value is itself NaN, no replacement is made. The array is always a float64 copy.

function

def load_rois(shp: Path, expected_sha256: str | None = None) -> RoiSet

Load ROI polygons from a shapefile as a RoiSet.

NameTypeDescription
shppathlib.PathPath to the .shp file. A sibling .prj file with the same stem is required.
expected_sha256strFrozen hex digest of the .shp file. A mismatch raises before parsing. Default None.
  • RoiSet — Polygons in shapefile record order, plus the .shp digest.
  • MethodInputError — On a digest mismatch, a missing .prj, a .prj that is not the lunar south polar stereographic CRS, a record that is not a Polygon, or a polygon with interior rings.

Geometry is read with pyogrio and parsed from WKB with shapely. The digest covers only the .shp file, not the .prj or .dbf files.

function

def check_compatible(slope: Grid, slope_rms: Grid) -> None

Check that the slope and slope RMS grids describe the same north-up grid.

NameTypeDescription
slopeGridThe slope grid and the slope RMS grid.
slope_rmsGridThe slope grid and the slope RMS grid.
  • MethodInputError — If the shapes differ, the affine transforms differ, the CRSs differ, or the transform is not north-up with square positive pixels (b == d == 0, a > 0, e == -a).

Grids are compared exactly and are never resampled. Pixel size is in metres.

function

def roi_mask(poly: Polygon, grid: Grid, index: int) -> BoolGrid

Closed-rule boolean mask of one ROI on the grid.

NameTypeDescription
polyshapely.geometry.PolygonROI polygon in the grid’s CRS (metres).
gridGridGrid providing the transform and the output shape. Its values are not read.
indexintROI index, used only in error messages.
  • numpy.ndarray of bool — Array with the grid’s shape. True marks cells whose centre is inside or on the boundary of poly.
  • MethodInputError — If the polygon violates the lattice precondition (see _check_lattice_polygon).

The polygon is checked, made valid with linework, buffered by _BOUNDARY_BUFFER_M (0.5 m, mitre joins) and rasterised with all_touched=False. Because the mask depends only on geometry, cells with missing values are still included in the mask.

function

def roi_masks(rois: RoiSet, grid: Grid) -> list[BoolGrid]

Closed-rule mask of every ROI, in shapefile order.

NameTypeDescription
roisRoiSetThe ROI polygons.
gridGridGrid providing the transform and the output shape.
  • list of numpy.ndarray of bool — One mask per ROI, each with the grid’s shape. Raises as roi_mask does.

function

def roi_union(masks: Sequence[BoolGrid], grid: Grid) -> BoolGrid

The union row’s cell set: any ROI mask.

NameTypeDescription
maskssequence of numpy.ndarray of boolPer-ROI masks, each with the grid’s shape.
gridGridUsed only for the output shape when masks is empty.
  • numpy.ndarray of bool — Elementwise logical OR of the masks. With no masks, an all-False array.

class

Row

Statistics for one ROI (or "union") at one threshold tau.

NameTypeDescription
roistrROI index as a string ("0", "1", …) or "union".
taufloatInclusion threshold, in the units of the slope RMS grid.
roi_cellsintCells in the closed-rule ROI mask, including cells with missing values.
valid_slope_cellsintROI cells whose slope value is valid (finite, within [0, 90] degrees).
valid_err_cellsintROI cells whose slope RMS value is valid (finite and strictly positive).
joint_cellsintROI cells where both the slope and the slope RMS value are valid.
included_cellsintJoint cells with slope RMS less than or equal to tau.
included_fractionfloat or Noneincluded_cells / joint_cells. None when joint_cells is zero, because the fraction is undefined, not zero.
slope_quantilesdict of str to float, or NoneInverted-CDF quantiles of the included slopes at probabilities 0.05, 0.25, 0.5, 0.75 and 0.95, keyed by str(probability). None when no cells are included.
slope_meanfloat or NoneMean slope of the included cells, in degrees. None when no cells are included.
slope_histogramlist of intCounts of included slopes in 2-degree bins over [0, 90] (45 bins). The last bin includes 90.
def as_json(self) -> dict[str, object]

Return the row’s fields as a plain dict. Values keep their Python types.

function

def valid_slope(s: FloatGrid) -> BoolGrid

Spec validity of a slope sample: finite and within [0, 90] degrees.

NameTypeDescription
snumpy.ndarray of float64Slope values in degrees. Missing values are NaN.
  • numpy.ndarray of bool — True where the value is finite and 0 <= s <= 90. Both endpoints are valid. NaN and infinite values are invalid.

Validity is s∈[0,90]s \in [0, 90] with ss finite. Missing values never pass.

function

def valid_err(e: FloatGrid) -> BoolGrid

Spec validity of a slope RMS sample: finite and strictly positive.

NameTypeDescription
enumpy.ndarray of float64Slope RMS values, in the units of the slope RMS grid. Missing values are NaN.
  • numpy.ndarray of bool — True where the value is finite and greater than zero. Zero is invalid.

Validity is σ>0\sigma > 0 with σ\sigma finite. Zero is invalid.

function

def inclusion_mask(joint: BoolGrid, e: FloatGrid, tau: float) -> BoolGrid

Spec inclusion rule: jointly valid and slope RMS <= tau.

NameTypeDescription
jointnumpy.ndarray of boolCells where both the slope and the slope RMS value are valid (see valid_slope and valid_err).
enumpy.ndarray of float64Slope RMS values.
taufloatInclusion threshold, inclusive, in the units of e.
  • numpy.ndarray of bool — True where joint holds and e <= tau.

The rule is elementwise, so it applies equally to an ROI’s samples or to the whole grid. The raster projection uses exactly this function for its per-tau inclusion layers. Formally, a cell is included when joint∧(e≤τ)\mathrm{joint} \wedge (e \le \tau); a NaN in e makes the comparison False, so missing cells are never included.

function

def check_taus(taus: Sequence[float]) -> tuple[float, ...]

Validate thresholds and return them as a tuple of floats.

NameTypeDescription
taussequence of floatInclusion thresholds, in the units of the slope RMS grid.
  • tuple of float — The thresholds in the order given.
  • MethodInputError — If taus is empty, or any value is non-finite or negative. Zero is accepted.

class

Result

Complete evaluation output for one set of inputs.

NameTypeDescription
slope_sha256, slope_rms_sha256, roi_shp_sha256strHex SHA-256 digests of the three input files.
shapetuple of int(rows, cols) of the grid.
transformtuple of floatAffine coefficients (a, b, c, d, e, f) of the grid transform.
rowstuple of RowOne row per ROI and threshold, grouped by ROI in shapefile order and then by threshold in the order given. The "union" rows come last.
def as_json(self) -> dict[str, object]

Return a JSON-ready dict of the complete result.

  • dict of str to object — Keys "method" (METHOD_ID), "implementation" (IMPLEMENTATION_ID), "inputs" (the three SHA-256 digests), "grid" (shape and transform) and "results" (one dict per Row, in row order). Values keep their Python types.
def canonical_bytes(self) -> bytes

Return as_json serialised by canonical_json.

function

def canonical_json(obj: object) -> bytes

Serialise obj as canonical UTF-8 JSON.

The output has sorted keys, compact separators and one trailing newline.

NameTypeDescription
objobjectA JSON-serialisable object, such as the dict returned by Result.as_json.
  • bytes — UTF-8 encoded JSON text ending in "\n".
  • ValueError — If obj contains NaN or infinity, because allow_nan=False.

function

def evaluate(
slope: Grid,
slope_rms: Grid,
rois: RoiSet,
taus: Sequence[float] = DEFAULT_TAUS,
) -> Result

DIRECT evaluation over in-memory, already-verified inputs.

NameTypeDescription
slopeGridThe slope grid and the slope RMS grid. They must share the same grid.
slope_rmsGridThe slope grid and the slope RMS grid. They must share the same grid.
roisRoiSetROI polygons.
taussequence of floatInclusion thresholds, in the units of the slope RMS grid. Default DEFAULT_TAUS.
  • Result — Input digests, grid shape and transform, and one row per ROI and threshold, then one row per threshold for the union.
  • MethodInputError — From check_compatible, check_taus, or the ROI lattice precondition.

No files are read and no digests are verified here. The caller is responsible for the inputs having been verified. Missing cells are NaN and are never counted as valid or included.

function

def run_paths(
slope: Path,
slope_rms: Path,
roi_shp: Path,
taus: Sequence[float] = DEFAULT_TAUS,
frozen: dict[str, str] | None = None,
) -> Result

Load from files (optionally verifying frozen sha256s) and evaluate.

NameTypeDescription
slopepathlib.PathRaster files for the slope and slope RMS grids.
slope_rmspathlib.PathRaster files for the slope and slope RMS grids.
roi_shppathlib.PathROI shapefile. A sibling .prj is required.
taussequence of floatInclusion thresholds, in the units of the slope RMS grid. Default DEFAULT_TAUS.
frozendict of str to strExpected hex SHA-256 digests keyed by "slope", "slope_rms" and "roi_shp". A key that is absent means that input is not pinned. Default None.
  • Result — As returned by evaluate.
  • MethodInputError — From the loaders or from evaluate. I/O errors raised by rasterio or pyogrio are not converted to MethodInputError.