Validating Routable Network Topology Before Isochrone Runs
This page solves one exact task: running a topology validation suite against a newly built routing graph, so a batch of ten thousand contours is never computed on a network that is quietly broken.
The reason this needs its own gate is that a broken graph does not fail. It returns polygons — smaller ones, or oddly shaped ones, or perfectly normal ones everywhere except the market whose bridge was severed by a clip. Nothing raises, nothing logs, and the error surfaces weeks later as a candidate site that scored badly for no reason anyone can reconstruct.
Prerequisites
- A built graph from preparing OSM network extracts for routing, not yet promoted to serving.
- Python packages:
networkxorscipy.sparse.csgraphfor component analysis,geopandasfor the store layer,httpxfor probing the engine. Install withpip install networkx geopandas httpx scipy. - The store estate with validated coordinates, so snapping can be checked against real locations rather than synthetic points.
- A fixed route set — a few hundred origin-destination pairs with known durations from the previous build.
Configuration and execution parameters
| Parameter | Value for this task | Notes |
|---|---|---|
min_giant_component_share |
0.985 | Share of edges in the largest component |
max_components_over |
50 edges | Count of non-trivial islands |
snap_tolerance_m |
250 | Store to nearest routable node |
probe_pairs |
400 | Fixed origin-destination set |
duration_tolerance_pct |
5 | Per-route change that triggers review |
contour_sample |
200 stores | Areas compared build to build |
contour_tolerance_pct |
8 | Area change that triggers review |
fail_action |
block promotion | Never a warning that a human must notice |
The two tolerance rows exist because a rebuild should change things. A gate set at zero fires every month and gets disabled; a gate set generously never fires at all. Five and eight per cent are starting points to be tuned from a few builds’ worth of observed variation.
Annotated implementation
The suite below runs against the built graph before promotion and returns a structured result the gate can act on.
from __future__ import annotations
from dataclasses import dataclass
import geopandas as gpd
import httpx
import numpy as np
from scipy.sparse import coo_matrix
from scipy.sparse.csgraph import connected_components
MIN_GIANT_SHARE = 0.985
SNAP_TOLERANCE_M = 250.0
DURATION_TOL = 0.05
@dataclass
class TopologyReport:
giant_share: float
components_over_50: int
stores_off_giant: list[str]
unsnapped_stores: list[str]
route_regressions: list[tuple[str, float]]
@property
def passed(self) -> bool:
return (self.giant_share >= MIN_GIANT_SHARE
and not self.stores_off_giant
and not self.unsnapped_stores
and not self.route_regressions)
def components(edges: np.ndarray, n_nodes: int) -> tuple[float, int, np.ndarray]:
"""Edges as an (m, 2) array of node indices. Undirected for this purpose:
a one-way pair still means the two nodes are in the same component."""
data = np.ones(len(edges), dtype=np.int8)
graph = coo_matrix((data, (edges[:, 0], edges[:, 1])), shape=(n_nodes, n_nodes))
n_comp, labels = connected_components(graph, directed=False)
sizes = np.bincount(labels)
giant = sizes.argmax()
return sizes[giant] / n_nodes, int((sizes > 50).sum() - 1), labels == giant
def check_stores(stores: gpd.GeoDataFrame, engine: str,
in_giant: np.ndarray) -> tuple[list[str], list[str]]:
"""Every store must snap within tolerance AND land in the giant component."""
unsnapped, off_giant = [], []
with httpx.Client(base_url=engine, timeout=20.0) as client:
for row in stores.itertuples():
lon, lat = row.geometry.x, row.geometry.y
r = client.get(f"/nearest/v1/driving/{lon},{lat}")
r.raise_for_status()
wp = r.json()["waypoints"][0]
if wp["distance"] > SNAP_TOLERANCE_M:
unsnapped.append(row.store_id)
elif not in_giant[wp["nodes"][0]]:
off_giant.append(row.store_id)
return unsnapped, off_giant
Treating the graph as undirected for component analysis is deliberate. A pair of one-way streets forming a loop is perfectly routable, and a strict strongly-connected-component analysis would flag legitimate one-way systems as unreachable. Directed reachability matters and belongs in the route probes, where it can be measured against a known answer rather than inferred from structure.
Failure modes and debugging
Stores that snap far from the road. A store snapping two hundred metres away is usually a service road that the profile excluded rather than a bad coordinate — retail parks are full of them. The tell is a cluster of such stores in similar formats; checking whether the profile admits service ways resolves most of them at once.
A giant-component share that is high and still wrong. A network can be 99 per cent connected and have the missing one per cent be a whole market. Reporting the share alone hides that; reporting components by their geographic extent, or simply checking store membership, finds it. This is why the store check exists alongside the structural one.
Probe routes that all fail. Almost always the engine pointing at the previous graph rather than the new one, which is a deployment question rather than a topology one. Querying the engine for its build version at the start of the suite eliminates a surprising amount of confusion.
A tolerance that never fires. If the gate has passed on every build for a year, it may be well-tuned or it may be measuring nothing. Feeding it a deliberately damaged graph once a quarter — a clip with no buffer, a profile with turn restrictions disabled — proves it still rejects, in the same spirit as every other assertion in the pipeline.
Verification
- Damage a graph on purpose and confirm the suite blocks it. A clip with zero buffer is the easiest damage to produce and exercises the component and store checks together.
- Confirm the engine under test is serving the candidate build, by asserting its reported version before any probe runs.
- Check the probe set still covers the estate. Routes chosen two years ago may all sit in markets that have since become a small share of the business; refreshing the set annually keeps it representative.
- Record every run’s measurements, not only its verdict. The trend in giant-component share across builds is what shows a clip slowly becoming inadequate as markets expand.
Frequently Asked Questions
Should validation run against the graph files or against a running engine?
Both, for different checks. Component analysis reads the graph structure directly and does not need a service; store snapping and route probes need an engine because they exercise the query path, which has its own failure modes — a graph that loads fine and a service configured to a different profile look identical structurally. Running a temporary engine instance against the candidate build gives both without touching production.
How many probe routes are enough?
Enough to cover every market and every road class the business depends on, which in practice means a few hundred rather than a few thousand. What matters more than the count is the spread: routes that all sit within one metro tell you nothing about a rural rebuild, and routes chosen at random tend to over-represent wherever the stores are densest. Choosing them deliberately — a handful per market, spanning short urban and long interurban trips — is a one-off exercise with a long payoff.
What if a genuine road change fails the gate?
Then the gate did its job and the answer is to promote deliberately rather than to loosen the threshold. A motorway opening changes durations across a whole market, and that is exactly the kind of change the analytics team should hear about before catchments move. Recording the override — what changed, who approved it, which routes moved — keeps the gate meaningful for the next build.
Can this suite replace the checks inside the contour pipeline?
No, they answer different questions. This suite asks whether the graph is sound; the contour checks ask whether an individual polygon is plausible — that it contains its origin, that its area is in a sensible band, that nested bands nest. A sound graph still produces bad contours from bad inputs, and a good contour can be produced from a graph that is broken somewhere else. Both layers are cheap and neither subsumes the other.
Who should receive the validation report?
Whoever can act on each part of it. The component and snapping results belong to whoever owns the extract and the profile; the route and contour regressions belong to the analytics team, because they describe how the world changed rather than whether the build worked. Sending the whole report to one channel produces a document nobody reads; splitting it along those lines produces two short messages that each land with someone who can do something.
Can the suite run against a subset to keep it fast?
The structural checks cannot usefully be sampled — a component analysis on part of the graph is a different question — but the store, route and contour checks can, and sampling them well is what keeps the suite to a few minutes. Stratify the sample so every market and density band is represented, keep it fixed between builds so comparisons are like-for-like, and refresh it only when the estate changes materially. A rotating random sample makes every build’s numbers incomparable with the last, which defeats the purpose.
Related
- Preparing OSM Network Extracts for Routing — the build this suite gates.
- Troubleshooting Disconnected Road Networks in Rural Areas — diagnosing what the component check reports.
- Automating Monthly OSM PBF Refresh for Routing Engines — where the gate sits in the schedule.