ADR-040 — dtalite-simulation: the third EDOC-1 row, the first deterministic-track external engine, closing the adr-029 honest-sourcing loop¶
Status: accepted (shipped)
File: docs/design/adr-040-dtalite-simulation.md
Context — the third row ADR-036 named, and the loop ADR-029 left open¶
ADR-036 is the design
authority for EDOC-1; its first row shipped as sumo-duaiterate
(ADR-037) and its second as matsim
(ADR-039, the stochastic flagship + the shared macrorep
substrate). This ADR ships the third row: DTALite 0.8.1’s simulation() —
the Zhou & Taylor (2014) mesoscopic queue-DNL simulator (zhou2014dtalite,
already in the canon; this row adds no new canon entry). It is the DTALite
wheel’s other entry point: ADR-029 shipped
assignment() (a static Frank-Wolfe on an exactly-mapped BPR, certified against
the DECLARED cost law) and named simulation() “a named non-goal here.” This row
closes that honest-sourcing loop — the 2014 paper’s actual mesoscopic content
lands as an EDOC producer, on the observational track, and the adr-029
docstring + the ROADMAP row now point to it.
Two firsts land with the row:
the deterministic track (ADR-036 R5’s complement):
simulation()’s only RNG is an LCG re-seeded101 + time_stepat every simulation step (TAPLite.cpp:5171-5174 + 2066-2068;a=17364, c=0, M=65521), so the engine consumes no seed —seedable=False,seed_list=(), no macroreps. The score is a singleRG_D1.scenario.seedstays hashed but is engine-INERT;per_seed_scenariosrefuses the empty list, so macrorep misuse is structurally impossible. This is stronger thansumo-duaiterate’s below-floor-seed-spread justification: there is no seed to spread over.the DTALite canonicalizer (R10): same
tabench-edoc-canon-v1;version, ascanon.pyalready promised (“the DTALite canonicalizer lands with its row, S4”).
Every number below was measured on this box on 2026-07-17 with the SHIPPED
estimator (occupancy-aware field + walk-enumeration TD-SP + per-first-edge
origin-wait profiles) on the installed DTALite==0.8.1 wheel; the engine-free
substrate additions are exercised in tests/test_edoc.py, the row (engine-free +
gated halves) in tests/test_dtalite_simulation.py (the dtalite CI leg, now
shared with dtalite-tap).
Provenance disclosure (ruling 9 — the adr-029 posture, carried)¶
The wheel-to-source linkage is UNVERIFIED and disclosed exactly as adr-029
discloses it: the wheel ships no C++ source (RECORD lists only DTALite.py,
__init__.py, and four binaries; the Python wrapper DTALite.py calls exactly two
ctypes entry points — DTA_AssignmentAPI and DTA_SimulationAPI — out of the 78
symbols nm -D exports, whose demangled mangled-C++ names match the vendored TAPLite
functions and so CORROBORATE, but do not prove, the trace-target linkage). The G0 pins
are therefore the ARTIFACTS:
DTALite==0.8.1(the CI-pinned wheel version, read viaimportlib.metadata);the Linux engine
.somd5e179ed662ae2984ba09e5230ed4151b9(the byte the engine runs; dll/x86-dylib/arm-dylib differ and are out of certified scope).
The trace target — where every cited TAPLite.cpp line was verified — is
github.com/itsfangtang/DTALite_release commit
df8a09346a592ffe3b1b8ea3a7ba8b14f0aa150e (tarball md5
31ed19ef065c11cf5185aac4097151d8). That the 0.8.1 .so was built from that
commit is not provable from the wheel (symbols + format strings are consistent
only). Byte-determinism is pinned on linux-x86_64 ONLY (CI runs
ubuntu-latest); other platforms are out of certified scope. Cited lines, all
verified against the trace target:
line |
fact |
used by |
|---|---|---|
2238 |
|
R9 (plans from |
4950 |
|
the 6 s dynamics grid ( |
4979 / 5301 |
|
the ~600 veh/h admission law (the engine’s cost law) |
5116 / 5123-5127 |
|
the boost census ( |
5171-5174 + 2066-2068 |
LCG re-seeded |
|
5378 |
main loop bounded by |
the head-block is bounded, NOT an infinite loop (ruling 1) |
5387 |
early exit |
the lull-drop hazard (ruling 2) |
The corrected/extended pilot record (ADR-036 is not edited)¶
ADR-036’s simulation() section was written from a pilot; three of its records are
refined here from the from-scratch reconstruction and this sprint’s shipped-family
measurements. ADR-036 itself is not edited (the adr-039 R10 precedent — a row
ADR corrects the pilot record in place):
Head-block is a bounded FAST drop, not an infinite loop (ruling 1). ADR-036 lines 258/348 say a pre-period departure “loops forever at rc=0” head-blocking
loadNewAgents. Measured: thesimulate()loop is structurally bounded bytotal_intervals(5378), so a pre-period departure terminates in ~0.03 s at rc=0 with the pre-period agent AND every later same-first-link agent silently dropped (all-filler07:00:00chains). On the shipped family this FAST census variant is what reproduces: a doctoredv0at engine-minute 410 corrupts the whole 781-agent route-A cohort (test_headblock_hazard_...), wall <replay_deadline_s. The pilot’s infinite-loop phrasing likely migrated from adr-029’sassignment()permuted-input loop. Both defenses ship regardless: the R6replay_deadline_s(its defense if the loop variant ever occurs) AND the per-agent completion census (its defense for the measured fast variant).The lull-drop hazard, unnamed in ADR-036 (ruling 2).
simulate()breaks at the first intervalt >= 600(60 min into the period) wheretotal_completed == total_loaded(5387), silently DROPPING any still-pending departure at rc=0. The family constructor refuses departure profiles that allow such an all-completed instant before a later departure —_assert_no_lull_drop, an eagerValueError, bounding earliest completions by the free-flow shortest path — and the G3 completion census is the certify-time backstop. This is coherent with the whole-hours/inside-one-day period gate (_engine_period_hours, eager): the horizon must be a whole number of engine hours ending ≤ hour 24, and the departure profile must keep someone provably in flight past 60 min.The OMP hazard is a CORRECTNESS pin, demonstrated (understated in ADR-036’s favor). ADR-036 argued the
#pragma omp parallel forover a sharedstd::dequeis unsafe. Executed on an 8-link congested net: OMP=4 gives 6 divergenttrajectory.csvmd5s + one SIGSEGV in 10 runs (plus 2347 tornsimulation_debug.csvlines from a shared ofstream race), while OMP=1 is byte-stable; default OMP on this 192-core box is 144 s vs 0.012 s (~10⁴×).OMP_NUM_THREADS=1is therefore a G0 correctness pin, not hygiene — set on every child, hashed insemantic_config, and the child override beats a hostile parent (test_hostile_parent_omp_is_overridden, envOMP_NUM_THREADS=64→ same canon hash). We do not assert a single racing pair — the empirical divergence+crash suffices for the pin.Wall boundary made precise (ruling 8). ADR-036’s “0.04-0.05 s” is the SUBPROCESS wall (python startup + banner + engine ≈ 0.045 s); the engine-only in-child wall is 0.012-0.015 s. This box measured a single subprocess replay (write GMNS +
vehicle.csv+ spawn + engine + parse) at ~0.08 s.The ~600 veh/h admission law is a STAIRCASE, not a flat band (scope note).
entrance_queue.size() < capacity_per_time_step = capacity/3600admitsceil(capacity/3600)-ish agents per 6 s interval as an integer-boundary staircase: the queue-empty case admits 1 agent per interval (≈600 veh/h) for capacity in (0, 3600], 2 per interval (≈1200 veh/h) for (3600, 7200], stepping up to ~6000 veh/h at cap 36000. The reference’s first-link capacities — 600 (1-lane bottleneck) and 1200 (2-lane) — both sit in the FLAT first step (≤ 3600), so both admit 1/interval ≈ 600 veh/h and the congestion physics (and thereforeRG_D1/floor_gap/separation) are exactly as measured. The instance defines its own admission as the engine’s cost law.MSA is hash-derived, not numpy-RNG (ruling 3). The pilot’s converged
RG_D1 = 0.039738was numpy-2.2.6default_rng(20260716).choice-specific. The shipped MSA switching picks are hash-derived (_msa_pick(instance_hash, k, improvers, n_move)— no cross-versionGenerator.choicedependence), so the shipped converged anchor is re-derived:0.050309(it differs from the pilot’s 0.039738 — expected and correct).
The fftt-column probe (ruling 6 — decisive)¶
The pilot wrote length/free_speed/vdf_fftt consistently and could not
distinguish which column simulation() reads. One-perturbation probe, executed
this sprint (O→M→D, one agent, baseline fftt 5 min each link):
perturbation |
measured exits ( |
conclusion |
|---|---|---|
baseline (fftt 5,5) |
|
5 min / link |
ONLY |
|
traversal MOVED |
ONLY |
|
traversal UNCHANGED |
simulation() reads vdf_fftt (minutes), NOT length/free_speed. The writer
sets vdf_fftt = the hashed fftt AND keeps length = vdf_fftt miles at
free_speed = 60 mph, so the geometry-derived fftt agrees by construction (either
read path yields the hashed fftt — robustness against an upstream read-path change).
The hash surface — the twin-run byte census, allowlist-only (R10)¶
The engine is byte-deterministic at OMP=1 (test_g1_replay_bit_deterministic:
twin replays in different dirs emit byte-identical trajectory.csv), so the
canonicalizer is the IDENTITY plus the gzip-decompress rule — idempotent and
content-sensitive by construction. The R10 founding-spec “positional-parse the
DTALite trajectory” necessity is a PARSER rule realized in
_parse_trajectory (a header/row-shape RAISE — the canon-version-bump trigger for
upstream format drift), not the hash canonicalizer.
Twin-run census: 14/14 simulation() emissions byte-identical on this box
across two temp trees — but sim_info.csv carries a timestamp header column and
the debug/summary logs are latent wall-clock surfaces. The G1 hash surface is
therefore an explicit ALLOWLIST of exactly {trajectory.csv}
(canon._DTALITE_HASHED_BASENAMES) — the one artifact the score consumes. The
pre-S4 SUMO/MATSim digests are byte-untouched (the surface= regression in
test_edoc.py re-run; both engine-gated canon digests re-confirmed).
The trajectory positional-parse facts the parser pins (all EXECUTED):
header = 13 names, every data row = 12 fields — the emitted row drops
travel_time, so positional field[7] =current_link_seq_no(1 = completed 2-link route, 0 = truncated/stuck);loaded_status(field[3]) is DEAD — reads 0 for completed, truncated, AND never-loaded alike (never read; pair D2);unvisited links carry literal
07:00:00(=_T0) filler chains; a period-end-truncated agent shows real first-link times then filler; a pre-period / head-blocked agent shows all-filler;raw second-fields carry per-agent sub-interval residuals (396/1000 rows), so the parser does NOT assert 6 s alignment of raw fields (the header-shape RAISE is the drift guard); the scheduled departure is the verbatim-echoed
departure_timecolumn;the completion census is chain-consistency-keyed (
current_link_seq_no == len(links) − 1AND a full non-filler monotone chain), NOT time-keyed — a LEGITIMATE07:00:00first entry (an agent scheduled at t=0) is not read as filler (pair D1).
The row (mirrors ADR-037/039 section-for-section; deviations named)¶
EDOC producer, not a
TrafficAssignmentModel—DTALiteSimulationAdapter(tabench.models.adapters.dtalite_simulation) emits plansP(the R9 step-0 FW split, theniterationsMSA1/(k+2)best-response blends) and derivesX+ the field from its OWN pinned replay ofP(the adr-037 artifact contract). NOT inMODEL_REGISTRY; re-exported UNCONDITIONALLY (a named deviation fromdtalite_tap’s module-scopefind_specguard, mirroringmatsim_edoc’s rationale — the module never importsDTALitein-host, so its engine-free test half runs on the core matrix legs and engine absence surfaces as the runtime G0 read). Class attrstrack="edoc-deterministic",seedable=False. It deliberately does not extenddtalite_tap.py(different track, compile map, outputs, and failure typing; only ~15 lines of subprocess boilerplate overlap, and its shape differs anyway).dtalite_tap.py’s own guard is untouched.G0 pins:
engine_versionread at certify time viaimportlib.metadata(never animport— the wheel prints a banner and ctypes-loads the OpenMP.so);assert_engine_pinagainst the instance pin; the CI==0.8.1pin. The seed pin is inert-but-hashed (deterministic track).The certifier writes EVERY engine input (pair N6 closed structurally): node/link/demand/settings from hashed fields, links sorted by
(from_node_id, to_node_id)(the adr-029 CRITICAL — same GMNS reader), enginelanes = 1always withcapacity = edge_lanes × 600(the adr-029 lanes² trap, live for the R9 step).vehicle.csvis written from the parsed plans dict, SORTED ASCENDING by(departure_time, agent)— the D5 mandatory gate (measured: an unsorted file silently filler-corrupts the later-departing agent at rc=0;test_d5_unsorted_vehicle_csv...). Because the certifier regeneratesvehicle.csv, no model-controlled byte order ever reaches the engine. There is no config echo to hash (unlike MATSim’soutput_config.xml) — mode selection IS the subprocess command (_SIM_CMD/_ASSIGN_CMD, one constant each, the N6 one-constant discipline), so the N6 defense here is purely structural.Subprocess discipline (S2 verbatim, S3 fixes from birth):
Popen( start_new_session=True)+killpg(SIGKILL)(F2 — a torn/emptytrajectory.csvfrom a mid-run kill is never parsed);stdin=DEVNULL(ExitMessage=getchar()); one subprocess per engine call (a second in-process call doubles state); the child env ALWAYS pinsOMP_NUM_THREADS=1;_intersect_replay_deadlinemakes the hashedreplay_deadline_sbound every certifier replay with the F1 caller-clip/scenario-deadline censor split (a caller wall tighter than the hashed deadline RAISESRuntimeErroras certifier-side budget exhaustion; only a SCENARIO-deadline expiry raisesPlanReplayFailure); pid-scopedmkdtempprefixes (F5); rc never trusted (every caller re-reads its artifact — success is the parse + census, the adr-029 doctrine).R3 disclosure (not a weakening). DTALite has no standalone router artifact —
simulation()never routes,assignment()never simulates — so ADR-036 R3’s engine-router cross-check clause has nothing to bind to. The substrate TD-SP is normative-only on this row, and the row substitutes a harness self-cross-check (_field_selfcheck, the adr-039 shape): every driven cost is re-derived by an independently written field composition and compared totdsp.evaluate_routeunderr3_tolerance_s— a field-arithmetic regression guard, infra RAISE, never a censor (measuredr3_max_s = 0.000 s). The third (R3) replay’sPlanReplayFailureis re-wrapped toRuntimeError(F4 typing).R9 plans construction (rulings 7).
_run_assignment_for_routesrunsassignment()on its OWN 1 h demand period (the adr-029 identity condition — the VDF divides volume by the period hours, so 1 h makesv/cthe textbook ratio; the 6 h simulation horizon would starve the FW of congestion and collapse the split to AON) and parsesroute_assignment.csvby NAME (drift RAISES; the 0.8.1 header carries a trailing comma).assignment()’s ownvehicle.csvis never read (header-only dead code, 2238)._integerize_route_volumeslargest-remainder rounds the FWvolumeshares onto each OD’s agent count (|count − share·N| ≤ 1) and fractional-position-interleaves the routes across the departure profile. The reference exercises a genuine multi-route split — 781.25/218.75 → 781/219 (the CI anchor tripwire pins it,test_r9_split_tripwire...; the adr-029link_performance.csvprecedent).The MSA loop — ROW-LOCAL. ADR-036 names no substrate MSA deliverable (its named substrate pieces all exist), and hoisting waits for a second router-less engine.
emit()= P0 from R9 → per iterate k: pinned replay → in-loop boost-census RAISE → substrate field + origin-wait builders → per-agent BR via the certifier’s own composition → per-OD blend movinground(frac·n)improvers (hash-picked) onto BR routes at1/(k+2). Emitting the certifier’s BR is pair-11 legal; the measured one-step overshoot (the reference’s step-0 0.372 would over-swing) is exactly why the model is the smoothed blend, not the one-step BR (R12: realized-BR is Tier-B, never scored).iterations=0emits the step-0 FW split as-is — the negative control.
The family and the R4 re-derivation (shipped estimator, 2026-07-17)¶
build_dtalite_corridor_scenario / reference_scenario: a two-route corridor
O →a1→ MA →a2→ D (fftt 2×300 s, 1-lane 600 veh/h bottleneck) vs O →b1→ MB →b2→ D
(2×420 s, 2-lane 1200 veh/h nominal — capped at ~600 by the admission law); 1000
agents at 2-per-6-s-slot over 50 min (1200 veh/h aggregate vs ~600 per-route), so
the step-0 FW piles the transfer queue onto route A and separates from the converged
blend. Reference content hash
f3bf543b1fc21e08dba9c5078d0114caea5ac26fcff9211c6e61ac6b9362d7c7 (topology
digest f93e00ed…). BURSTY profiles are refused by shape (agents_per_slot > 2 →
ValueError: the transfer-entrance service is non-FIFO under burst arrivals —
measured 10-per-slot parks ~2 agents/slot until the stream ends, a delta no
interval-mean field represents; at ≤ 2/instant the queue is FIFO, delta ~2 s).
dial |
value |
how derived (all shipped-estimator measurements) |
|---|---|---|
Δ / |
6 s / 3600 (21600 s = 6 h, period 7→13 h) |
the engine dynamics grid IS 6 s (4950); the horizon must be whole engine hours ( |
|
6.0 s (≤ 2 per grid instant) |
the 6 s grid; a coarser multiple is unnecessary — departures are already whole-6-s slots and the FIFO-≤2 shape gate binds |
negative control |
step-0 FW split |
7.399147× floor-displayed separation ≥ the declared 5.0; both anchors floor-displayed ( |
|
10.0 |
Δ-scan of the field-vs-experienced delta: the converged reference measures delta = 1.75 s (≪ 10); floor 10 keeps the anchor feasible with wide margin AND leaves the row RANKED — |
|
60.0 |
measured max insertion backlog 0 s (origin insertion is NOT admission-gated — 976 veh/h inserted onto a 600-cap link with zero origin wait), so |
|
30.0 |
the R6 form: a fixed python-child startup allowance + a multiple of the measured whole-run replay wall (~0.08 s), NEVER a bare per-iteration multiple; 30 s is ~375× the single-replay wall |
|
2 |
driven routes are 2-edge walks → in the TD-SP universe, |
RANKED, not manufactured (ruling 4). The Δ/floor were derived FIRST, then the
converged reference measured: it lands above the floor (RG_D1 0.050309 > floor_gap 0.011840), so it ships RANKED honestly — no family tuning was applied
to force it. Had it landed sub-floor, it would ship displayed-at-floor; the
separation gate uses the floor-displayed basis on both sides regardless (the
adr-039 ruling-4 basis, adopted from birth because the 0/0 shared-topology vacuous
pass exists on the deterministic track too — shared_bottleneck_scenario is the
refusal demonstration).
Vetting-scope disclosure. The separation gate is TOPOLOGY-digest-keyed (the S3 F3
anti-forgery choice — vetting a forgeable family STRING is the defect it fixes), and
that digest EXCLUDES agent_depart (and dt/n_intervals), so a same-topology
instance with a DIFFERENT departure profile borrows the reference’s vetting. The
≥ separation_factor× guarantee therefore holds for the vetted departure profile;
sub_floor is the per-instance vacuity backstop that still fires on a borrowed profile
(a review-constructed borrowed profile that separates only 4.85× lands sub_floor=1,
i.e. NOT ranked — every borrowed profile that DID rank also genuinely separated > 5×).
Folding agent_depart + dt/n_intervals into the vetting digest is a possible
future strengthening, deliberately deferred: folding it now would perturb the
boost-variant test’s same-topology-digest assertion (n_intervals is not a topology
field, which is exactly what lets the 1 h boost variant reuse the reference’s vetting).
Reference certify readout: feasible=1, rg_d1=0.050309, floor_gap=0.011840,
sub_floor=0, delta=1.753 s, max_backlog=0 s, br_coverage=1.0, n_improvers=523,
tstt=844614 s, r3_max_s=0.000 s.
Walls (this box): vet (2 emit + 4 certify replays) 2.1 s; emit (R9 + 16 MSA
replays + final) 1.6 s; certify (2 G1 + 1 R3 replay) 0.25 s; single
subprocess replay ~0.08 s; engine-only in-child 0.012-0.015 s. The whole row
file runs well inside the shared dtalite CI job’s minutes.
The boost census is boost-CLEAN on the reference. Onset = 21600 − 720×6 =
17280 s (4.8 h); the family exits ~10 min in, hours before onset, so no crossing.
The 1 h-horizon variant (n_intervals=600) has onset = 3600 − 4320 = −720 s <
0: the entire horizon is inside the boost window, so the instance is
boost-DEGENERATE — every agent is boost-exposed and boost_crossing_n = n_agents
(see the three-layer gate).
Forgery pairs¶
Ports of the twelve (adr-036) with DTALite realizations: P1 unreached-demand
hiding — ports WITH FORCE (pre-period silent drop + head-block + period-end
truncation, all rc=0): defended by the departure-window gate + the G3 census from
current_link_seq_no/non-filler chains (never loaded_status) + the boost census +
the lull-drop gate. P2 self-report substitution — G1 replays trajectory.csv
byte-exactly (the strongest of the three engines; the printed gap/summary is
provenance). P3 plan-set impoverishment — full-network substrate TD-SP; the
0.372 step-0 anchor IS this defense measured. P4/N1-N4 seed forgeries — VACUOUS
on the deterministic track (no seed; seed_list=()); pinned by the
per_seed_scenarios empty-list refusal. P5 cost-averaging — an averaged
trajectory is not the fixed replay output; byte-exact G1 censors it. P6
version drift — G0 importlib.metadata + assert_engine_pin + the CI ==0.8.1
pin. P7 aggregation-window gaming — hashed Δ + the raw-field delta floor gate.
P8 poison-the-alternative — occupancy witness EXACT from trajectory link spans
(the matsim event-span precedent). P9 volatile-byte hash games — the
realization IS the new canonicalizer + {trajectory.csv} allowlist + the parser’s
header-shape RAISE. P10 departure-time gaming — G2 exact departures on the 6 s
grid (verbatim schedule echo makes the write-back exact when on-grid). P11
emit-the-certifier’s-BR — legal; the MSA blend is its principled use. P12
vacuous/degenerate instances — construction refusals (_assert_no_lull_drop, the
whole-hours period gate, the burst-shape gate, the boost census) + the
floor-displayed separation refusal of the shared-edge control.
New DTALite-specific pairs (named for the review): D1 trajectory-filler
exploitation (read 07:00:00 filler as real samples) — the chain-consistency filler
rule; D2 dead-column games (claim state via loaded_status) — never read; D3
boost-window laundering (finish congested agents inside the last 72 min) — the
three-layer gate below; D4 OMP nondeterminism as a hash-dodge — the always-on
child OMP_NUM_THREADS=1 + the G1 double + X-equals-replay collapse to P2; D5
vehicle.csv order games (an unsorted file silently corrupts later agents at rc=0) —
the certifier writes it sorted, no model byte order reaches the engine.
The three-layer boost gate (D3 / pair 12), three correctly-typed layers:
in-loop RAISE — any MSA iterate whose replay census crosses the onset raises
ValueErrorinemit()(the model’s own field would be boost-contaminated); this also realizes pair-12’s “every constructor-side run” clause.certify-time CENSOR —
certify_emittedruns the boost census onemitted.experienced(G1 has bound X to the replay tuple-for-tuple, so the census on X IS the census on the replay, zero extra engine calls) and returnsfeasible=0,boost_crossing_n,boost_onset_swhenever crossings exist — even if a downstream substrate gate also censored (a too-short horizon also truncates late agents at G3; the boost window is the instance-design ROOT cause, so its diagnostic must reach the metrics dict). A boost-clean emission passes through unchanged.constructor ValueError —
negative_control_separationcensors on a boost crossing via the same certify-time arm, so a boost-degenerate topology RAISES.
_boost_crossings semantics: for onset > 0 (a partial window) only COMPLETED
agents whose exit is at/after the onset are contaminated; for onset ≤ 0 the
entire horizon is boost-covered, so the census is the whole emission (n_agents) —
a topology-stable count. On the 1 h variant this is exactly 1000
(test_boost_window_censor_live).
CI, tutorial, docs¶
CI: the existing
dtalitejob is EXTENDED (not cloned) — same 1.3 MB wheel, sameDTALite==0.8.1pin, same failure domain (adr-029’s per-dependency isolation is already served; a sixth job would isolate nothing). The test step addstests/test_dtalite_simulation.py(its engine-gated half un-skips here; its engine-free half already ran on the matrix legs); the notebook step becomes-k "dtalite-tap or dtalite-simulation"; the job comment names both rows.actionlintclean.Tutorial:
tutorials/11-external/06-dtalite-simulation.ipynb({track: external, unit: dtalite-simulation, requires_extra: dtalite, covers: []}, committed stripped,find_specguard cell).requires_extra: "dtalite"already has afind_spec("DTALite")probe in both maps — zero probe-map changes (the named contrast with matsim’s callable probe). The_track_manifest["dtalite-simulation"]entry lands UNCONDITIONALLY and atomically with the notebook (the matsim pattern — the module imports everywhere);_ALLOWLISTstays EMPTY. Headless execution ~8.5 s (well inside the 300 s per-cell cap).Docs: the ROADMAP
zhou2014dtaliterow gains the shipped-as-dtalite-simulationextension + the R8 non-comparability sentence (the adr-036-promised annotation, previously outstanding);docs/ARCHITECTURE.md’s external-engines paragraph names the third EDOC row;dtalite_tap.py’s “a named non-goal here” docstring gains the pointer notingsimulation()now closes the honest-sourcing loop. MODELS.md unchanged (EDOC rows are not theMODEL_REGISTRYsurface); no new canon entry (zhou2014dtalitepre-exists);tools/generate_references.pynever run.
Substrate reuse (no hash migration this row)¶
The row plugs into the EDOC substrate purely as the injected ReplayRunner —
EdocEvaluator, field, tdsp, replay, scenario, macrorep are UNCHANGED.
canon.py gains canonicalize_dtalite + is_hashed_dtalite_artifact +
hash_dtalite_artifacts under the UNCHANGED version, on the existing surface=
plumbing (S3), so every pre-S4 SUMO/MATSim digest is byte-identical. No new
EdocScenario field, so NO hash migration (the seed_list migration was S3’s; S4
adds none). The golden static Braess cf00f411… is domain-separated and
byte-untouched (re-asserted).
Adversarial review — S4 finding record¶
A 3-lens review (1 Opus certificate-soundness + 2 Sonnet lenses, run against the
uncommitted row) found NO certificate-soundness code defect. The Opus lens
independently re-derived RG_D1 from the raw trajectory.csv bytes and matched the
harness bit-exact (0.05030897743426841), and confirmed every gate fires: the
three-layer boost census, the R6 caller-clip/scenario-deadline censor split, the
head-block + lull-drop defenses, hash discipline over every outcome-bearing constant,
the D5 sorted-vehicle.csv gate, the G0 pins, the OMP=1 determinism, and the canon
allowlist. Two design points were ratified as reviewed-and-correct:
The onset ≤ 0 boost-census count =
n_agents. The 1 h variant reportsboost_crossing_n = 1000while only 769 agents completed (231 truncate at the period end): the shipped semantics count the whole emission as boost-exposed when the horizon is fully boost-covered (a scenario property, version-stable atn_agents), and the 231 truncations are separately caught at G3. This replaced the dead implementer’s unverified test guess of 1000 with a measurement-backed definition that yields exactly 1000.certify_emittedruns the boost census UNCONDITIONALLY (not only onfeasible==1.0), soboost_crossing_nsurfaces even when a substrate gate already censored — safe because G1 independently censors a doctored X (the boost diagnostic on a doctored X is harmless; the emission is already rejected).
The fix batch, all EXECUTED against the live pinned toolchain:
# |
Sev |
Finding |
Fix |
Pin |
|---|---|---|---|---|
F1 |
MAJOR |
the DTALite canon additions ( |
a dedicated engine-free canon block in |
4 new tests ( |
F2 |
MAJOR (doc) |
the ~600 veh/h admission was described as a flat band “[600, 7200)” — it is an integer-boundary STAIRCASE ( |
rewrote the paragraph as the staircase; the reference caps 600 + 1200 both sit in the flat first step (≤ 3600), so every certified number is unaffected (the Opus lens reproduced them bit-exact) |
the corrected prose + the unchanged anchors |
F3 |
MINOR |
the |
fixed 5386 → 5387 in the adapter module docstring, |
the corrected citations |
F4 |
MINOR |
“ |
reworded: the Python wrapper CALLS exactly two ctypes entry points out of 78 symbols, whose demangled names corroborate (not prove) the trace-target linkage |
the corrected provenance prose |
F5 |
NOTE |
the topology-digest vetting excludes |
a vetting-scope disclosure sentence + a deliberately-deferred strengthening note (folding |
the disclosure prose |
Sources (the honest ledger)¶
MEASURED THIS SPRINT (DTALite 0.8.1 on this box, 2026-07-17; the shipped estimator on fresh scratch trees): the fftt-column probe; the R4 anchors (control 0.372244, converged 0.050309, 7.399147× separation, delta 1.753 s); the RANKED-vs-floor outcome; the head-block fast-census variant on the 781-agent cohort; the lull-drop gate; the D5 sorted-
vehicle.csvgate; the twin-run byte census + allowlist; the R9 781.25/218.75 split; all walls; the reference content hash. The.somd5, the TAPLite.cpp line citations, the OMP=4 divergence/SIGSEGV, the 600 veh/h band, and the boost demonstration reproduce the research record.ATTRIBUTED (canon tool-paper, software lineage): Zhou & Taylor (2014), DTALite (
zhou2014dtalite, pre-exists) — anchors the engine’s lineage; the row validates ADAPTER + engine fidelity, never the paper’s numerics (the adr-027/029/037/039 posture).Design authority + repo precedents read in full: ADR-036 (the certificate), ADR-037 + ADR-039 (the row precedents mirrored section-for-section), ADR-029 +
dtalite_tap.py(the sibling engine, honest-sourcing loop closed here), ADR-030 (the superseded deferral), the S4 research dossier (from-scratch anchor reconstruction + design). Noreferences.bibedit and notools/generate_references.pyrun were made.