ADR-017: node-model — Tampère et al. (2011) generic first-order node model¶
Status: accepted (implemented)
Date: 2026-07-09
Deciders: DNL-models track — the general merge/diverge node (unlocks network loading)
File: docs/design/adr-017-node-model.md
Context¶
The dnl-core (adr-010) shipped the node axioms N1–N6, assert_node_axioms, and
the trivially-axiom-satisfying SeriesNode / OriginNode / DestinationNode,
but deliberately deferred the general merge/diverge solver — the NetworkLoader
raised for any interior node that was not 1-in-1-out. Without it, the DNL link
models (ctm, ltm) could only load single corridors. This sprint implements the
general node model, which is what Daganzo (1995) Part II’s network CTM needs and
what makes the loader usable on real junctions.
Decision¶
TampereNode(NodeModel)insrc/tabench/dnl/node.py, on the frozentransfer(s, r, turns, caps)interface — no new state, no signature change.caps(q_max_i·dt, already passed by the loader) are the priority weights.Oriented-capacity-proportional distribution with FIFO (the “equal priority movements” algorithm). Each active movement
(i, j)(turns[i,j] > 0,s[i] > 0) flows at ratealpha[i,j] = caps[i]·turns[i,j]; advance every active movement by the largest common stepthetabefore some incoming link exhausts itssbudget or some outgoing link saturates itsr; a saturated outgoing link then removes every movement of each competing approach that uses it (FIFO — a blocked turn holds its whole approach back in proportion); repeat until no movement is active. It terminates in at mostn_inbinding rounds. Because every movement of rowiaccruesturns[i,j]·(caps[i]·Σθ), the row is turn-proportional by construction (N4 exact), and it reduces tomin(s, r)at a series node, capacity-proportional priority at a merge, and the FIFO hold-back at a diverge.Loader default.
NetworkLoadernow instantiatesTampereNodefor every interior merge/diverge node instead of raising (an explicitnode_modelsentry still overrides it). No other loader change — it already assembles exactly(sending[ins], receiving[outs], turns, caps)per interior node.
Analytic anchors (exact fractions, machine-verified — test_dnl_tampere_node.py)¶
Merge 2→1:
s=[1,1],r=1→caps=[1,1]gives[0.5,0.5];caps=[2,1]gives[2/3,1/3](capacity-proportional).Diverge 1→2 FIFO:
s=[2],turns=[[0.6,0.4]], out-link 2 supplies0.4→phi=min(1e6/1.2, 0.4/0.8, 1)=0.5, soq=[0.6,0.4](one of two vehicles held back on both movements — the whole approach throttled).2×2, out-link A binding:
s=[10,10],caps=[6,8],turns=[[0.7,0.3],[0.4,0.6]],r=[5,100]→q=[[105/37,45/37],[80/37,120/37]], out-link A saturated at 5, rows exactly turn-proportional.N6 invariance: inflating a non-binding sending flow (a FIFO-blocked diverge approach
s: 2→200; a receiving-limited merge approachs: 5→500) leavesqbit-identical.End-to-end: the loader now certifies merge and diverge
DynamicScenarios (C1 conservation clean, C8 turn fidelity~0, capacity-proportional bottleneck sharing) that previously raised.
Alternatives considered¶
Deferring to an explicit
node_modelsarg (status quo): rejected — the point of this sprint is that the loader handles junctions by default; the override path is retained for research.Daganzo (1995) specific merge/diverge rules: the Tampère generic model subsumes them (Daganzo’s are special cases), so
TampereNodecovers the network CTM/LTM case with one algorithm;daganzo1995cellis left as its historical special-case reference.
Adversarial review¶
An adversarial review (300k+ fuzzed transfer calls + end-to-end loader runs)
confirmed termination (proven + 200k trials), N6 invariance, FIFO correctness,
determinism, and clean loader loading (a 6-link diamond diverge→merge under CTM
and LTM, never censored). It caught one real defect: a global s.sum()-scaled
tolerance let one huge approach’s float dust swallow a tiny co-incident approach’s
entire sending flow (an N5 violation at ~1e12 capacity ratios; lost flow bounded
by machine-dust, hard to trigger loudly through the loader). Fixed to per-element
tolerances relative to each row’s caps[i] and each finite column’s r[j];
regression-pinned (test_tiny_approach_not_dropped_at_extreme_capacity_ratio +
a 2000-case extreme-ratio fuzz). The benign divide-by-zero warning the review noted
was also removed (masked division).
Consequences¶
The benchmark gains network loading — ctm/ltm on arbitrary merge/diverge
networks — via one axiom-satisfying node model. All changes are additive (a new
class + loader default + tests + exports); the one loader test that asserted the
old raise is updated to assert the new default. The 570-test suite, every road/DNL
hash, and the golden Braess content hash are byte-untouched.
Sourcing¶
Tampère, Corthout, Cattrysse & Immers (2011) TR-B 45(1):289–309 is paywalled, attributed unread. The algorithm is restated from two open, read sources: Boyles/Lownes/Unnikrishnan Transportation Network Analysis Vol. I §9.6.2 (“equal priority movements”; §9.7 notes credit the desiderata and node models to Tampère et al. 2011) and Yperman (2007) PhD thesis (KU Leuven) Ch. 5 (eq. 5.6–5.12). Flötteröd & Rohde (2011) and Corthout et al. (2012, non-unique intersection flows) are attributed via Boyles’ secondary citation only, not independently read.