A sampled elevation profile is a vector; an edge weight needs a scalar. The function that reduces one to the other looks trivial and is where a surprising amount of ETA accuracy is won or lost, because different reductions describe genuinely different physical quantities. This page covers that reduction as the aggregation stage of elevation and terrain data integration, sitting inside the OSM Graph Architecture & Network Modeling pipeline and feeding the kinematics in speed profile calibration for heavy vehicles.
The profiles arrive from sampling SRTM and Copernicus DEM along edges as arrays with possible NaN gaps. What leaves this stage is a signed percentage per direction plus a confidence flag.
When to use this approach
Use climb-weighted aggregation rather than net rise when:
- The vehicle loses energy on every climb. Any internal-combustion profile, and any electric profile whose regeneration efficiency is below about 90 %, which is all of them.
- Edges are long relative to terrain variation. A 600 metre edge across rolling ground can have a net rise of zero and three separate 6 % pitches. Net rise reports flat.
- The cost model has a grade cutoff. A cutoff applied to net rise never fires on rolling terrain, so the constraint it encodes — a loaded artic that cannot sustain more than 10 % — is silently unenforced.
There is a third option worth knowing about and rarely worth using: the maximum pitch anywhere in the profile. It answers a genuine question — can this vehicle physically climb this edge at all — but as a cost input it is far too pessimistic, because a single 12 % ramp of twenty metres would price a two-kilometre edge as though the whole thing were steep. Where a maximum-pitch constraint genuinely matters, such as an articulated vehicle with a known gradeability limit, store it as a separate attribute alongside the aggregated grade rather than replacing it. The cost function then multiplies by the aggregate and hard-filters on the maximum, which is the behaviour the physics actually implies.
Net rise remains correct for elevation-difference questions: what altitude does this route end at, how much potential energy does the vehicle gain. Those are real questions, just not the one an edge weight answers.
Implementation
# requires: numpy (pip install numpy)
import numpy as np
from dataclasses import dataclass
@dataclass(frozen=True)
class GradeResult:
grade_pct: float
inferred: bool # profile needed void bridging
clamped: bool # value hit the plausibility bound
def aggregate_grade(
elev: np.ndarray,
length_m: float,
*,
mode: str = "climb_weighted",
clamp_pct: float = 25.0,
) -> GradeResult:
"""Reduce an elevation profile to a signed grade with provenance flags."""
if length_m <= 0 or elev.size < 2:
return GradeResult(0.0, inferred=True, clamped=False)
good = ~np.isnan(elev)
if good.sum() < 2:
return GradeResult(0.0, inferred=True, clamped=False)
inferred = bool((~good).any())
if inferred:
idx = np.arange(elev.size)
elev = np.interp(idx, idx[good], elev[good])
net = float(elev[-1] - elev[0])
if mode == "net":
raw = 100.0 * net / length_m
else:
# Sign from net direction, magnitude from total vertical movement
sign = 1.0 if net >= 0 else -1.0
raw = sign * 100.0 * float(np.abs(np.diff(elev)).sum()) / length_m
clamped = abs(raw) > clamp_pct
value = float(np.clip(raw, -clamp_pct, clamp_pct))
return GradeResult(round(value, 3), inferred=inferred, clamped=clamped)
Two design choices in there are worth naming. The result carries flags rather than only a number, so a consumer can distinguish a directly measured 8 % from an 8 % that was interpolated across a void or clipped down from 400 %. And the clamp is symmetric and generous — 25 % is well above any public road — so it only ever catches artefacts, never real terrain.
Deriving the reverse direction is one line, and it should be exactly one line:
def directed_grades(fwd: GradeResult) -> tuple[GradeResult, GradeResult]:
"""Forward and reverse grade for the same physical segment."""
rev = GradeResult(-fwd.grade_pct, fwd.inferred, fwd.clamped)
return fwd, rev
Recomputing the reverse from a reversed profile is the alternative and it is worse, because floating-point summation is order-dependent and the two values can differ in the last digits. That difference is harmless numerically and poisonous operationally: it means the invariant grade_fwd + grade_rev == 0 no longer holds exactly, so the assertion that would have caught a genuine sign bug has to be loosened to a tolerance, and a real defect can hide inside it.
Key parameters and tuning
| Parameter | Recommended value | Notes |
|---|---|---|
mode |
climb_weighted |
Use net only for potential-energy questions, not for cost |
clamp_pct |
25.0 | Above any real public road; catches artefacts without touching terrain |
| Void bridging | linear over the gap | Only sound for isolated cells; a mostly-void profile should return zero and flag |
| Minimum valid samples | 2 | Below this there is no gradient to compute, only a guess |
| Rounding | 3 decimal places | Below the DEM’s own vertical accuracy; more digits imply precision that is not there |
| Reverse derivation | negation only | Never recompute; the exact-zero invariant is worth more than the symmetry |
| Flag propagation | carry to the edge attribute | Lets a cost model discount inferred grades rather than trusting them equally |
Integration points
Into the cost function. Grade enters the composite weight described in configuring edge weights for freight logistics as both a multiplicative penalty and, above a cutoff, a hard filter. The inferred flag is worth consulting at the cutoff specifically — excluding a road from a heavy-vehicle profile on the strength of an interpolated grade is a harsher decision than the data supports.
Into EV energy models. The climb-weighted magnitude and the net rise are both needed for electric profiles: total climb drives consumption, net rise drives what regeneration can recover. Storing both costs one extra float per edge and saves recomputing the profile later, as the model in modeling battery-range constraints for EV fleets needs the two separately.
Into structure correction. Edges flagged bridge or tunnel should have their profile replaced before reaching this function, per handling DEM voids and bridge-tunnel artefacts. Aggregating first and correcting afterwards does not work, because the correction operates on the profile shape rather than on the scalar.
Validation checklist
- Forward and reverse sum to exactly zero. Not approximately — exactly. Any tolerance in this assertion is a place a sign bug can live.
- A synthetic ramp returns its known grade. Feed a linearly increasing profile of known rise and length; the result must match to the rounding precision in both modes.
- A symmetric rolling profile returns zero under
net. And a non-zero value underclimb_weighted. If both return zero, the mode switch is not wired up. - Clamped count is small and stable. A few per hundred thousand edges is normal at borders and coastlines. A rising trend means DEM coverage is degrading.
- Flags reach the edge attributes. Assert that
inferredandclampedare persisted alongside the grade. A flag computed and dropped is worse than never computing it, because the code implies a safety that is not there.
Related
- Elevation and terrain data integration — the full pipeline from DEM tiles to directed grade attributes
- Sampling SRTM and Copernicus DEM along edges — producing the profiles this aggregation consumes
- Handling DEM voids and bridge-tunnel artefacts — the corrections that must run before aggregation
- Speed profile calibration for heavy vehicles — the mass-dependent response that turns a grade into a speed