sue-probit-msa — Probit SUE by MSA, certified by pinned Monte Carlo¶
What. Probit SUE draws perceived route costs from correlated normals instead of
the logit’s independent Gumbels, so — unlike logit — it has no closed-form
loading. sue-probit-msa reaches the probit fixed point by MSA over a Monte-Carlo
STOCH loading, and the harness certifies it with a pinned Monte-Carlo residual
(fixed sample count r_cert), reported with a standard error and a noise floor
([daganzo1977stochastic], ADR-003, docs/REFERENCES.md).
Why it is in the benchmark. It is the benchmark’s stochastic-certificate case: the scored number is itself an estimate, so it carries uncertainty. See the model compendium and docs/ARCHITECTURE.md (P1).
Scope. Runs on the built-in two-route probit anchor (β = 0.1). The budget is capped at 50 iterations for tutorial runtime — so the run is deliberately short of the fixed point, and the certificate says so honestly (residual above the MC floor).
How this notebook is graded¶
A notebook never claims a number it does not compute in that cell. Every scored
quantity below is recomputed live by the P1 Evaluator from the flows the model
emitted, in the cell where it is claimed. Model self-reports are shown only as
provenance and diffed against the certificate as an honesty check, exactly as the
harness treats them (README, Certified, not self-reported).
# Setup. `sue-probit-msa` is a core model: a plain `pip install -e .` suffices — no
# optional extra, so no guard cell. The inline backend is Agg-based (headless CI
# renders into the notebook); NEVER matplotlib.use("Agg") in-kernel — it silently
# suppresses inline figure capture.
%matplotlib inline
import numpy as np
from tabench import (
Budget,
Evaluator,
RngBundle,
SueProbitMsaModel,
Trace,
two_route_scenario,
viz,
)
The scenario¶
The same two disjoint routes as the logit anchor, but the probit task: perceived route costs are correlated normals with perception variance β = 0.1 per unit free-flow time. A different equilibrium definition → a different content hash (P2).
scenario = two_route_scenario(sue_theta=0.1, sue_family="probit")
net = scenario.network
print(f"scenario : {scenario.name}")
print(f"content hash : {scenario.content_hash()[:16]}…")
print(f"SUE family : {scenario.sue_family} (beta = {scenario.sue_theta})")
print(f"total demand : {scenario.demand.total}")
scenario : tworoute
content hash : ab27373c328c9ecc…
SUE family : probit (beta = 0.1)
total demand : 4.0
Solve¶
The model contract (CONTRIBUTING.md): a model receives
(scenario, budget, rng, trace), records checkpoints, and respects the budget.
Budgets are hardware-free (iterations / shortest-path calls; wall-clock is recorded
but never the ranking axis, P7). Whatever the model writes into self_report is
provenance, not a score.
# Budget capped at 50 iterations: every certified checkpoint costs a full Monte-Carlo
# loading, so this keeps the tutorial fast (the model is then short of the fixed point).
model = SueProbitMsaModel()
bundle = model.solve(scenario, Budget(iterations=50), RngBundle(0), Trace())
final = bundle.final
print(f"model : {model.name}")
print(f"budget spent : {final.coords.iterations} iterations")
print(f"emitted flows : {np.round(final.link_flows, 4)}")
print(f"self-reported resid: {final.self_report['sue_fixed_point_residual']:.4f} (provenance only)")
model : sue-probit-msa
budget spent : 50 iterations
emitted flows : [2.6122 2.6122 1.3878 1.3878]
self-reported resid: 2.6122 (provenance only)
Certify (P1)¶
Probit SUE has no closed form, so the harness certifies with a pinned Monte-Carlo
loading (r_cert = 2000 samples): the residual is an estimate with a standard error
se and a noise floor below which it is indistinguishable from zero (ADR-003). The
model’s own residual estimate uses different draws, so it legitimately DIFFERS from
the certificate — the sharpest form of never trust the self-report. We recompute
the analytic probit fixed point in-cell with brentq.
from scipy.optimize import brentq
from scipy.stats import norm
# Pinned Monte-Carlo certificate: r_cert samples, root_seed fixes the draws (ADR-003).
evaluator = Evaluator(scenario, root_seed=0, r_cert=2000)
metrics = evaluator.evaluate(final.link_flows)
residual = metrics["sue_fixed_point_residual"]
se = metrics["sue_residual_se"]
floor = metrics["sue_residual_floor"]
print(f"certified probit residual : {residual:.4f} ± {se:.4f} (MC se)")
print(f"MC noise floor : {floor:.4f}")
print(f"feasible : {metrics['feasible']:.0f}")
assert metrics["feasible"] == 1.0
assert np.isfinite(residual) and residual >= 0.0
# NEVER trust the self-report (P1): the model's own residual estimate differs from the
# pinned-MC certificate — different draws, different counts. The certificate is scored.
self_resid = final.self_report["sue_fixed_point_residual"]
print(f"model self-report resid : {self_resid:.4f} (provenance only — NOT the score)")
# Analytic anchor RECOMPUTED: disjoint routes -> independent normal route costs ->
# P(A) = Phi((c_B - c_A) / sqrt(3.5 beta)); the fixed point via brentq.
demand = scenario.demand.total
beta = scenario.sue_theta
def _probit_residual(f_a):
c_a = 2.0 + f_a
c_b = 1.5 + 2.0 * (demand - f_a)
return f_a - demand * norm.cdf((c_b - c_a) / np.sqrt(3.5 * beta))
f_a = brentq(_probit_residual, 1e-9, demand - 1e-9)
ref_flows = np.array([f_a, f_a, demand - f_a, demand - f_a])
print(f"probit fixed point f_A : {f_a:.4f} (recomputed); emitted f_A: {final.link_flows[0]:.4f}")
# At this capped budget the run is short of the fixed point: the certified residual
# sits ABOVE the MC floor, and the certificate reports that honestly.
assert residual > floor
assert 0.0 < f_a < demand
certified probit residual : 1.4862 ± 0.0402 (MC se)
MC noise floor : 0.0321
feasible : 1
model self-report resid : 2.6122 (provenance only — NOT the score)
probit fixed point f_A : 2.4444 (recomputed); emitted f_A: 2.6122
Visualize¶
Both figures come from tabench.viz. Left/top: the emitted probit-SUE flows on the
two-route network. Right/bottom: the emitted flows against the brentq-recomputed
probit fixed point — points sit OFF the y = x guide because the 50-iteration budget
stopped short of the fixed point, exactly what the certified residual reports.
display(viz.plot_network_flows(net, final.link_flows))
display(viz.plot_flow_scatter(("probit SUE (brentq)", ref_flows), {"sue-probit-msa": final.link_flows}))
Takeaways & pointers¶
The score is an estimate. The probit residual is Monte-Carlo — the harness reports it with a standard error and a floor; it never claims more precision than the sampling affords (ADR-003).
Self-report ≠ certificate. The model’s own residual differs from the pinned-MC certificate; only the certificate is scored.
Honest about budget. Capped at 50 iterations, the run is short of the fixed point and the residual sits above the floor — reported, not hidden.
Where next. The closed-form logit variant:
sue-msa; ADR-003 in the model compendium.