# ADR-022: vi-due — Friesz et al. (1993) VI dynamic user equilibrium **Status:** accepted (implemented) **Date:** 2026-07-10 **Deciders:** analytical-DTA track — the dynamic USER equilibrium (closes the track) **File:** `docs/design/adr-022-vi-due.md` ## Context Friesz, Bernstein, Smith, Tobin & Wie (1993) cast the simultaneous route-AND-departure-time (SRDC) dynamic user equilibrium as a variational inequality over path departure-rate profiles: find `h*` in the volume-feasible set (`sum_p int h_p = Q` per OD) with ` >= 0`, where `Psi_p(t, h)` is the *effective delay* (traversal plus schedule-delay penalty). At a DUE every USED (route, departure-time) pair carries equal minimal effective delay and unused pairs cost at least as much — the field's standard DUE definition, first stated in this paper. The VI is loading-agnostic; the 1993 paper's own link-delay model is FIFO-safe only for affine delays, so the computable-DUE literature instantiates the same VI with the *generalized Vickrey* point-queue loading (Han, Friesz & Yao 2013 — the standing existence theory). That is exactly the loading this repo already ships (`bottleneck/`, ADR-019), which makes the faithful, certifiable benchmark unit: **the Friesz VI on parallel Vickrey-bottleneck routes** — one route with `f = 0` reduces literally to the shipped `vickrey` model, and the route axis adds the simultaneous-choice content the single-bottleneck model lacks. ## Decision 1. **Extend the `bottleneck/` module** (`due.py`) — the machinery *is* the departure-time-equilibrium machinery. `DUEScenario` (frozen, read-only, hashed under `"tabench-due-scenario-v1;"`) is the Vickrey scalars plus per-route `(f_r, s_r)` arrays. The analytic structure is exposed on the scenario: with `delta = beta*gamma/(beta+gamma)` each used route runs its own Vickrey equilibrium at the common cost level `C`, so `N_r = s_r (C - alpha f_r)/delta` and C = (delta*N + alpha * sum_U s_r f_r) / sum_U s_r with the used set `U` found by a greedy sweep in increasing-`f` order (a route is used iff `alpha f_r < C`). `due_closed_form` emits a `DUEProfile` — per-route cumulative departure curves on one shared grid (the multi-route `BottleneckSchedule`). 2. **P1 certificate** (`metrics/due_gaps.py`, `DUEEvaluator`). From `(scenario, emitted profile)` alone: censor gates (hash, finiteness, grid monotonicity, zero starts, nondecreasing rows, TOTAL-volume conservation — the per-route split is deliberately ungated: the DUE chooses it); per route, shift by `f_r`, extend the grid until the queue provably clears, simulate the deterministic point queue, and score PER TRAVELER by level inversion of both curves (the ADR-019 lesson, unchanged). The route axis introduces a genuinely new failure mode the single-route certifier cannot see: an all-on-one-route profile equalizes its own used costs while the idle route is strictly cheaper. The reference minimum therefore scans the **marginal-insertion cost of every route** (used or not) — an infinitesimal deviator joins behind `R_r(t)` travelers, exiting at `max(t + f_r, D_r^{-1}(R_r(t)))` — over the profile's grid UNION a dense harness-chosen sweep of the analytic window UNION `{t* - f_r}` (a profile cannot hide the cheap region by its choice of grid). Score: due_gap = (max used per-traveler cost - min marginal cost anywhere) / C `0` iff the discretized Friesz DUE conditions hold across BOTH choice dimensions; positive with the incentive to deviate in time OR route. Tier-B: recomputed totals (cost, expected cost, max queue, travel delay). ## Analytic anchors (machine-verified — `test_due.py`) Worked instance `friesz_two_route_scenario` (`N=6000`, routes `(f=0.2, s=3000)` and `(f=0.7, s=1500)`, `alpha=1, beta=0.5, gamma=2, t*=9`): - `C = 0.9`; split `(5250, 750)`; queue costs `(0.7, 0.2)`; windows `[7.4, 9.15]` and `[7.9, 8.4]`; total cost `C*N = 5400`; max queue 2100. The closed form certifies `due_gap = 0`. - **Both-used threshold:** routes 2 is used iff `N > alpha*s_1*(f_2 - f_1)/delta = 3750`; at `N = 3000` only route 1 runs (`C = 0.6 < alpha f_2 = 0.7`) and still certifies. - **Equal free-flow times:** the split is capacity-proportional, `C = delta*N/(s_1+s_2)`. - **Single-route `f = 0` reduction:** identical `C*` to the shipped `vickrey` model, the emitted curve matches `ue_closed_form` pointwise, and the DUE profile certifies `equilibrium_gap ~ 1e-13` under the ADR-019 `BottleneckEvaluator` — a cross-certifier consistency pin. - **False equilibria rejected:** all-on-one-route scores exactly `(1.0 - 0.7)/0.9 = 1/3` (the marginal-insertion scan); the ADR-019 burst-dump and an SO-style metered profile score large positive gaps; non-conserving/non-monotone/wrong-hash profiles are censored. During research the closed form was additionally verified by an independent complementarity-bisection solve (scalar monotone fixed point on `C` — machine precision agreement) and by discrete-event queue simulation; naive fixed-point dynamics (Smith swap, raw projection) were confirmed to CYCLE on this operator (it is non-monotone — the known instability of departure-time adjustment), and a projection iterate's ergodic average converges in aggregates while its profile is NOT an equilibrium (repo-certified gap 0.48) — exactly why the P1 certifier, not the solver's claim, is the arbiter. ## Alternatives considered - **A general-network VI-DUE with grid-based fixed-point dynamics:** rejected for this sprint — the effective-delay operator is non-monotone and the naive dynamics demonstrably cycle; shipping a solver whose convergence cannot be certified would put the benchmark's weight on luck. The parallel-bottleneck class has the exact closed form, full existence theory under GVM loading, and machine-checkable certificates; a general-network extension belongs with a route-and-time column-generation sprint of its own. - **The 1993 link-delay-model loading:** rejected — FIFO-safe only for affine delays, uncapacitated exits, and superseded by the GVM instantiation in the computable-DUE literature; the VI itself is loading-agnostic, so the GVM choice is faithful to the formulation. - **Gating the per-route split:** rejected — the DUE *chooses* the split; identical routes make it non-unique. Only total volume is conservation. ## Adversarial review Three independent attack lenses were launched; in round 1 two of them (soundness, formulation) died on an infrastructure session limit and ONLY the numerics lens completed — so the two dead lenses were re-run as a fresh round 2 against the round-1-fixed code. Full coverage was reached across the two rounds; every finding below is CONFIRMED by an executed repro and every fix is regression-pinned in `test_due.py`. **Round 1 (numerics lens).** (a) MAJOR: the certifier extended the served curve past the horizon with a 65-point *interpolated* linspace, bending the queue-clearing kink — the same piecewise-linear plan scored up to 25.6× differently depending on where its emitted grid ended. Fixed by reconstructing the point-queue served curve EXACTLY: within each segment the arrival rate is constant, so the queue either serves at `s_r` or tracks the arrival curve, with at most one interior queue-empties kink — computed analytically and inserted, plus the exact post-horizon clearing chord. (b) MINOR: the greedy used-set broke at `k = 1` when `alpha*f_1` was so large that the queue term rounded away (`C = inf`); the cheapest route is now always used. (c) MINOR: `N_r = s(C - alpha*f)/delta` cancelled catastrophically at large `f` (the closed form missed its own conservation gate); replaced by the difference form `Cq_r = (delta*N + alpha*sum s_i(f_i - f_r))/sum s_i` plus exact renormalization. (d) MINOR: degeneracy floors `alpha - beta >= 1e-9*alpha`, `gamma <= 1e9*beta` added as documented domain bounds. NOTEs: the per-step monotonicity gate was replaced by a running-max total-retraction gate (the DTA eps-accumulation family). Survived: greedy vs `2^R` brute force on 12k draws; closed-form exactness ~2.3e-10 across grid densities; cross-certifier agreement with the adr-019 evaluator. **Round 2 (soundness + formulation lenses, on the fixed code).** Both lenses independently converged on the same CRITICAL: the marginal-insertion reference scan swept `linspace(min(span_lo, t[0]), max(span_hi, t[-1]), 4001)` — a fixed COUNT over a hull the solver's own grid stretches — and the true cheap-insertion times (the queue-drain dip, where waiting for a residual queue to vanish is cheap) are kinks that were never candidates. Executed repros: a truncated-isocost two-route profile in which EVERY traveler pays exactly 2.0 while deviating into the hidden drain window costs 1.6 (a 20% improvement) certified `due_gap = 0.0` once one flat pad point at `t = 1e6` diluted the sweep; a single-ramp profile with true gap 0.765 certified −0.0001. FIXED by making `min_ref` EXACT: the marginal-insertion cost is piecewise linear in the insertion time with enumerable kinks — the profile's grid, the pullbacks `A^{-1}` of every served-curve kink level, the pullback of level `S(t*)`, and the queue-vanishing zeros of `A(t) - S(t + f_r)` — all enumerated per route, with the dense sweep demoted to belt-and-braces over the FIXED analytic window. The repros now score 0.2027 and 0.7653, invariant under pads to `1e7`. Also confirmed and fixed: MAJOR — scenarios with `N <= tol` skipped every route and certified arbitrary conserving garbage at `due_gap = -inf` (now censored explicitly); MAJOR — in-domain instances with `(beta+gamma)*ulp(t*)` comparable to `C` score the honest closed form at gap up to 1.0 because no float64 profile can resolve the equilibrium (98/1100 fuzz draws pre-fix; now rejected at construction by a conditioning gate `(beta+gamma)*eps_mach*(|t*|+window) <= 1e-9*C`); MINOR — the level sampling excluded the first/last traveler where the cost supremum is often attained (boundary levels `eps`/`n_r - eps` now sampled); NOTEs — Tier-B totals now use a grid-size-independent level count plus the switch/`t*`-crossing levels. Survived round 2: greedy vs exact `2^R` KKT enumeration on 6000 draws (zero structural disagreements, near-ties at 1e-13, thresholds at ±1e-15); an independent event-driven point-queue simulator matching the reconstructed served curves on 120 profiles to <1e-9; all 15 numeric anchors re-derived independently; gate-boundary float-dirt probes; the round-1 fixes themselves. Post-fix, the reviewers' 2200-draw property fuzz re-run: every accepted draw certifies its closed form at worst `|due_gap| = 8.4e-9` (pre-fix: up to 1.0), zero false-censors, zero negative gaps, zero crashes. ## Consequences The analytical-DTA track is complete: departure-time UE (`vickrey`), SO-DTA in both classical formulations (`merchant-nemhauser`, `lp-so-dta`), and the dynamic USER equilibrium (`vi-due`) — Wardrop's two principles, dynamic, under one P1 pattern. All changes are additive; the 652-test suite and the golden Braess hash are byte-untouched. ## Sourcing Friesz, Bernstein, Smith, Tobin & Wie (1993) "A Variational Inequality Formulation of the Dynamic Network User Equilibrium Problem," *Operations Research* 41(1):179–191, doi:10.1287/opre.41.1.179 — paywalled, attributed. The VI statement, SRDC-DUE conditions, effective-delay operator, and the link-delay-model caveats are cross-verified from open sources read in full: Han, Friesz & Yao (arXiv:1211.0898 — the GVM instantiation and existence theory; also arXiv:1211.4621), the Friesz–Kwon–Bernstein handbook chapter (nested exit-time composition, LDM history), and Peeta & Ziliaskopoulos (2001) §2 (the 1993 attribution of the DUE conditions). The parallel-route closed form is standard bottleneck-theory material (Arnott–de Palma–Lindsey lineage, attributed), derived from scratch here from the isocost conditions and machine-verified by LP-free bisection + discrete-event simulation; it is deliberately NOT attributed to the 1993 paper, which is a formulation/existence paper with no closed form. No page-precise quotes reproduced.