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

  1. 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 curves n_in / n_out at grid edges are the canonical emitted output (dnl/output.py::DNLOutput). CTM, LTM, Newell and Godunov all become LinkModel subclasses with no interface change — Lebacque demand/supply on the FundamentalDiagram ABC (dnl/fd.py), whose envelope_params() triangular majorant keeps every certificate a necessary condition for any concave FD.

  2. New, domain-separated scenario construct. DynamicScenario (dnl/scenario.py) reuses the static Network topology read-only and adds LinkDynamics + piecewise-constant DynamicDemand + TimeGrid + optional TurningFractions. 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.py is never edited, so no static hash can move (re-asserted in the dnl tests).

  3. 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 — mirroring metrics/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; ≤ κ·L on 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.

  4. 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.

  5. 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) and bottleneck_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_cap FD, node-capacity signature and PQ-spillback fixture were adopted.

  • Extending core.Scenario with 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 raw fifo_residual is 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.turns is 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 outflow d_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 in d_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), OriginNode multi-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.