ADR-010: dnl-core — generic supply/demand dynamic-network-loading foundation¶
Status: accepted (implemented in v1)
Deciders: dnl-core design panel (3 competing designs, judged) → synthesizer
Date: 2026-07-07
File: docs/design/adr-010-dnl-core.md
Context¶
TASKS.md Phase 2 opens with dnl-core: the time-expanded / cell infrastructure
that five later sprints must share without rework — ctm (Daganzo 1994/95),
ltm (Yperman 2007), newell-kw (Newell 1993), godunov (Lebacque 1996),
and node-model (Tampère et al. 2011). The foundation has to anticipate all
five link/node paradigms, emit a P1-certifiable artifact, and stay strictly
additive — the static machinery is frozen and the golden Braess content hash
cf00f411cdccec88019979e8cfbf3d8014ba590688b285a1db667315ac96762d must remain
byte-identical.
Decision¶
Generic sending/receiving (S/R) architecture. Every link model exposes
sending(k)/receiving(k)in vehicles-per-step (dnl/link.py); node models allocate transfer flows from(S, R, turning fractions, capacities)under the Tampère first-order node axioms (dnl/node.py); time-indexed per-link cumulative curvesn_in/n_outat grid edges are the canonical emitted output (dnl/output.py::DNLOutput). CTM, LTM, Newell and Godunov all becomeLinkModelsubclasses with no interface change — Lebacque demand/supply on theFundamentalDiagramABC (dnl/fd.py), whoseenvelope_params()triangular majorant keeps every certificate a necessary condition for any concave FD.New, domain-separated scenario construct.
DynamicScenario(dnl/scenario.py) reuses the staticNetworktopology read-only and addsLinkDynamics+ piecewise-constantDynamicDemand+TimeGrid+ optionalTurningFractions. It hashes under a"tabench-dnl-scenario-v1;"domain prefix over exactly the scored content (topology, KW physics, demand, grid, turns) — the static BPR fields are ignored and unhashed (P2: hash exactly what is scored).core/scenario.pyis never edited, so no static hash can move (re-asserted in the dnl tests).P1 certification in
metrics/dnl_gaps.py. From(DynamicScenario, DNLOutput)alone the harness recomputes, censoring on any gating failure (dnl_feasible = 0, scored quantities NaN, residual columns populated — mirroringmetrics/gaps.py):C0 validity (finite, zero-start, monotone, matching grid, scenario-hash),
C1 conservation (per interior node + per step, per-origin coupling, global vehicle identity),
C2 capacity (both boundary fluxes ≤
q_max · dt),C3 storage bounds (nonnegative; ≤
κ·Lon finite-jam links),C4 free-flow causality (grid-edge-relaxed Newell upper envelope),
C6 FIFO / travel-time consistency (level-matched curve inversion),
C7 demand coupling (release ≤ cumulative demand). C5 (backward-wave envelope) is a non-gating Tier-B residual with two-level flags: standard CTM at CFL = 1 under spillback provably violates the sharp bound via numerical hole diffusion, so gating it would falsely censor a correct convergent scheme; the raw residual is always reported so no threshold hides a real violation. Scored TSTT / delay / unserved / completed / in-network are recomputed from counts, never trusted.
Sanctioned operating point = CFL = 1 on cell-aligned grids. The grid-edge relaxations (C4, and C6’s travel-time face) are exact when
L/(vf·dt)is an integer and conservative by at most one step otherwise. Gating conformance is promised only at CFL = 1; the ctm/godunov sprints must ship congested-instance conformance regressions before leaderboard use. Unaligned (CFL < 1) schemes are answerable to the always-reported raw residuals, not the gate.Test-only reference link.
dnl/_reference.py::PointQueueLink(Vickrey-lineage point queue; never registered as a benchmark model) exercises every interface without pre-empting the ctm sprint. Two analytic anchors are hand-derived AND machine-verified:single_link_dynamic_scenario(free-flow translation, TSTT = 5.0, zero delay) andbottleneck_dynamic_scenario(2-link corridor queue build/dissipate).
Alternatives considered¶
Cumulative-curve-native banks with path demand (design 1): rejected on minimality — path demand departs from the repo’s OD world and pre-loads FIFO machinery the five sprints don’t need. Its Tier-B semantics for the backward-wave bound was adopted (Decision 3).
Time-expanded vectorized engine with CSR commodity bookkeeping (design 2): rejected on foundation weight, but its envelope majorant, trapezoidal
q_capFD, node-capacity signature and PQ-spillback fixture were adopted.Extending
core.Scenariowith optional dnl fields (the static house pattern): rejected — it edits a frozen file and drags BPR bytes into DNL identity.
Known limitations & deferred hardening¶
Recorded honestly from the post-implementation adversarial reviews (2026-07-07, and the gated DNL review 2026-07-09 that landed the two items below):
C6 false-censor fixed; off-CFL completeness honestly scoped (landed 2026-07-09). C6 was reformulated as the per-entered-level (Newell travel-time) restatement of C4’s free-flow envelope: each count level’s exit-inverse time is checked against the entry curve at the grid edge at-or-after
t_out(L) − L/vf— C4’s own one-step relaxation, over levels drawn from the union of both curves. This removes the old O(dt) false-censor off CFL = 1 (a correct emission now passes). The gated review’s key correction: C6 adds no coverage over C4 — an empirical fuzz (~2M trials) and a short monotonicity argument show C4-passing implies C6-passing — so it is a redundant, independently-derived restatement of C4 (belt-and-suspenders against an implementation bug in either check, plus the per-vehicle travel-time residual), not a gap-closer, and a genuine off-CFL sub-step violation still slips past both. So C6, exactly like C4/C5, is gating-complete only at CFL = 1; off it the rawfifo_residualis the science, not the flag. The earlier “sample the union … and it closes the false-accept” plan was retired as unsupported. No shipped scenario is unaligned, so the residual gap stays latent.Turn-fraction fidelity gated at every diverge as C8 (landed 2026-07-09).
scenario.turnsis now read: at any node with ≥ 1 incoming and ≥ 2 outgoing links,d_in[out_j] == Σ_i frac[i, j] · d_out[in_i]is checked every step. Because each incoming link’s outflowd_out[in_i]is observed separately, this is decidable from aggregate single-commodity counts at multi-in nodes too — the original “underdetermined at a multi-in node” belief was wrong, and the gated review caught that the 1-in-only draft silently certified a gross merge-diverge violation. The check is node axiom N4 (Daganzo 1995 CTF), a necessary condition that holds per step even under a congested FIFO diverge (whole approach held back in ratio) and a supply-limited merge (each realized transfer already ind_out[in_i]), so it never false-censors a correct emission. It is sufficient at a 1-in diverge (aggregate ≡ the single row); at a multi-in node it is necessary but not sufficient — a wrong per-row split whose rows cross-cancel to the correct column totals is unobservable from per-link aggregate counts (the same aggregate-vs-per-commodity limit C6 FIFO carries; reserved for a per-commodity emission). No shipped scenario has a diverge, so the gate is inert until one ships.Certificate test coverage. The review found four surviving mutations (tolerance magnitude, C2 inflow-side, C1 origin-coupling, C6 plateau convention). Regression tests pinning each were added in the hardening commit and this scope was documented in the module docstring.
Open questions carried for the implementing sprints: origins-first supply convention (R1), per-link Python objects at corridor scale (R3),
OriginNodemulti-out split placeholder (R4), Budget/Trace integration deferred to the DTA-runner sprint (R6), multi-commodity per-destination emission as a strict additive superset (R7), time-varying turning fractions as a v2 hash bump (R8).
Consequences¶
The five downstream sprints implement LinkModel / NodeModel subclasses only. All
DNL work is additive; static golden hashes are provably untouched (re-asserted in
tests/). Certificates are recomputable, censor-first, and honest about what
aggregate single-commodity counts can and cannot falsify (within-link overtaking is
not observable; spillback fidelity is reported, not gated; CFL < 1 and turn fidelity
are scoped, not silently certified).
Sourcing¶
Paywalled primaries are attributed unread; equations are taken from open restatements and cross-verified; no DOIs or page-precise quotes.
Daganzo (1994) TR-B 28(4) + (1995) Part II TR-B 29(2) — CTM cell demand/supply mins and the
(vf, w, kj, q_max)trapezoidal form, cross-verified from open restatements (Yperman thesis; mirrored lecture notes). Unread.Yperman (2007) The Link Transmission Model for Dynamic Network Loading, PhD thesis, KU Leuven — open; primary source for LTM sending/receiving in cumulative form and per-step vehicle-count conventions.
Newell (1993) “A simplified theory of kinematic waves” Parts I–III, TR-B 27(4) — the two cumulative-curve envelopes (C4/C5) restated openly. Unread.
Lebacque (1996) “The Godunov scheme and what it means for first order traffic flow models” (ISTTT 13) — demand/supply Δ/Σ formalism; open restatements circulate.
Tampère, Corthout, Cattrysse, Immers (2011) “A generic class of first order node models” TR-B 45(1) — paywalled, unread; requirements N1–N6 restated from Yperman (open) and Lebacque & Khoshyaran (2005, invariance). The node-model sprint re-verifies its requirement list against whatever open restatement it implements.
Vickrey (1969) — point-queue lineage for the test-only reference link; attribution only.