ADR-007 — Combined trip distribution + assignment (Evans 1976): a P1-pure certificate¶
Status: accepted (shipped in v1)
File: docs/design/adr-007-combined-distribution-assignment.md
Context¶
Every fixed-demand task fixes the whole OD matrix; the elastic task (ADR-005) makes each
pair’s total demand a pointwise function of its own cost. Combined trip distribution +
assignment (Evans 1976) goes one step further: only the trip-end margins are fixed — the
productions O_i and attractions D_j — and the entire OD matrix d_ij is endogenous,
distributed across pairs by a doubly-constrained gravity model at the equilibrium travel
costs. Distribution and assignment, historically two separately-iterated models, become one
convex program (Evans 1976; Sheffi, Urban Transportation Networks 1985 ch. 6; Boyles,
Lownes & Unnikrishnan, Transportation Network Analysis §6):
min_{x,d} Σ_a ∫_0^{x_a} t_a(w) dw + (1/β) Σ_ij d_ij (ln d_ij − 1)
s.t. Σ_j d_ij = O_i, Σ_i d_ij = D_j, d ≥ 0, x = assign(d)
Its stationarity conditions give both the doubly-constrained gravity
d_ij = A_i B_j exp(−β u_ij) (u_ij = equilibrium OD cost, A/B = Furness balancing
factors) and Wardrop route equilibrium of d. This is a genuinely new problem class,
and — like elastic demand — it collides with the harness’s fixed-OD assumptions. ADR-005
anticipated it (“Deferred: a combined distribution–assignment task (Evans 1976) that reuses
this machinery”); this ADR is that reuse.
Sourcing (dual-verified against open textbooks)¶
The primary — Evans (1976), Derivation and analysis of some models for combining trip
distribution and assignment, Transportation Research 10(1), DOI
10.1016/0041-1647(76)90100-3 (canon key evans1976derivation, tier 1) — is paywalled; its
abstract and the “Evans algorithm” convergence result are attributed unread. The formulas
implemented here (the combined objective, the partial-linearization algorithm, the
doubly-constrained gravity subproblem, the Furness/IPF balancing) were recovered from two
primary-accessible textbooks read in full and cross-checked formula-for-formula: Sheffi
(1985) ch. 6 and Boyles et al. TNA §6. Wilson’s entropy-maximising gravity model is the
second intellectual parent (via the (1/β) Σ d(ln d − 1) term) but has no bibkey in the
canon and is noted, not cited. Canon keys already present: evans1976derivation,
beckmann1956studies, florian1974method, sheffi1985urban.
Decision 1 — Represent the fixed margins as content-hashed scenario data¶
CombinedDemand(productions, attractions, beta) (core/scenario.py) carries the fixed
trip-end margins O_i, D_j and the single shared gravity dispersion β. It validates
Σ O = Σ D (doubly-constrained feasibility) and β > 0, and exposes the reusable
gravity(od_cost) — the doubly-constrained Furness / iterative-proportional-fitting
recursion over the interzonal support (i ≠ j, O_i > 0, D_j > 0), made deterministic
(fixed tolerance/iteration cap, exact-row final rescale) so the solver and the harness
recompute byte-identical demand from the same costs.
Scenario.combined_demand is optional and hashed only when set, appended after the SUE
and elastic fields, so every prior scenario keeps its byte-identical hash — the golden Braess
hash cf00f411… is asserted preserved (test_golden_braess_hash_preserved). Combined is
mutually exclusive with the SUE fields and elastic_demand (each makes the OD demand
non-fixed in an incompatible way). The ordinary Demand.matrix carries the free-flow
gravity distribution — the deterministic uncongested-equilibrium OD matrix, a meaningful
reference with the right margins and full support.
Decision 2 — Solve with Evans’ partial-linearization Frank-Wolfe¶
evans (EvansCombinedModel, paradigm static_ue_combined) linearizes only the assignment
(Beckmann) term and keeps the entropy term exact. At iterate (x, d) the subproblem
min_y Σ_ij u_ij y_ij + (1/β) Σ_ij y_ij(ln y_ij − 1) s.t. margins
has the closed-form solution y = gravity(O, D, β, u) (the doubly-constrained Furness
balancing); its all-or-nothing assignment w gives the descent direction, and an exact
Brent line search on the combined objective — root of
g(α) = t(x + α dx)·dx + (1/β) Σ ln(d + α dd)·dd, nondecreasing on [0,1] — sets the step.
x is kept a feasible assignment of d throughout (x₀ = AON(gravity at free-flow), and
every update advances x and d by the same step to convex combinations), so the
route-equilibrium gap of d stays ≥ 0. Only real link flows are emitted; the auxiliary
gravity y is both the FW subproblem solution and the demand the harness recomputes.
Decision 3 — A P1-pure certificate: recompute the demand, then score¶
The harness knows O, D, β (content-hashed), so from the emitted link flows v it recomputes
everything (metrics/gaps.py, gated on scenario.combined_demand), the elastic recipe with
the gravity in place of D(u):
t = link_cost(v)
u = od_cost_matrix(t) # per-OD shortest-path cost over the gravity support
d* = gravity(u) # doubly-constrained gravity demand at those costs
relative_gap = (v·t − Σ u·d*) / (v·t) # route equilibrium given d*
node_balance_residual = ‖ balance(v) − div(d*) ‖_∞ # demand-consistency + conservation
realized_demand = Σ d* (= Σ O_i) # scored quantity
Both are pure functions of (v, scenario) — no self-report is trusted (P1) — and the model’s
self-reported relative_gap is defined to equal the scored gap, so the honesty diff is
~0. The skim is driven by the gravity support (the fixed margins), not the reference
matrix’s nonzeros, so a reference entry that underflowed to zero can never desync the solver
from the certificate.
The feasibility gate — and an honest bound on soundness (adversarial-review finding)¶
As for elastic (ADR-005), there is no given matrix, so through-node conservation alone is not
a sufficient gate; the gate is node_balance(v, d*) (a phantom flow routing zero OD demand
disagrees with d* > 0 and is censored — regression-tested), plus the negative-excess guard
(SPTT > TSTT censored). One property is specific to combined demand and must be stated
plainly: the doubly-constrained gravity always reproduces the margins O, D, so
node_balance(v, d*) only certifies that v routes the correct margins — it carries no
information about the OD-pair distribution (unlike elastic, where d* = D(u) has cost-varying
margins). The only distributional teeth is therefore the negative-excess guard.
This is NOT a full soundness guarantee, and we do not claim one. An adversarial review
constructed the exact counterexample: on a cost-degenerate instance where every
margin-feasible flow induces the same uniform OD-cost skim u, all such flows satisfy
Σ u·d* = u·T = v·t, so a wrong-distribution flow (e.g. “dump all trips on the cheapest
links”) certifies with feasible = 1 and relative_gap = 0 even though it is not the
equilibrium. We verified this is exactly the aggregate-vs-per-OD limitation the whole harness
already documents (single-commodity node balance is necessary, not sufficient, for
multi-commodity feasibility): the fixed-demand certificate admits the identical false
positive on the same network — it is inherited, not introduced by Evans. A fully sound
link-flow-only certificate is impossible here, because distinct margin-feasible distributions
that induce the same skim are indistinguishable at the link level; closing it needs per-OD /
multi-commodity information the “real link flows only” contract withholds (a full
multi-commodity feasibility check is future harness-wide work). We handle it three ways rather
than hide it:
The scored
relative_gapremains a valid necessary condition — a genuine combined equilibrium always passes — and stays P1-pure.The analytic anchor is deliberately made non-degenerate. Its near/far link intercepts are spaced (1 vs 3) so
c_near(s) − c_far(T − s) = 0.2 s − 3 < 0for every feasible split; the margin-feasible flows form a one-parameter familyv(s) = (s, T−s, T−s, s)andgap(s) ∝ (s − s*)·(c_near − c_far)has a unique zero at the equilibriums* ≈ 6.92(β = 0.5), every othersbeing censored (negative excess) or strictly gapped. So the anchor itself does not exhibit the limitation, and “dump on the cheapest links” is censored on it (regression-tested,test_anchor_admits_only_the_true_equilibrium).The limitation is pinned transparently by
test_aggregate_multicommodity_limitation, which constructs the degenerate instance, asserts the false positive, and asserts the fixed-demand certificate shares it. On any scenario carrying a reference (like the anchor) theflow_rmse_vs_referencecolumn exposes such flows independently of the gap.
Convergence and scope¶
The solver early-stops on max(route-gap, distribution-gap), both non-negative, so
target_relative_gap never trips on the transiently-negative combined gap a naive stop would.
The doubly-constrained gravity requires Σ O = Σ D; a disconnected instance raises during the
shortest-path step and the certificate censors it. Per-OD dispersion parameters, a
singly-constrained (production-constrained / destination-choice-logit) variant, and
conjugate/bush acceleration of the inner assignment are future extensions.
Consequences¶
New:
CombinedDemand;Scenario.combined_demand; paradigmstatic_ue_combined; theevansmodel; the combined branch ofEvaluator(scoredrelative_gap,node_balance_residual,realized_demand);evans_symmetric_scenarioanalytic anchor (symmetric bipartite → binary logit split, a scalar fixed point recomputed with brentq — flows(p, q, q, p),p ≈ 6.92atβ = 0.5, not trusted; intercepts spaced so the anchor is degeneracy-free);tabench run --scenario evans.Unchanged: every prior scenario hash (golden Braess preserved); the fixed-demand / SO / SUE / elastic certificate paths; all prior models and tests (201 prior pass, +19 new = 220).
Deferred: per-OD dispersion; singly-constrained / logit destination-choice variant; conjugate/bush inner solver; large-network scaling of the
~zones²-support gravity.