Method specification (slope_quality/v1)
This page specifies lunartrace.slope_quality/v1, the method implemented in
src/lunartrace/science/slope_quality.py. Every rule below is taken from that module
unless a different source is named. The method counts raster cells inside regions of
interest (ROIs) that pass validity and inclusion tests at a set of thresholds, and it
summarises the slope values of the included cells.
Notation and inputs
Section titled “Notation and inputs”The method takes three inputs: a slope raster in degrees, a slope RMS raster in
degrees, and an ROI shapefile with polygons in metres. Both rasters
must share one north-up grid (see Grid and cell centres). Grids
are compared exactly and never resampled (check_compatible).
Let the grid have rows and columns, so cells. Cells are indexed by column and row . Write and .
Grid and cell centres
Section titled “Grid and cell centres”The grid transform is an affine map from pixel corner coordinates
to projected metres. The method requires a north-up square grid, , and
(check_compatible). With pixel size metres, the cell centre
of column and row is
Here is the transform’s coefficient, so and are the upper-left corner of
the grid. The inverse map used for ROI vertices is and
(_check_lattice_polygon). Equation (1)
is the same map read in the forward direction.
The production grid is cells at m with transform (see Source data).
Validity predicates
Section titled “Validity predicates”Missing values are NaN after loading (load_grid). NaN fails every
predicate below, so a missing value is never treated as zero.
Slope validity, for a sample in degrees:
Both endpoints are valid (valid_slope).
Slope RMS validity, for a sample in the units of the slope RMS raster:
Zero is invalid (valid_err).
Joint validity of a cell is
These predicates are applied cell by cell. The source note that no NaN cells were observed in the production slope rasters does not replace the check (see Source data).
Inclusion at threshold τ
Section titled “Inclusion at threshold τ”For a finite threshold the inclusion predicate is
The comparison is inclusive and performed directly on the slope RMS values, with no unit
conversion (inclusion_mask). Thresholds are validated by check_taus:
the list must be non-empty, and every value must be finite and
non-negative. Zero is accepted. The default thresholds are
(DEFAULT_TAUS).
ROI membership by cell centre
Section titled “ROI membership by cell centre”Each polygon is a single Polygon with no interior rings (load_rois).
Membership of a cell is decided by its centre from equation (1). The closed
rule is
where is the interior together with the boundary. This matters because ROI
vertices lie on cell centres, so a strictly interior test would omit boundary cells
(roi_mask docstring).
The method realises equation (6) with three steps, in the order used by roi_mask:
- Lattice precondition. Every exterior vertex must lie exactly on a cell centre, with
no floating-point tolerance, and every consecutive exterior vertex pair, including the
closing edge, must differ by at most one cell in column and in row. Otherwise the
polygon fails closed with
MethodInputError(_check_lattice_polygon). - Linework validity. The polygon is passed through
shapely.make_valid(..., method="linework")(_closed_region). This keeps pinch lobes as separate parts and keeps collapsed spurs as lines, so that every ring segment is preserved. - Buffered centre-inside test. The geometry is buffered by m with mitre
joins and rasterised with
all_touched=False(roi_mask).
The buffer m is fixed in metres. It is chosen so that a centre on the
boundary falls inside the buffer, while a centre not on the boundary lies more than
from it. Under the lattice precondition, any centre not on the boundary lies at
least m from it on the 5 m grid (slope_quality module docstring).
The margin is the reason the test equals “inside or on the boundary”.
Missing values do not affect membership. A cell with a NaN slope is still a member of
and is counted in roi_cells (slope_quality module docstring).
Multi-ROI union
Section titled “Multi-ROI union”The union row uses set union over the cells, not a sum over ROIs:
A cell that lies in several ROIs is therefore counted once (roi_union).
With no ROIs, is all false. The number of cells in the union is in general less than
the sum of the per-ROI roi_cells.
Counts and the included fraction
Section titled “Counts and the included fraction”For a membership mask (one ROI or ), the method forms these counts over the masked cells only:
The included fraction is a valid-cell fraction:
is undefined, not zero, when . It is exactly zero when and no
joint cell passes (_row). It counts grid cells and is not an
area fraction. Polar stereographic cells are not equal-area, and no area correction is
applied (see Limitations).
Slope statistics of included cells
Section titled “Slope statistics of included cells”Let be the sorted included slope values, with . For a probability the inverted-CDF quantile (Hyndman and Fan type 1) is
Each quantile is an observed sample value. The code calls
numpy.quantile(..., method="inverted_cdf") (_row), which returns the smallest
observed value whose empirical CDF is at least . The quantile keys are str(p).
The mean is computed in float64:
The histogram has 45 bins of width 2 degrees with edges for . The count in bin is
The final bin is closed, so it includes (HISTOGRAM_EDGES; np.histogram). Every included value is in , so no value falls outside the bins.
When , the quantiles and mean are null and the histogram is a list of 45 zeros.
This is distinct from , which makes null (_row).
Algorithm
Section titled “Algorithm”The following block gives the per-ROI and union procedure in the order that
evaluate executes it.
Input : S (H x W), E (H x W), polygons P_0 .. P_{K-1}, taus TOutput: rows[(label, tau)] for label in {"0".."K-1", "union"} and tau in T
1 check_compatible(S, E) # same shape, transform, CRS; north-up square2 T <- check_taus(T) # finite, >= 0, non-empty3 for k in 0..K-1:4 check lattice precondition on P_k # fail closed5 M_k <- rasterise(buffer(make_valid_linework(P_k), 0.5 m, mitre), all_touched=False)6 U <- OR_k M_k # set union; all-false if K = 07 for (label, M) in [("0", M_0), ..., ("union", U)]:8 for tau in T:9 s <- S[M]; e <- E[M] # masked samples, NaN retained10 vs <- finite(s) and 0 <= s <= 9011 ve <- finite(e) and e > 012 j <- vs and ve13 inc <- j and (e <= tau)14 v <- s[inc] # included slopes only15 rows.append(counts(M, vs, ve, j, inc),16 fraction = sum(inc)/sum(j) if sum(j) > 0 else null,17 quantiles = inverted_cdf(v, P) if sum(inc) > 0 else null,18 mean = mean(v) if sum(inc) > 0 else null,19 histogram = hist(v, edges 0..90 step 2) )20 return rowsComplexity
Section titled “Complexity”Let , the number of ROIs, the number of thresholds, the number of cells in a mask and the number of included cells.
- Rasterisation. Each ROI mask is produced over the grid, so mask construction is time. The primary path (GDAL) stores one boolean mask of bytes per ROI and one for the union. The lattice check is in the number of exterior vertices.
- Per (ROI, ) statistics. The validity, joint and inclusion predicates are elementwise over the masked cells, so each pair costs .
- Quantiles and histogram. Sorting the included values costs for each pair. The histogram costs .
- Total. time, where is the mean mask size. Memory is dominated by the two float64 grids and the boolean masks.
For the production grid, cells. One float64 grid occupies
bytes, about 78.1 MiB. The two raster inputs together therefore occupy
about 156 MiB as float64 arrays before masks are formed (load_grid returns float64 copies).
Worked example (SYNTHETIC)
Section titled “Worked example (SYNTHETIC)”Grid. A grid with 5 m pixels and origin , so the transform is . Cell has centre . Columns run left to right and rows top to bottom.
Slope (degrees):
| row r \ col c | 0 | 1 | 2 |
|---|---|---|---|
| 0 | 10 | 20 | NaN |
| 1 | 30 | 95 | 40 |
| 2 | 50 | 60 | 70 |
Slope RMS (degrees):
| row r \ col c | 0 | 1 | 2 |
|---|---|---|---|
| 0 | 0.5 | 2.0 | 1.0 |
| 1 | 0.0 | 3.0 | 25.0 |
| 2 | 0.4 | NaN | 30.0 |
ROIs. Vertices are given as cell-centre indices .
- : . This is an L-shape with every edge a one-cell step. By the closed rule it contains the 8 cells . The corner is outside.
- : . It contains .
The two ROIs share three cells, , so the union has cells.
ROI 0 counts (equations 2–5, 8).
- .
- Slope valid (): , so . Cells (NaN) and () fail.
- RMS valid (): , so . Cells (, not ) and (NaN) fail.
- Joint (): , so .
| included cells | mean | non-zero histogram bins | |||||||
|---|---|---|---|---|---|---|---|---|---|
| 0.5 | 2/4 = 0.5 | 10 | 10 | 10 | 50 | 50 | 30 | bin 5: 1; bin 25: 1 | |
| 2.0 | 3/4 = 0.75 | 10 | 10 | 20 | 50 | 50 | 80/3 ≈ 26.67 | bin 5: 1; bin 10: 1; bin 25: 1 | |
| 25.0 | 4/4 = 1.0 | 10 | 10 | 20 | 40 | 50 | 30 | bin 5: 1; bin 10: 1; bin 20: 1; bin 25: 1 |
The bin index is , so slope 10 is in bin 5, 20 in bin 10, 40 in bin 20 and 50 in bin 25. Quantiles follow equation (10). For example, at the sorted values are with . Then , so , and , so .
The boundary inclusion is visible at and . Cell has , which is included at because the comparison is . Cell has , which is included at .
ROI 1 counts. , , , (cells and ).
- and : no joint cell has . So , (not null, because ), and the quantiles, mean and histogram are null, null and all zero.
- : with is included, and with is not. So , , all five quantiles equal 40, the mean is 40, and bin 20 has count 1.
Union (equation 7). . The union joint set is , so , and . The union includes in but never in , because its exceeds 25.
| statistics | |||
|---|---|---|---|
| 0.5 | 2 | 2/5 = 0.4 | same as ROI 0 at |
| 2.0 | 3 | 3/5 = 0.6 | same as ROI 0 at |
| 25.0 | 4 | 4/5 = 0.8 | same as ROI 0 at |
The union has rather than . The three shared cells are counted once, which is the set-union rule of equation (7).
Output
Section titled “Output”evaluate returns one row per (ROI, ) pair, grouped by ROI in shapefile order and then
by threshold, followed by the union rows. Each row has the fields roi, tau, roi_cells,
valid_slope_cells, valid_err_cells, joint_cells, included_cells, included_fraction,
slope_quantiles, slope_mean and slope_histogram (Row). Its canonical
JSON form has sorted keys, compact separators, allow_nan=False and a trailing LF
(canonical_json). The same inputs and thresholds therefore give the same
bytes.
Related pages
Section titled “Related pages”- Verification: oracle, hash gating and byte-identical replay.
- Source data: the frozen PGDA product and its grid.
- Limitations and FAQ: interpretation limits.
- Glossary: definitions of terms used here.