ADR-005 — Elastic (variable) demand: a new problem class with a P1-pure certificate

Status: accepted (shipped in v1) File: docs/design/adr-005-elastic-demand.md

Context

Every task so far fixes the OD demand matrix. Elastic-demand user equilibrium (Florian & Nguyen 1974) makes demand endogenous: the trips between OD pair rs are a strictly decreasing function of that pair’s equilibrium travel cost, d_rs = D_rs(u_rs). The equilibrium couples two conditions (Boyles/Lownes/Unnikrishnan, Transportation Network Analysis v1.0 §9.1, eqs (9.8)–(9.14); Sheffi, Urban Transportation Networks 1985 ch. 6):

  1. Route equilibrium given demand d — the ordinary Wardrop condition.

  2. Demand consistencyd_rs = D_rs(u_rs) with u_rs the equilibrium min OD cost.

The convex program is the Beckmann objective plus an inverse-demand term (Sheffi [6.1a], Boyles (9.16)):

min_{x,d}  Σ_a ∫_0^{x_a} t_a(w) dw  −  Σ_rs ∫_0^{d_rs} D_rs^{-1}(w) dw

This is a genuinely new problem class, and it collides with two harness assumptions that this ADR resolves.

Sourcing (dual-verified, one attribution corrected)

Formulas were recovered from two primary-accessible textbooks read in full — Sheffi (1985) ch. 6 and Boyles et al. TNA §9.1 — which agree formula-for-formula. Two things are deliberately not attributed to the paywalled primary:

  • The excess-demand / dummy-arc transformation used by the solver is Gartner (1980) (Boyles §9.1.2, verbatim: “developed by Gartner circa 1980”), not Florian & Nguyen.

  • Florian & Nguyen (1974) is cited as the seminal computational paper for the problem; its own method is Generalized Benders Decomposition (abstract verified; body paywalled/unread), which is not what fw-elastic implements.

Canon keys: florian1974method, gartner1980optimal, sheffi1985urban, beckmann1956studies — all already in docs/references.json.

Decision 1 — Represent the demand law as content-hashed scenario data

ElasticDemand(form, param) (core/scenario.py) carries the decay law; the reference demand d0_rs = D_rs(0) is the ordinary Demand.matrix (already hashed). Two forms:

form

D(u)

D^{-1}(d)

excess-arc cost W(e)=D^{-1}(d0−e)

linear

d0·max(0, 1−u/param)

param·(1−d/d0)

param·e/d0 — bounded, singularity-free

exponential

d0·exp(−param·u)

ln(d0/d)/param

−ln(1−e/d0)/param — cited default, log-singular as d→0 (floored)

Scenario.elastic_demand is optional and hashed only when set, appended after the SUE fields, so every fixed-demand scenario keeps its byte-identical hash — the golden Braess hash cf00f411… is asserted preserved (test_golden_braess_hash_preserved). Elastic and SUE are mutually exclusive (deterministic-endogenous-demand vs stochastic-fixed-demand).

Decision 2 — Solve as fixed-demand UE on the Gartner excess-demand network

fw-elastic (ElasticDemandFWModel, paradigm static_ue_elastic) adds one direct r→s “excess-demand” arc per OD pair with the increasing cost W(e)=D_rs^{-1}(d0−e), fixes the augmented demand at d0, and runs Frank & Wolfe with an exact augmented-Beckmann line search. At equilibrium every used r→s path — real or dummy — has the common cost u_rs, so the dummy flow e_rs = d0 D_rs(u_rs) is the unmet demand and the realized demand is exactly D_rs(u_rs). The solver starts feasible at e=0 (all demand on the real network), which sidesteps the exponential form’s D^{-1}(0) singularity. Only real link flows are emitted — the dummy arcs are an internal device the certificate never sees. FW’s slow tail is inherited (and documented); conjugate/bush variants on the augmented network are a future extension.

Decision 3 — A P1-pure certificate: recompute the demand, then score

The harness knows D (content-hashed), so from the emitted real link flows v it recomputes everything (metrics/gaps.py, gated on scenario.elastic_demand):

t   = link_cost(v)
u   = od_cost_matrix(t)          # per-OD shortest-path cost (new PathEngine skim)
d*  = D(u)                       # demand-consistent demand
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*                            # scored elastic quantity

Both relative_gap and node_balance_residual are 0 iff v is the elastic UE, and both are pure functions of (v, scenario) — no self-report is trusted (P1). This is the “direct-solver” gap of Boyles §9.1.5 (relative gap + total-misplaced-flow), computed by the harness. It is consistent whether or not the model used dummy arcs: at equilibrium D(u(v)) = d0 e = realized, so the reconstruction recovers the model’s own realized demand exactly (proven by hand on the analytic anchor).

The feasibility gate is demand-consistency (a stricter, principled choice)

For fixed demand, node_balance is an exact invariant (all-or-nothing always routes the matrix). For elastic demand there is no given matrix, and through-node conservation alone is not a sufficient gate: a flow circulating a through-node cycle — or, on an all-zone network like Sioux Falls where there are no through nodes, any flow — carries no OD traffic yet conserves everywhere, and would certify a phantom elastic UE with a tuned relative_gap 0 (this exact exploit was found in adversarial review). So the gate must tie v to the demand it routes, and the only quantity that does is node_balance(v, d*). It is therefore the feasibility gate, exactly as for fixed demand.

The consequence — unique to elastic demand — is that an off-equilibrium flow is not feasible: the real flows route D(u(v)) only at equilibrium, so node_balance(v, d*) is a convergence quantity and a checkpoint certifies only once the solver has (nearly) converged. This is correct: unlike fixed demand there is no given demand for an intermediate flow to be feasible-but-suboptimal against — off equilibrium there is no well-defined routed demand at all. A fixed-demand solver run on an elastic task is thus censored (it routes the reference demand, not D(u(v))); zero and phantom flows are censored (their zone divergences disagree with d* = D(u) > 0). The known aggregate-vs-per-OD limitation of the fixed-demand audit (necessary, not sufficient for multi-commodity feasibility) is inherited unchanged.

Convergence and scope

fw-elastic self-reports the real-route relative gap (identical to the scored relative_gap) and early-stops on max(real, augmented) gap, so target_relative_gap is comparable to a fixed-demand solver’s and never trips on the transiently-negative real gap a naive stop would. Because the augmented network carries one excess-demand arc per OD pair (≈ zones²), FW’s tail is slower than fixed-demand FW: it certifies tightly on small networks and converges (more slowly) on large ones — conjugate/bush variants on the augmented network, or a demand-diagonalization reusing the fast fixed-demand solvers, are future work. Every positive-demand OD pair must be reachable in the real network (a disconnected instance, which the formulation could absorb as fully unmet demand, is out of scope for v1 and raises during the shortest-path step; the certificate censors it rather than crashing). Intrazonal (diagonal) reference demand never enters the network and is excluded from realized_demand.

Consequences

  • New: ElasticDemand; Scenario.elastic_demand; paradigm static_ue_elastic; PathEngine.od_cost_matrix; fw-elastic model; realized_demand (scored) + self_realized_demand (provenance) CSV columns; elastic_two_route_scenario analytic anchor (u=5, f_A=3, f_B=2, realized 5, flows (3,3,2,2) — recomputed with brentq in tests, not trusted).

  • Unchanged: every fixed-demand scenario hash (golden Braess preserved); the fixed-demand / SO / SUE certificate paths; all prior models and tests (187 pass).

  • Deferred: per-OD demand parameters; conjugate/bush solvers on the augmented network; a combined distribution–assignment task (Evans 1976) that reuses this machinery.