Topological Validation Rules for Production HD Mapping

Topological validation is the deterministic quality gate between raw LiDAR/vision-derived spatial features and a production-grade HD map. Within the Lane Geometry Extraction & Road Network Processing pipeline it sits immediately after centerline generation and before any tile is promoted to simulation or fleet deployment: latent connectivity faults, broken adjacency relationships, or geometric discontinuities propagate directly into particle-filter localization drift, path-planner oscillation, and unsafe control commands. This stage must therefore enforce hard, reproducible constraints — node-degree consistency, adjacency separation within ±0.15 m, tangent continuity, curvature bounded by the vehicle kinematic envelope, and zero dangling edges — and quarantine anything that fails rather than passing it downstream. The walkthrough below covers schema normalization, the four constraint families, the numeric QC thresholds that gate a CI build, and the failure and scale patterns that break naive implementations.

Each rule stage is a gate — features that fail are quarantined or routed to remediation:

Topological Validation Gate Pipeline Vertical flow of validation stages. An extracted lane network is normalized into a directed multigraph G equals open paren V comma E close paren, then enters a pre-validation gate checking for null geometry, self-intersection, and CRS mismatch; failures branch right to a quarantine box. Passing features run the connectivity and adjacency stage (node degree and Hausdorff less than or equal to 0.15 metres), then the geometric continuity stage (G-one tangent and curvature kappa), then a final gate testing centerline alignment and cycle closure. Passing tiles reach the CI gate to simulation and deployment; failing tiles branch right to automated remediation with a violation report. Extracted lane network Normalize to multigraph G = (V, E) · 1 mm endpoint snap Pre-validation null geom · self-int · CRS? fail Quarantine feature held pass Connectivity & adjacency node degree · Hausdorff ≤ 0.15 m Geometric continuity G¹ tangent ≤ 8° · curvature κ ≤ 1/R_min Alignment + cycle RMSE ≤ 0.10 m · closure OK? fail Remediation + violation report pass CI gate → ship simulation / fleet

Validation strategy comparison #

A topological validator can be built around several backends, each trading correctness guarantees against compute cost and integration effort. The table fixes the trade-offs so the choice is explicit rather than incidental.

Approach What it checks well Accuracy / guarantee Compute cost Best-fit use case
Graph-only (networkx degree/reachability) Connectivity, dangling edges, cycle closure Exact on the graph, blind to geometry Low — O(V+E) per tile Fast pre-gate; catches structural faults cheaply
Geometry-only (shapely predicates) Self-intersection, overlap, parallelism Exact metric, blind to traversal direction Medium — STRtree-bounded pairwise Lateral adjacency + boundary alignment
Hybrid graph+geometry Both, with attribute-aware rules Strongest; catches phantom merges Medium-high Production gate (recommended default)
Spec-conformance (OpenDRIVE/Lanelet2 XSD + rules) Schema legality, regulatory attributes Format-correct, not metrically validated Low (parse) + high (rule eval) Pre-export gate against OpenDRIVE schema validation

The hybrid backend is the production default: it reuses the lane-level topology model as its graph substrate and layers shapely metric predicates on top, so a phantom connection across a median (legal in the graph, impossible in geometry) is caught by the geometry pass while an orphaned but geometrically clean stub is caught by the graph pass.

Stage-by-stage implementation walkthrough #

Stage 1 — Graph normalization and schema enforcement #

Before any rule runs, the extracted network must be normalized into a directed multigraph G=(V,E)G = (V, E), where vertices VV encode topological breakpoints (intersections, lane terminations, merge/diverge points) and edges EE carry continuous drivable segments. Every node and edge must share a single projected coordinate reference system — typically EPSG:326xx UTM or a local engineering grid established in coordinate reference systems for AVs — so that all downstream distances are metric and free of projection distortion. Edge attributes are schema-checked (lane_id UUID, traversal_direction enum, legal_speed_limit float, centerline LineString); features with null geometry, self-intersecting lines, or a CRS mismatch are quarantined at the gate rather than admitted.

python
import networkx as nx
from shapely.geometry import LineString
from shapely.validation import explain_validity

REQUIRED = ("lane_id", "traversal_direction", "legal_speed_limit", "centerline")

def normalize(features, target_epsg=32633):
    """Build a directed multigraph; quarantine malformed features."""
    G = nx.MultiDiGraph(crs=target_epsg)
    quarantine = []
    for f in features:
        geom = f.get("centerline")
        if (not all(k in f for k in REQUIRED)
                or geom is None
                or not isinstance(geom, LineString)
                or not geom.is_valid
                or geom.is_empty):
            quarantine.append((f.get("lane_id"), explain_validity(geom) if geom else "null"))
            continue
        u = tuple(round(c, 3) for c in geom.coords[0])   # snap endpoints to 1 mm
        v = tuple(round(c, 3) for c in geom.coords[-1])
        G.add_edge(u, v, key=f["lane_id"],
                   direction=f["traversal_direction"],
                   speed=f["legal_speed_limit"], geom=geom)
    return G, quarantine

Snapping endpoints to a 1 mm grid (round(c, 3) in metres) absorbs floating-point drift so that two segments meant to share a node actually do, eliminating false dangling-edge reports later.

Stage 2 — Connectivity and adjacency constraints #

Topological integrity is governed by node-degree distributions and lateral adjacency. Intersection vertices must hold in-degree/out-degree ratios consistent with lane counts and turn restrictions; parallel lanes must keep consistent lateral separation without illegal crossings or overlapping footprints. Adjacency is verified with a spatial index (shapely.STRtree) to bound the pairwise comparison, then a Hausdorff distance flags centerlines whose separation drifts outside the urban HD-mapping tolerance of 0.15 m.

python
from shapely import STRtree
import numpy as np

def check_adjacency(G, min_sep=0.15, max_sep=4.0):
    edges = [(k, d["geom"]) for *_ , k, d in G.edges(keys=True, data=True)]
    geoms = [g for _, g in edges]
    tree = STRtree(geoms)
    violations = []
    for i, (lane_id, g) in enumerate(edges):
        for j in tree.query(g.buffer(max_sep)):
            if j <= i:
                continue
            h = g.hausdorff_distance(geoms[j])           # symmetric worst-case offset
            if h < min_sep:                              # overlapping / crossing footprints
                violations.append((lane_id, edges[j][0], "overlap", round(h, 3)))
    # dangling edges: a non-terminal node with degree 1
    for n in G.nodes:
        if G.degree(n) == 1 and not G.nodes[n].get("terminal"):
            violations.append((n, None, "dangling_edge", G.degree(n)))
    return violations

Transition validity is layered on top: a rule blocks invalid lane-to-lane handoffs (opposing-traffic merges, phantom connections across a median) by comparing direction enums and the angular delta between an inbound and outbound tangent at each shared node.

Stage 3 — Geometric continuity and curvature thresholding #

Beyond discrete connectivity the validator enforces geometric smoothness across segment boundaries, especially at merge and diverge zones. A break in first-order tangent direction (G1G^1 continuity) or an abrupt second-order curvature shift injects systematic drift into scan matching and destabilizes the lateral controller. Each edge is densely resampled and discrete curvature is computed as κ=xyyx(x2+y2)3/2\kappa = \frac{|x'y'' - y'x''|}{(x'^2 + y'^2)^{3/2}}; segments whose curvature exceeds the vehicle kinematic limit, or whose superelevation implies lateral acceleration above ay0.3ga_y \leq 0.3g for passenger vehicles, are routed to remediation.

python
import numpy as np

def curvature_profile(geom, step=0.5):
    """Discrete curvature κ sampled every `step` metres along an edge."""
    n = max(int(geom.length / step), 3)
    pts = np.array([geom.interpolate(d).coords[0]
                    for d in np.linspace(0, geom.length, n)])
    dx, dy = np.gradient(pts[:, 0]), np.gradient(pts[:, 1])
    ddx, ddy = np.gradient(dx), np.gradient(dy)
    denom = (dx**2 + dy**2) ** 1.5
    kappa = np.abs(dx * ddy - dy * ddx) / np.where(denom == 0, np.nan, denom)
    return kappa

def check_continuity(G, kappa_max=0.20, tangent_tol_deg=8.0):
    bad = []
    for u, v, k, d in G.edges(keys=True, data=True):
        kappa = curvature_profile(d["geom"])
        if np.nanmax(kappa) > kappa_max:                 # 1/min_turn_radius
            bad.append((k, "curvature", round(float(np.nanmax(kappa)), 4)))
    return bad

The curvature ceiling kappa_max is the reciprocal of the platform minimum turning radius and is shared with the methodology in Road Curvature & Superelevation Mapping, so a curvature value that passes there cannot be re-flagged here.

Stage 4 — Centerline alignment and global consistency #

The final stage verifies centerline-to-boundary alignment and global topological consistency across the tile. Boundary polylines must stay parallel to their centerline within a 0.10 m envelope, every drivable segment must terminate at a valid graph node (no orphaned geometry), and the network as a whole must have closed cycles where the road layout implies them. Cross-references against regulatory attribute databases and the batch lane attribute extraction output confirm that semantic tags survived the geometric transforms intact.

python
def check_alignment(centerline, left, right, tol=0.10, step=1.0):
    """Lateral parallelism of boundaries to their centerline."""
    n = max(int(centerline.length / step), 3)
    errs = []
    for dist in np.linspace(0, centerline.length, n):
        p = centerline.interpolate(dist)
        errs.append(abs(left.distance(p) - right.distance(p)))   # symmetric offset
    rmse = float(np.sqrt(np.mean(np.square(errs))))
    return rmse, rmse <= tol

Validation and QC automation #

Every rule emits a structured violation record — rule_id, world coordinate, severity, measured value, threshold — serialized to Parquet or GeoJSON so the failing geometry is locatable on a map and the regression is queryable in CI. The gate is binary: a tile advances to simulation only when every blocking rule reports zero violations.

Rule Metric Pass threshold Severity
Adjacency overlap Hausdorff distance ≥ 0.15 m separation blocking
Boundary alignment lateral offset RMSE ≤ 0.10 m blocking
Curvature max κ ≤ 1 / R_min blocking
Tangent continuity heading delta at node ≤ 8° blocking
Dangling edges count 0 blocking
Self-intersections count per tile 0 blocking
Cycle closure residual error ≤ 0.25 m warning
python
def ci_gate(G):
    report = (check_adjacency(G) + check_continuity(G))
    blocking = [r for r in report if r[1] != "cycle_closure"]
    assert not blocking, f"{len(blocking)} blocking topology violations: {blocking[:5]}"
    return report

Wiring ci_gate into a pre-merge job means a map version that introduces a phantom connection or an over-curved merge fails the build deterministically, the same way a unit test would, rather than surfacing as a localization incident in the field.

The four stages catch defects in a deliberate order, because a check run on an un-normalized graph reports the normalization problem rather than the defect it was written to find:

Why the Validation Stages Are Ordered A four-step dependency chain, each step stating what it assumes from the previous step, with a side note showing the false failure produced by running a later stage first. 1 · normalize + schema assumes nothing · produces canonical ids, types, winding 2 · connectivity + adjacency assumes canonical ids · produces the successor relation 3 · continuity + curvature assumes the successor relation · needs to know which pairs join 4 · alignment + cross-tile consistency assumes all three · the only stage that spans tiles run stage 3 first and you get "heading discontinuity at 4 812 joins" — every pair the un-normalized winding put back to front, and none of the real ones a stage that fails aborts the run: reporting stage 4 against a graph that failed stage 1 wastes the reviewer's time

Edge cases and failure patterns #

  • Endpoint micro-gaps. Two segments that should share a node sit 2–3 mm apart from independent fits, producing spurious dangling_edge reports. Mitigated by the 1 mm endpoint snap in Stage 1; raising the snap tolerance too far instead merges genuinely distinct nodes — keep it below the lane-width floor.
  • STRtree false negatives on long thin geometry. Bounding-box queries over near-axis-aligned centerlines return many candidates; without the max_sep buffer the adjacency pass degrades toward O(n²). Always pre-buffer the query geometry rather than widening the index.
  • Curvature spikes from undersampling. A step larger than the local radius of curvature aliases κ\kappa and emits false positives at tight intersections. Clamp step to ≤ R_min/10 in dense zones.
  • CRS drift across tiles. Adjacent tiles built in different UTM zones report apparent discontinuities at the seam; normalize every tile to a single engineering grid before cross-tile validation, consistent with managing map tile boundaries in ROS 2.
  • OpenDRIVE namespace drift. Spec-conformance rules silently skip elements when the XSD namespace changes across versions; pin the schema version and fail closed on unknown namespaces.

The thresholds this section enforces, gathered in one place with what each is derived from. None of them is a round number chosen for tidiness — each traces back to a physical or regulatory quantity:

Where Each Validation Threshold Comes From Five rows pairing a numeric gate with the physical or regulatory quantity it is derived from, so the number can be re-derived rather than copied. gatevaluederived from endpoint coincidence ≤ 0.10 m the lane-level positional budget, spent in full by the pipeline above heading continuity ±5° through lateral acceleration a passenger tolerates over one segment length centerline alignment ≤ 0.15 m lane width minus vehicle width, halved — the room actually available near-miss gap report ≤ 0.25 m twice the endpoint tolerance, so a reported gap is never a rounding artefact curvature continuity ≤ 0.10 m⁻¹/m the steering-rate limit of the vehicle class the map is built for

Performance and scale notes #

City-scale validation is dominated by the geometry pass, so the graph pre-gate should run first and short-circuit cheap structural failures before any shapely predicate executes. Partition tiles by H3 hex or GeoHash and validate non-overlapping cells in parallel with concurrent.futures.ProcessPoolExecutor; the multigraph for a single urban tile fits comfortably under a 2 GB worker ceiling, so concurrency is bounded by core count rather than RAM. Build the STRtree once per tile and reuse it across the adjacency and alignment passes instead of rebuilding per rule. Serialize violation reports as partitioned Parquet keyed by tile id so a CI run over thousands of tiles produces a single queryable dataset rather than scattered logs, and hash the input geometry so unchanged tiles skip re-validation entirely on incremental builds.

Why the stages run in this order #

The four stages are ordered by dependency rather than by importance, and running them out of order produces reports that are technically true and practically useless. Normalization comes first because every later check assumes canonical ids, types and winding. Connectivity comes second because it produces the successor relation that continuity checking needs in order to know which pairs of segments should join. Continuity comes third. Cross-tile alignment comes last because it is the only stage that spans tiles and therefore the only one that needs all three earlier results to be stable.

Run stage three first and the report reads "heading discontinuity at 4 812 joins" — every pair that un-normalized winding put back to front, and none of the real ones. A stage that fails should therefore abort the run rather than continuing: reporting stage four findings against a graph that failed stage one wastes the reviewer's time on artefacts of the earlier failure.

The invariants topology checks cannot see #

Every rule above asks a question about connectivity: does this lane have a successor, does the graph close, is the heading continuous across the join. A lane can satisfy all of them and still be undrivable, because none of them looks at the space the lane occupies.

Three geometric invariants close that gap, and each catches a defect the connectivity rules pass without comment. Width inside the band its road class allows, measured per station rather than per lane — a lane averaging a normal 3.4 m can be 2.1 m wide for eight metres in the middle, and the average hides exactly the stretch a vehicle cannot fit through. Bands have to be per class, because a 2.7 m residential lane is normal and a 2.7 m motorway lane is a defect; one global band accepts both.

No intersection between the bodies of adjacent lanes, measured on centreline-plus-width footprints and suspended inside junction extents, where turn paths are meant to cross. Applying the open-road rule inside a junction reports every junction as broken, which is the fastest way to have a check disabled.

No lane whose body intersects itself, which is what a smoothing or resampling step produces on a hairpin when the deviation budget is comparable to the local radius. The lane still has one start, one end and valid successors, so every connectivity rule passes; the body folds through itself and no vehicle can drive it.

The width check needs one process discipline to stay honest: pin the station spacing and record it with the result. A narrowing found at 1 m sampling and missed at 5 m sampling is not a fixed defect, and a gate whose spacing drifts coarser over time quietly stops finding the thing it was written for.

FAQ #

Should adjacency be measured with Hausdorff distance or a sampled lateral offset? Use Hausdorff for the overlap/crossing test because it captures the symmetric worst-case separation along the whole pair and never under-reports a local breach. For the parallelism check in boundary alignment, a station-sampled lateral offset RMSE is the right metric — it tolerates uniform width while still catching drift. The two are complementary, not interchangeable: Hausdorff alone misses a slow lateral splay that stays under the overlap floor, and offset RMSE alone misses a single sharp crossing.

Why snap endpoints to 1 mm rather than relying on exact coordinate equality? Independently fitted segments almost never share bit-identical endpoints, so exact equality reports spurious dangling_edge faults at every genuine junction. Snapping to a 1 mm grid (round(c, 3) in metres) absorbs sub-millimetre fit drift while staying far below the lane-width floor, so it never merges two distinct nodes. Raising the tolerance toward a few centimetres starts collapsing closely-spaced merge/diverge nodes and silently rewrites topology — keep it tight.

Should curvature violations block the build or only warn? Block them. A curvature spike above 1 / R_min is dynamically infeasible for the platform and injects systematic lateral-controller error, so it is a hard gate shared with the road curvature and superelevation mapping thresholds. The one caveat is undersampling: confirm the spike survives a refit with step ≤ R_min/10 before failing the tile, otherwise you are gating on an aliasing artifact rather than real geometry.

How do I keep cross-tile seams from producing false discontinuities? Normalize every tile into a single engineering grid before any cross-tile pass. Adjacent tiles authored in different UTM zones report apparent breaks at the seam that are pure projection artefacts; resolving them is the same coordinate-discipline problem handled in managing map tile boundaries in ROS 2. Validate seam connectivity only after re-projection, and treat cycle-closure residuals at seams as warnings, not blocking faults.

Up one level: Lane Geometry Extraction & Road Network Processing.