ADR-019: vickrey — Vickrey (1969) single-bottleneck departure-time equilibrium

Status: accepted (implemented) Date: 2026-07-09 Deciders: analytical-DTA track — the first departure-time-equilibrium model File: docs/design/adr-019-vickrey.md

Context

The analytical-DTA track opens with Vickrey’s (1969) single-bottleneck model — the canonical departure-time equilibrium. N travelers each choose a departure time to trade point-queue delay at a bottleneck of capacity s against schedule delay relative to a desired arrival t* (early penalty beta, late gamma, travel-time value alpha, with 0 < beta < alpha). This is a different paradigm from the repo’s route-choice and link-loading models — there is no network topology, just six scalars and a departure-rate decision — so it gets its own parallel module, exactly as transit/ and dnl/ did (ADR-014/010).

Decision

  1. A parallel module src/tabench/bottleneck/ touching no road/DNL/transit code (the golden Braess hash cf00f411… re-asserted in the tests). BottleneckScenario (scenario.py) is the six scalars, frozen and content-hashed under a "tabench-bottleneck-scenario-v1;" domain prefix.

  2. Closed-form UE and SO (solve.py), each emitting a BottleneckSchedule — a cumulative departure curve R(t) on a time grid, the P1-certifiable artifact (analogue of FlowState/TransitStrategy). The UE queue-builds at r_early = s·alpha/(alpha-beta) on [t1, t_n] then queue-dissipates at r_late = s·alpha/(alpha+gamma) on [t_n, t2], with t1 = t* - C*/beta, t2 = t* + C*/gamma, t_n = t* - C*/alpha, and C* = beta·gamma/(beta+gamma)·N/s. The SO meters departures uniformly at s over the same window (no queue).

  3. P1 certificate (metrics/bottleneck_gaps.py, BottleneckEvaluator). From (scenario, emitted R(t)) alone — never the solver’s r_early/t1/C* provenance — the harness simulates the deterministic point queue n_{k+1} = max(0, n_k + dR_k - s·dt), recomputes each used departure time’s generalized cost c(t) = alpha·T + beta·[t*-(t+T)]+ + gamma·[(t+T)-t*]+ (T = n/s), and scores equilibrium_gap = (max c - min c)/C* over used departure times — 0 iff no traveler can improve by shifting (a user equilibrium), positive otherwise. Feasibility gates on conservation (R ends at N), monotonicity, and the scenario hash. Total/expected cost, max queue, and total travel delay are recomputed and reported.

Analytic anchors (machine-verified — test_bottleneck.py)

Worked instance N=6000, s=3000, alpha=1, beta=0.5, gamma=2, t*=9:

  • C* = 0.8; window t1=7.4, t2=9.4 (width N/s = 2); peak t_n=8.2; r_early=6000, r_late=1000; max queue 2400; UE total 4800, mean 0.8.

  • The UE certifies equilibrium_gap = 0 (every used departure time yields exactly C*), while the SO certifies a positive gap (uniform metering spreads schedule delay, so it is not a departure-time equilibrium) with no queue and total 2400.

  • Price of anarchy = 2 for any beta, gamma (UE_total / SO_total = 2, a general bottleneck result, regression-fuzzed over random penalties — not just the symmetric case).

  • A schedule perturbed off the equilibrium curve certifies a positive gap; non-conserving / non-monotone / wrong-hash schedules are censored.

Alternatives considered

  • Reusing the DNL point queue (dnl/_reference.py::PointQueueLink): rejected — that is a numerical loading kernel driven by a grid loop; Vickrey is an analytic departure-time equilibrium over a decision (the departure rate), with a schedule-delay objective the DNL machinery has no notion of. The certifier’s own tiny point-queue recomputation keeps the module self-contained.

  • Trusting the solver’s C*/rates: rejected (P1) — the certifier recomputes the queue and costs from the emitted curve, so it certifies any departure-time schedule (a future heuristic/learned solver included), not just the closed form.

Adversarial review

The review confirmed the closed form (c(t) C* re-derived algebraically for any alpha,beta,gamma), the PoA-2 result at extremes, the feasibility gates, hash, and isolation — and caught two real certifier bugs, both fixed:

  • CRITICAL false-accept: the first certifier sampled each step’s cost at the start-of-step queue, so a “burst dump” schedule (all mass at the window boundaries) certified gap 0 despite a total cost 2.1× the true UE — the intra-step congestion was never seen. Fixed by scoring per traveler: invert both the arrival curve A(t)=R(t) and the bottleneck-served curve D(t) = min(A, D_prev + s·dt) at each count level to get each traveler’s actual departure and exit times (the level-based approach the DNL/transit certifiers use).

  • False-censor: a fixed eps = tol·N “used-step” mask censored legitimate schedules on very fine grids / tiny N. Removed — level sampling needs no such threshold.

Both are regression-pinned (test_burst_dump_is_not_a_false_equilibrium, test_fine_grid_and_small_n_not_false_censored).

Consequences

The benchmark gains its first departure-time equilibrium and the entry point of the analytical-DTA track (Merchant–Nemhauser, Friesz VI-DUE, Ziliaskopoulos LP to follow). All changes are additive (a new module + a new certifier + tests), so the 592-test suite, every road/DNL/transit hash, and the golden Braess content hash are byte-untouched.

Sourcing

Vickrey (1969) “Congestion Theory and Transport Investment,” AER P&P 59(2):251-260 (no DOI; JSTOR 1823678) — attributed. The bottleneck equilibrium closed form is standard textbook; taken from Boyles Transportation Network Analysis Ch. 10 (open, the repo’s own tna_ch10_tdsp.md grounding) and independently re-derived from the first-order equal-cost conditions and machine-verified against a discrete-event queue simulation. Arnott–de Palma–Lindsey (1990, JUE 27(1)) is the standard general-alpha,beta,gamma reference (attributed, not full-text-read). No DOIs or page-precise quotes reproduced.