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.

RMSE tolerance-gate workflow Transformed points and matched certified control monuments feed a residual step that yields per-point planar separations. Those reduce to one batch horizontal RMSE, which enters a tolerance gate comparing it to the agency limit. Within tolerance emits a PASS submission record; over tolerance emits a FAIL record for rework. transformed + control E/N per-point residuals RMSE ≤ τ ? tolerance gate yes PASS record no FAIL — rework

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 ii has an easting/northing residual against its matched certified monument, (EiEictrl,NiNictrl)(E_i - E_i^{\text{ctrl}}, N_i - N_i^{\text{ctrl}}). The per-point planar residual is the Euclidean separation ri=(EiEictrl)2+(NiNictrl)2r_i = \sqrt{(E_i - E_i^{\text{ctrl}})^2 + (N_i - N_i^{\text{ctrl}})^2}, and the batch horizontal RMSE is the root of the mean of those squared separations:

RMSEH=1ni=1nri2=1ni=1n[(EiEictrl)2+(NiNictrl)2]\text{RMSE}_H = \sqrt{\frac{1}{n} \sum_{i=1}^{n} r_i^{\,2}} = \sqrt{\frac{1}{n} \sum_{i=1}^{n} \left[ (E_i - E_i^{\text{ctrl}})^2 + (N_i - N_i^{\text{ctrl}})^2 \right]}

Two properties matter for compliance. This is a two-dimensional RMS — the sum of squared full separations divided by nn, not by 2n2n — so it reports the radial positional error a surveyor expects, roughly 2\sqrt{2} larger than a per-axis one-dimensional figure. And it is a root-mean-square, dominated by the largest residuals; it is not the mean error, which can hide a scattered set of large misses behind cancellation. The gate is the plain comparison RMSEHτ\text{RMSE}_H \le \tau against the agency tolerance τ\tau, commonly 0.020 m horizontal for cadastral work.

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
Summing squared residuals over both axes and dividing by 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
The mean of the residuals understates accuracy because it is not dominated by the worst points and, for signed per-axis errors, lets positive and negative misses cancel. Compliance gates are defined on the RMS, which squares before averaging so large residuals cannot be masked.
Mixing millimetres and metres
If the transformed array is in metres but the control is in millimetres — or the tolerance is entered as 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
An RMSE is only meaningful against monuments whose coordinates are themselves certified in the target realization. Checking against provisional, superseded, or wrong-epoch control produces a residual that measures the reference error, not the transformation, and a passing gate then certifies nothing.