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.
MethodInputError
Section titled “MethodInputError”class
MethodInputErrorInputs 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
GridA single-band raster loaded as float64 with its georeferencing and source hash.
Attributes
Section titled “Attributes”| Name | Type | Description |
|---|---|---|
values | FloatGrid | (rows, cols) float64 array. Missing cells are NaN, never zero. |
transform | affine.Affine | Affine transform from pixel to projected coordinates in metres. |
crs | rasterio.crs.CRS | The lunar south polar stereographic CRS. |
sha256 | str | Hex SHA-256 digest of the source file’s bytes. |
RoiSet
Section titled “RoiSet”class
RoiSetROI polygons read from a shapefile, in file order, with the shapefile’s hash.
Attributes
Section titled “Attributes”| Name | Type | Description |
|---|---|---|
polygons | tuple of shapely.geometry.Polygon | One polygon per shapefile record. Holes are rejected at load time. |
sha256 | str | Hex SHA-256 digest of the .shp file’s bytes. |
sha256_file
Section titled “sha256_file”function
def sha256_file(path: Path) -> strReturn the hex SHA-256 digest of a file’s bytes, read in 1 MiB chunks.
load_grid
Section titled “load_grid”function
def load_grid(path: Path, expected_sha256: str | None = None) -> GridLoad a single-band raster as a Grid.
Parameters
Section titled “Parameters”| Name | Type | Description |
|---|---|---|
path | pathlib.Path | Raster file readable by rasterio. It must have exactly one band and a CRS. |
expected_sha256 | str | Frozen hex digest. When given, the file is hashed first and a mismatch raises before the raster is opened. Default None. |
Returns
Section titled “Returns”Grid— float64 values, the affine transform, the CRS and the file digest.
Raises
Section titled “Raises”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.
load_rois
Section titled “load_rois”function
def load_rois(shp: Path, expected_sha256: str | None = None) -> RoiSetLoad ROI polygons from a shapefile as a RoiSet.
Parameters
Section titled “Parameters”| Name | Type | Description |
|---|---|---|
shp | pathlib.Path | Path to the .shp file. A sibling .prj file with the same stem is required. |
expected_sha256 | str | Frozen hex digest of the .shp file. A mismatch raises before parsing. Default None. |
Returns
Section titled “Returns”RoiSet— Polygons in shapefile record order, plus the.shpdigest.
Raises
Section titled “Raises”MethodInputError— On a digest mismatch, a missing.prj, a.prjthat 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.
check_compatible
Section titled “check_compatible”function
def check_compatible(slope: Grid, slope_rms: Grid) -> NoneCheck that the slope and slope RMS grids describe the same north-up grid.
Parameters
Section titled “Parameters”| Name | Type | Description |
|---|---|---|
slope | Grid | The slope grid and the slope RMS grid. |
slope_rms | Grid | The slope grid and the slope RMS grid. |
Raises
Section titled “Raises”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.
roi_mask
Section titled “roi_mask”function
def roi_mask(poly: Polygon, grid: Grid, index: int) -> BoolGridClosed-rule boolean mask of one ROI on the grid.
Parameters
Section titled “Parameters”| Name | Type | Description |
|---|---|---|
poly | shapely.geometry.Polygon | ROI polygon in the grid’s CRS (metres). |
grid | Grid | Grid providing the transform and the output shape. Its values are not read. |
index | int | ROI index, used only in error messages. |
Returns
Section titled “Returns”numpy.ndarray of bool— Array with the grid’s shape. True marks cells whose centre is inside or on the boundary ofpoly.
Raises
Section titled “Raises”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.
roi_masks
Section titled “roi_masks”function
def roi_masks(rois: RoiSet, grid: Grid) -> list[BoolGrid]Closed-rule mask of every ROI, in shapefile order.
Parameters
Section titled “Parameters”| Name | Type | Description |
|---|---|---|
rois | RoiSet | The ROI polygons. |
grid | Grid | Grid providing the transform and the output shape. |
Returns
Section titled “Returns”list of numpy.ndarray of bool— One mask per ROI, each with the grid’s shape. Raises asroi_maskdoes.
roi_union
Section titled “roi_union”function
def roi_union(masks: Sequence[BoolGrid], grid: Grid) -> BoolGridThe union row’s cell set: any ROI mask.
Parameters
Section titled “Parameters”| Name | Type | Description |
|---|---|---|
masks | sequence of numpy.ndarray of bool | Per-ROI masks, each with the grid’s shape. |
grid | Grid | Used only for the output shape when masks is empty. |
Returns
Section titled “Returns”numpy.ndarray of bool— Elementwise logical OR of the masks. With no masks, an all-False array.
class
RowStatistics for one ROI (or "union") at one threshold tau.
Attributes
Section titled “Attributes”| Name | Type | Description |
|---|---|---|
roi | str | ROI index as a string ("0", "1", …) or "union". |
tau | float | Inclusion threshold, in the units of the slope RMS grid. |
roi_cells | int | Cells in the closed-rule ROI mask, including cells with missing values. |
valid_slope_cells | int | ROI cells whose slope value is valid (finite, within [0, 90] degrees). |
valid_err_cells | int | ROI cells whose slope RMS value is valid (finite and strictly positive). |
joint_cells | int | ROI cells where both the slope and the slope RMS value are valid. |
included_cells | int | Joint cells with slope RMS less than or equal to tau. |
included_fraction | float or None | included_cells / joint_cells. None when joint_cells is zero, because the fraction is undefined, not zero. |
slope_quantiles | dict of str to float, or None | Inverted-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_mean | float or None | Mean slope of the included cells, in degrees. None when no cells are included. |
slope_histogram | list of int | Counts of included slopes in 2-degree bins over [0, 90] (45 bins). The last bin includes 90. |
Row.as_json
Section titled “Row.as_json”def as_json(self) -> dict[str, object]Return the row’s fields as a plain dict. Values keep their Python types.
valid_slope
Section titled “valid_slope”function
def valid_slope(s: FloatGrid) -> BoolGridSpec validity of a slope sample: finite and within [0, 90] degrees.
Parameters
Section titled “Parameters”| Name | Type | Description |
|---|---|---|
s | numpy.ndarray of float64 | Slope values in degrees. Missing values are NaN. |
Returns
Section titled “Returns”numpy.ndarray of bool— True where the value is finite and0 <= s <= 90. Both endpoints are valid. NaN and infinite values are invalid.
Validity is with finite. Missing values never pass.
valid_err
Section titled “valid_err”function
def valid_err(e: FloatGrid) -> BoolGridSpec validity of a slope RMS sample: finite and strictly positive.
Parameters
Section titled “Parameters”| Name | Type | Description |
|---|---|---|
e | numpy.ndarray of float64 | Slope RMS values, in the units of the slope RMS grid. Missing values are NaN. |
Returns
Section titled “Returns”numpy.ndarray of bool— True where the value is finite and greater than zero. Zero is invalid.
Validity is with finite. Zero is invalid.
inclusion_mask
Section titled “inclusion_mask”function
def inclusion_mask(joint: BoolGrid, e: FloatGrid, tau: float) -> BoolGridSpec inclusion rule: jointly valid and slope RMS <= tau.
Parameters
Section titled “Parameters”| Name | Type | Description |
|---|---|---|
joint | numpy.ndarray of bool | Cells where both the slope and the slope RMS value are valid (see valid_slope and valid_err). |
e | numpy.ndarray of float64 | Slope RMS values. |
tau | float | Inclusion threshold, inclusive, in the units of e. |
Returns
Section titled “Returns”numpy.ndarray of bool— True wherejointholds ande <= 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 ; a NaN in
e makes the comparison False, so missing cells are never included.
check_taus
Section titled “check_taus”function
def check_taus(taus: Sequence[float]) -> tuple[float, ...]Validate thresholds and return them as a tuple of floats.
Parameters
Section titled “Parameters”| Name | Type | Description |
|---|---|---|
taus | sequence of float | Inclusion thresholds, in the units of the slope RMS grid. |
Returns
Section titled “Returns”tuple of float— The thresholds in the order given.
Raises
Section titled “Raises”MethodInputError— Iftausis empty, or any value is non-finite or negative. Zero is accepted.
Result
Section titled “Result”class
ResultComplete evaluation output for one set of inputs.
Attributes
Section titled “Attributes”| Name | Type | Description |
|---|---|---|
slope_sha256, slope_rms_sha256, roi_shp_sha256 | str | Hex SHA-256 digests of the three input files. |
shape | tuple of int | (rows, cols) of the grid. |
transform | tuple of float | Affine coefficients (a, b, c, d, e, f) of the grid transform. |
rows | tuple of Row | One row per ROI and threshold, grouped by ROI in shapefile order and then by threshold in the order given. The "union" rows come last. |
Result.as_json
Section titled “Result.as_json”def as_json(self) -> dict[str, object]Return a JSON-ready dict of the complete result.
Returns
Section titled “Returns”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 perRow, in row order). Values keep their Python types.
Result.canonical_bytes
Section titled “Result.canonical_bytes”def canonical_bytes(self) -> bytesReturn as_json serialised by canonical_json.
canonical_json
Section titled “canonical_json”function
def canonical_json(obj: object) -> bytesSerialise obj as canonical UTF-8 JSON.
The output has sorted keys, compact separators and one trailing newline.
Parameters
Section titled “Parameters”| Name | Type | Description |
|---|---|---|
obj | object | A JSON-serialisable object, such as the dict returned by Result.as_json. |
Returns
Section titled “Returns”bytes— UTF-8 encoded JSON text ending in"\n".
Raises
Section titled “Raises”ValueError— Ifobjcontains NaN or infinity, becauseallow_nan=False.
evaluate
Section titled “evaluate”function
def evaluate( slope: Grid, slope_rms: Grid, rois: RoiSet, taus: Sequence[float] = DEFAULT_TAUS,) -> ResultDIRECT evaluation over in-memory, already-verified inputs.
Parameters
Section titled “Parameters”| Name | Type | Description |
|---|---|---|
slope | Grid | The slope grid and the slope RMS grid. They must share the same grid. |
slope_rms | Grid | The slope grid and the slope RMS grid. They must share the same grid. |
rois | RoiSet | ROI polygons. |
taus | sequence of float | Inclusion thresholds, in the units of the slope RMS grid. Default DEFAULT_TAUS. |
Returns
Section titled “Returns”Result— Input digests, grid shape and transform, and one row per ROI and threshold, then one row per threshold for the union.
Raises
Section titled “Raises”MethodInputError— Fromcheck_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.
run_paths
Section titled “run_paths”function
def run_paths( slope: Path, slope_rms: Path, roi_shp: Path, taus: Sequence[float] = DEFAULT_TAUS, frozen: dict[str, str] | None = None,) -> ResultLoad from files (optionally verifying frozen sha256s) and evaluate.
Parameters
Section titled “Parameters”| Name | Type | Description |
|---|---|---|
slope | pathlib.Path | Raster files for the slope and slope RMS grids. |
slope_rms | pathlib.Path | Raster files for the slope and slope RMS grids. |
roi_shp | pathlib.Path | ROI shapefile. A sibling .prj is required. |
taus | sequence of float | Inclusion thresholds, in the units of the slope RMS grid. Default DEFAULT_TAUS. |
frozen | dict of str to str | Expected 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. |
Returns
Section titled “Returns”Result— As returned byevaluate.
Raises
Section titled “Raises”MethodInputError— From the loaders or fromevaluate. I/O errors raised by rasterio or pyogrio are not converted to MethodInputError.