RMSE-to-Agency-Submission Workflow in Python
The number that decides whether a transformed cadastral batch may be filed is its horizontal root-mean-square error against certified control monuments, and turning that statistic into a pass/fail submission record is the accuracy gate inside compliance report generation for agency submission, part of Batch Transformation & Automation for Cadastral Coordinate Pipelines. This page computes the horizontal RMSE of transformed points against certified monuments, compares it to the agency’s tolerance gate, and emits the structured pass/fail record the submission carries.
Figure — matched points reduce to residuals, then a batch RMSE the gate turns into a submission verdict.
The Statistic and the Gate
Each transformed point
Two properties matter for compliance. This is a two-dimensional RMS — the sum of squared full separations divided by
Complete Runnable Implementation
The function below computes the per-point residuals, the batch RMSE, and the worst offender, then packages the tolerance verdict into a structured submission record with units carried explicitly.
from __future__ import annotations
import json
import logging
from dataclasses import dataclass, asdict
import numpy as np
logger = logging.getLogger("rmse_submission")
@dataclass(frozen=True)
class SubmissionRecord:
"""Pass/fail accuracy record for an agency submission."""
n_points: int
rmse_h_m: float # batch horizontal RMSE, metres
max_residual_m: float # largest single planar residual, metres
worst_index: int # row of the worst offender, for diagnosis
tolerance_m: float # agency gate, metres
passed: bool
def to_json(self) -> str:
return json.dumps(asdict(self), sort_keys=True, indent=2)
def rmse_submission(
transformed_en: np.ndarray, # (n, 2) easting, northing in metres
control_en: np.ndarray, # (n, 2) certified monument E/N in metres
tolerance_m: float = 0.020,
) -> SubmissionRecord:
"""Compute horizontal RMSE against certified control and gate it.
Both arrays must be metre-scale easting/northing pairs in the same CRS.
Returns a SubmissionRecord; the caller inspects `passed` before filing.
"""
if transformed_en.shape != control_en.shape:
raise ValueError("transformed and control arrays must match in shape")
if transformed_en.ndim != 2 or transformed_en.shape[1] != 2:
raise ValueError("expected (n, 2) easting/northing arrays in metres")
diff = transformed_en.astype(np.float64) - control_en.astype(np.float64)
per_point = np.hypot(diff[:, 0], diff[:, 1]) # full planar residual
# Two-dimensional RMS: mean of squared separations, then root (÷ n, not 2n).
rmse = float(np.sqrt(np.mean(per_point ** 2)))
worst = int(np.argmax(per_point))
record = SubmissionRecord(
n_points=int(transformed_en.shape[0]),
rmse_h_m=round(rmse, 4),
max_residual_m=round(float(per_point[worst]), 4),
worst_index=worst,
tolerance_m=tolerance_m,
passed=rmse <= tolerance_m,
)
logger.info("rmse_submission %s", json.dumps(asdict(record)))
return record
Parameter Reference
| Name | Type | Units | Valid range | Notes |
|---|---|---|---|---|
transformed_en |
np.ndarray |
metres | shape (n, 2) |
transformed easting/northing; must be projected, not degrees |
control_en |
np.ndarray |
metres | shape (n, 2) |
certified monument coordinates, same CRS and axis order |
tolerance_m |
float |
metres | > 0 | agency gate; 0.020 m is a common cadastral horizontal limit |
rmse_h_m |
float |
metres | ≥ 0 | two-dimensional batch RMSE, the headline gate figure |
max_residual_m |
float |
metres | ≥ 0 | worst single point; a lever for diagnosing an outlier |
worst_index |
int |
row index | 0 … n−1 | which monument drove the worst residual |
passed |
bool |
— | — | False blocks the submission and routes to rework |
Worked Example
A four-point batch against certified monuments, with residuals a few millimetres each, clears a 20 mm gate.
transformed = np.array([[500000.006, 5000000.004],
[500100.005, 5000100.007],
[500200.008, 5000200.003],
[500300.002, 5000300.009]])
control = np.array([[500000.008, 5000000.006],
[500100.007, 5000100.010],
[500200.010, 5000200.007],
[500300.004, 5000300.012]])
rec = rmse_submission(transformed, control, tolerance_m=0.020)
print(rec.rmse_h_m, rec.max_residual_m, rec.passed)
# 0.0036 0.005 True
The batch RMSE is about 3.6 mm and the worst point about 5 mm, both well inside the gate, so passed is True and the record is ready to attach to the submission.
Validation Check
Gate the verdict and confirm the statistic is a genuine RMS — never below the mean of the residuals — before the record leaves the pipeline.
diff = transformed - control
per_point = np.hypot(diff[:, 0], diff[:, 1])
assert rec.rmse_h_m >= float(np.mean(per_point)) - 1e-9, "RMS below mean is impossible"
assert rec.rmse_h_m <= rec.tolerance_m, "batch exceeds agency tolerance"
assert 0 <= rec.worst_index < rec.n_points, "worst index out of range"
Common Mistakes
Reporting a one-dimensional RMSE for a two-dimensional position
2n gives a per-axis figure that is about a factor of √2 smaller than the radial horizontal RMSE an agency expects. Divide the sum of squared full separations by n, as np.mean(per_point ** 2) does here.Using the mean error instead of the root-mean-square
Mixing millimetres and metres
20 meaning millimetres against a metre-scale RMSE — the gate compares incommensurable quantities and passes or fails nonsensically. Keep every array and the tolerance in metres, and assert the unit at the boundary.Comparing against uncertified or stale control
Related
- Compliance report generation for agency submission — the parent report this pass/fail record feeds.
- Generating audit hashes for transformation batches — the integrity digest that sits beside this accuracy record.
- Validating datum alignment with control points — establishing the certified control this workflow measures against.
- Least-squares adjustment for control networks — diagnosing an outlier the worst-residual field flags.
- Batch Transformation & Automation for Cadastral Coordinate Pipelines — the parent reference on automating and certifying batch transformations.