ADR-014: transit-strategy — Spiess & Florian (1989) optimal-strategy transit assignment¶
Status: accepted (implemented)
Date: 2026-07-09
Deciders: static-extensions track — the last Phase-1 static model (transit)
File: docs/design/adr-014-transit-strategy.md
Context¶
The remaining Phase-1 model, Spiess & Florian (1989, Transportation Research
Part B 23(2):83-102, spiess1989optimal), assigns transit demand by optimal
strategies: at each stop a passenger designates a set of attractive lines and
boards the first-arriving one, minimizing the expected total travel time
(in-vehicle time + expected wait). Under random arrivals the wait at a stop whose
attractive lines have combined frequency F is 1/F, and the passenger boards
line a with probability f_a/F. This is a convex LP (globally optimal), solved
in two label-setting/loading passes — not an iterative equilibrium, and
uncongested / frequency-based (costs are flow-independent).
It cannot be a road model. The road Network (core/scenario.py) forbids
parallel links (unique (init_node, term_node)) and its costs are flow-dependent
BPR — but the whole point of the “common lines” problem is several lines
serving the same ordered stop pair, and transit costs here do not depend on
volume. ADR-010 already rejected “extend core.Scenario with optional fields”
for the less invasive DNL case; transit is further still (it cannot even reuse
Network read-only for topology, given the parallel-arc requirement).
Decision¶
A parallel module
src/tabench/transit/(mirroringsrc/tabench/dnl/), touching no road code, so no static golden hash can move (the Braess hashcf00f411…is re-asserted intests/test_transit.py). It has its own frozen, content-hashed scenario, its own solver, and its own certifier.A directed multigraph as the network (
transit/network.py,TransitNetwork): arraystail/head/time/freq, one entry per arc, with parallel arcs allowed. A finitefreqis a boardable line (expected wait1/freq);freq = infis a deterministic arc (walk / transfer / in-vehicle continuation, no wait). This directly encodes the common-lines problem without the split-node convention.TransitDemandcarries(origin, destination, volume)triples (0-based node ids);TransitScenariowraps them with a domain-separatedcontent_hash()("tabench-transit-scenario-v1;"prefix). The emitted solution isTransitStrategy(arc volumes + per-destination labelsper-pair costs) — the transit analogue of
FlowState.
Two-pass solver (
transit/strategy.py,optimal_strategy,OptimalStrategyModel, nametransit-strategy). Per destination: (a) label-setting over arcs in nondecreasing onward costu[head]+time, adding arca=(i,j)to nodei’s attractive set iff its onward cost is strictly belowu_i, updatingu_i ← (f_i u_i + f_a(u_j+c_a))/(f_i+f_a),f_i ← f_i+f_a, with the expected wait seeded as the+1on the FIRST attractive line (u_i ← 1/f_a + (u_j+c_a)); a deterministic arc that beats the current cost closes the node. (b) Loading nodes farthest-first, splitting by frequency sharev_a = (f_a/f_i)V_i. It is standalone (its scenario is aTransitScenario, not the roadScenario), so it is NOT in the roadMODEL_REGISTRY— the same parallel-module choice the DNL core made.Certificate (P1) (
metrics/transit_gaps.py,TransitEvaluator). The harness recomputes the LP optimumZ*independently from the scenario (as the road certifier recomputes an all-or-nothing bound), and scores the emitted primal costZ_emitted— recomputed from the emitted arc volumes assum_a c_a v_a + sum_i w_i— viaoptimality_gap = (Z_emitted − Z*)/Z*(≥ 0, 0 iff optimal), the transit analogue of the relative gap. The wait is per commodity: it is per-(node, destination), so the harness certifies the PER-DESTINATION arc-volume decomposition (TransitStrategy.dest_arc_volumes; a single-destination scenario may pass the summed volumes) with per-commodity conservation, and at each node uses the LP-minimal feasible waitw_i = max_a v_a/f_a(every out-arc must satisfyf_a w_i >= v_a) — NOTV_i/F_iover a tolerance-thresholded subset, which would let a sub-tolerance sliver on a near-zero-frequency arc dodge its wait and drive the gap negative. Withw_i = max_a v_a/f_athe gap is provably ≥ 0, and a non-proportional split is feasible-but-suboptimal (a largerw_iraises the gap) rather than censored. Certification gates on demand feasibility alone (nonnegative, conserving each destination’s demand); the model’s self-reported labels/costs are never trusted. [Both a multi-destination wait-aggregation bug and the near-zero-frequency negative-gap hole were caught by adversarial review and fixed to this per-destination, max-wait form; regression-pinned intests/test_transit.py.]Analytic anchors (
transit/builtin.py, both recomputed by the closed form in the tests). The classic common-lines example, two parallel lines to the sink:common_lines_scenario: line 0(f=1/6, t=21), line 1(f=1/12, t=18)→ combinedF=1/4, optimalC* = (1 + 1/6·21 + 1/12·18)/(1/4) = 6/(1/4) = 24min (wait 4 + ride 20), demand split 2:1 (v0 = 2D/3,v1 = D/3);common_lines_unattractive_scenario: line 0(1/6, 15)alone gives 21 min, line 1(1/12, 40)has onward cost40 ≥ 21so it is excluded — all demand on line 0,C* = 21. Both satisfy the primal-dual identityZ_emitted = Z*exactly.
Alternatives considered¶
Optional field on the road
Scenario(the static house pattern used byevans/br-ue/sc-tap/vi-asym/multiclass): rejected — parallel arcs breakNetwork’s unique-link invariant and BPR costs are the wrong model; a transit hash would also have to live in the road hash space. Same reasoning as ADR-010 for DNL, only stronger.Split-node encoding (separate line-nodes per stop, so all arcs are unique): rejected as unnecessary — the multigraph with per-arc
(time, freq)models the common-lines problem directly and more legibly; deterministic in-vehicle continuations are justfreq = infarcs.Registering it in the road
MODEL_REGISTRY: rejected —TrafficAssignmentModel.solveis typed to the roadScenarioand the runner constructs road scenarios; the DNL core set the precedent that a different scenario type gets its own module/solver/certifier, exercised directly (not through the road CLI/runner).
Consequences¶
The benchmark gains its first transit model and its first uncongested,
frequency-based, hyperpath assignment — a genuinely different domain from road
UE, with a sound harness-recomputed optimality certificate and hand-derived
common-lines anchors. All changes are additive (a new module + new files only),
so every road/DNL hash and the 517-test road suite are untouched. Follow-ups: the
congested transit extension (De Cea & Fernández 1993, decea1993transit, already
tier-2 in-canon), transfer/boarding penalties, and CLI/experiment-matrix
integration for the transit scenario type.
Sourcing¶
Spiess & Florian (1989, Transportation Research Part B 23(2):83-102,
spiess1989optimal, DOI 10.1016/0191-2615(89)90034-9, Crossref-verified) is
the primary; it was already in the verified reference canon. The two-pass
algorithm, the common-lines expected-cost formula, and both analytic anchors are
hand-derived here (the expected wait 1/F, the frequency-share split, and the
attractiveness threshold c_l < C), checked for internal consistency
(Z_emitted = Z*); no number from the paper is reproduced. It must not be
confused with Spiess (1990) spiess1990gradient, the OD-adjustment gradient
method already shipped as the T2 estimator spiess.