# ADR-030 — MATSim / DynaMIT / DYNASMART adapters: measured deferral, and the ADR that unblocks them **Status:** accepted (deferral record) **File:** `docs/design/adr-030-external-dta-simulators-deferred.md` ## Context — the queue after `dtalite-tap`, and a rule earned twice The external-adapter queue holds three more rows: `matsim-adapter` (Horni et al. 2016, canon `horni2016multiagent`), `dynamit-adapter` (Ben-Akiva et al. 2001), and the DYNASMART pair (Jayakrishnan et al. 1994 adapter / Peeta & Mahmassani 1995 solver). The "external tools cannot run in CI" assumption has now fallen **twice** — `eclipse-sumo` (ADR-027) and `DTALite` (ADR-029) both ship engine binaries in pip wheels — so no row may be deferred on speculation: this ADR records **measured probes, run 2026-07-15**, for all three, and defers them on what the probes actually found. A deferral here is an attempt record, not a guess. ## Decision — DEFER all three adapter rows; the blocker is named per row ### MATSim: executable, but the certificate it needs does not exist yet **Executability is PROVEN, not presumed** (probe record, this box, 2026-07-15): PyPI `install-jdk` fetches a Temurin JDK without root (~6 s; MATSim 2026.0 needs JDK 25, 2025.0 runs on JDK 21 — both verified); the official `matsim-2026.0-release.zip` (113 MB, GitHub `matsim-org/matsim-libs`) downloaded in ~2 s; a hand-built two-route scenario (6 nodes / 7 links — the engine forces entry/exit links and strong connectivity, both crash surfaces captured) with 100 agents ran 11 iterations headless in **6.6 s / 550 MB RSS**; per-link flows come from `output_links.csv.gz` and events (caveat: the CSV undercounts the arrival link — count `entered link` events); **same seed → bit-identical outputs**, even across the 2025.0/2026.0 releases; exit codes are trustworthy (rc 1 on all six failure probes, informative messages). A CI job (actions/setup-java + cached zip) projects to 1–2 min cold. The PyPI `matsim` package (0.2.3) is an unrelated *neuronal* simulator (name collision) and `matsim-tools` is file I/O only — the engine is Java-only. (Full recipe: the scoping dossiers, session scratchpad `matsim-probe/`.) **The blocker is formulation — A2 is impossible in kind, not degree.** Both shipped external-engine ADRs made the cost-matched anchor the shipping condition ("the row does not ship without A2", ADR-027/029). MATSim's QSim has **no static latency function**: each link's traversal time is `length/freespeed` — a **constant** — whenever the flow capacity does not bind, and emergent time-varying queueing when it does. No `(freespeed, flowCapacity, storageCapacity)` choice produces `t = fft·(1 + b·(v/c)^p)` on any interior flow range; the exactly-representable set under the declared BPR is **`{b = 0}`** — constant-cost links, which `aon` already matches trivially. The concrete consequence (analytic, from the declared Braess costs — NOT a measured MATSim run): with capacities above saturation every link is cost-flat below capacity, so MATSim's co-evolution converges — correctly, under ITS OWN model — to all demand on the bypass `(6,0,6,0,6)`, which the harness certifies at **RG = (816−660)/816 ≈ 0.19**. That number would conflate four unattributable error sources (queue-vs-BPR cost-law gap; the invented time axis a static scenario doesn't have; integer-agent quantization; seeded replanning noise) with **no decomposition gate** — unlike marouter's `incremental` negative control (RG 0.07, attributed to a non-equilibrium loading of a *matched* cost law). The equilibrium concept itself differs: a stochastic co-evolutionary fixed point over plans under experienced dynamic times, not Wardrop UE under static declared costs — the same category ADR-027 rejected for `duaIterate` and ADR-029 named a non-goal for DTALite's `simulation()`. `docs/ARCHITECTURE.md` already states the policy: external engines get "certification where static costs permit; **otherwise scored on the observational track**" — and that observational track for external dynamic engines does not exist yet. **Scoped-ship variants considered and rejected:** (a) a `{b=0}`-only slice duplicates `aon`; (b) a no-certificate P5 observational row violates the A2 shipping bar and the duaIterate precedent (a different equilibrium concept is not scored against static BPR at all); (c) calibrating MATSim capacities per-scenario until Braess flows match the UE oracle inverts the benchmark's direction (the harness certifies models; models are never tuned to the reference) — refused. ### DynaMIT: no public artifact exists (probe record) GitHub search returns **zero** public DynaMIT repositories (re-verified independently); the system was historically distributed by the MIT ITS Lab on request; the successor SimMobility (`smart-fm/simmobility-prod`) is public but carries SPDX `NOASSERTION` licensing. There is nothing to adapt that a public benchmark may execute or that CI may install. Deferred on artifact absence. ### DYNASMART: licensing bars a CI row; the WHITE-BOX sibling stays live FHWA DYNASMART-P is distributed under FHWA/McTrans licensing (no pip/public artifact; the McTrans distribution page redirects). A licensed binary cannot back a public CI row. The **adapter** path is deferred — but the TASKS queue item bundles it with **Peeta & Mahmassani (1995)** (*System optimal and user equilibrium time-dependent traffic assignment in congested networks*), a **white-box solver** row on the roadmap that needs no binary at all: the SO/UE time-dependent formulation can be implemented from the paper on the repo's own DTA ladder (the `merchant-nemhauser`/`lp-so-dta` precedent). That row is NOT deferred by this ADR; it stays in the implementable queue. ## What would unblock the adapters (named, concrete) **One ADR: the external-dynamic-engine observational certificate** — the dynamic scenario family + certificate for external-simulator output that ADR-027 (the `duaIterate` follow-up) and ADR-029 (the `simulation()` follow-up) both name. It unblocks **three queued rows at once** — MATSim, SUMO `duaIterate`, DTALite `simulation()` — a far better sprint shape than forcing any one of them through the static certificate. The repo's own DNL certificates (C0–C8, ADR-010 and the gated review) are the natural scoring substrate. On that ladder MATSim ships as the **first agent-based, first stochastic-track external engine** (`global.randomSeed` → seedable, P8 macroreps + bootstrap CIs), with the execution recipe above already proven on this box. DynaMIT remains artifact-blocked regardless; DYNASMART remains license-blocked regardless — those two need upstream changes no repo ADR can supply. ## Consequences - No code, no new dependency, no CI job, no hash change — a documentation-only record. - `docs/ROADMAP.md`: the Horni et al. (2016) and Ben-Akiva et al. (2001) black-box rows and the Jayakrishnan et al. (1994) evaluation-tool row carry a deferral pointer to this ADR (hand-annotated, like shipped flips — `tools/generate_references.py` is never run). The Peeta & Mahmassani (1995) white-box row is untouched and live. - `docs/ARCHITECTURE.md`: the external-engines paragraph's "MATSim planned" updated to point here. - The scoping dossiers (execution + formulation, with the full probe logs) live in the session scratchpad; their load-bearing facts are recorded above so the deferral survives the scratchpad. - Better next sprints, per the same scoping: `xu2024-dataset` (CC-BY download-on-demand fetcher, pure P9), `simopt-protocol` (progress curves / solvability profiles, pure metric code serving P5), `bo4mob-scenario` (arXiv 2510.18824) — each produces certified, interpretable artifacts now.