96 lines
3.6 KiB
Python
Raw Normal View History

2026-07-30 18:51:15 +02:00
#!/usr/bin/env python
"""Analytic sanity checks: simulator vs closed-form theory.
Run with the project venv: python scripts/verify.py
Exits non-zero if any check fails.
"""
from __future__ import annotations
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
import numpy as np # noqa: E402
import pandas as pd # noqa: E402
from tsi_sim.config import SimConfig # noqa: E402
from tsi_sim.engine import run_trajectory # noqa: E402
from tsi_sim.epoch import simulate_epoch # noqa: E402
from tsi_sim.rng import rng_for # noqa: E402
from tsi_sim.stake import make_stake # noqa: E402
from tsi_sim.theory import expected_ratio # noqa: E402
F = 1.0 / 30.0
K = 128 # scaled: T = 6*floor(128/f) = 23040 slots
EPOCHS = 45
REPS = 12
BURN = 25
def tail_mean(cfg: SimConfig, col: str, reps: int = REPS) -> tuple[float, float]:
vals = []
for r in range(reps):
df = pd.DataFrame(run_trajectory(cfg.__class__(**{**cfg.__dict__, "replicate": r})))
vals.append(df[col].iloc[BURN:].mean())
return float(np.mean(vals)), float(np.std(vals) / np.sqrt(reps))
def check(name: str, ok: bool, detail: str) -> bool:
print(f"[{'PASS' if ok else 'FAIL'}] {name}: {detail}")
return ok
def main() -> int:
results = []
# 1. Active-slot rate ~= f when D_est = D_true, L=0, U=0.
cfg = SimConfig(n_nodes=2000, stake_dist="uniform", latency=0, max_uncles=0,
k=K, epochs=1, genesis_d_factor=1.0)
rng = rng_for(cfg)
stake = make_stake(cfg, rng)
er = simulate_epoch(cfg, stake, float(stake.sum()), rng)
active_rate = er.n_active / cfg.period_T
results.append(check("active-slot rate ~ f (L=0,U=0,D=D_true)",
abs(active_rate - F) / F < 0.05,
f"active_rate={active_rate:.5f} f={F:.5f}"))
# 2. U=0 equilibrium ratio ~= expected_ratio(f, measured q).
cfg = SimConfig(n_nodes=1000, stake_dist="uniform", latency=4, max_uncles=0,
k=K, epochs=EPOCHS, genesis_d_factor=0.5)
ratio, se = tail_mean(cfg, "ratio")
q, _ = tail_mean(cfg, "q")
pred = float(expected_ratio(F, q))
results.append(check("U=0 ratio ~ theory(q)",
abs(ratio - pred) < 0.02 + 2 * se,
f"sim={ratio:.4f}±{se:.4f} theory(q={q:.3f})={pred:.4f}"))
# 3. Underestimate at higher latency (q < 1 => ratio < 1), U=0.
cfg = SimConfig(n_nodes=1000, stake_dist="uniform", latency=10, max_uncles=0,
k=K, epochs=EPOCHS, genesis_d_factor=0.5)
ratio, se = tail_mean(cfg, "ratio")
q, _ = tail_mean(cfg, "q")
results.append(check("U=0 underestimates true stake at latency",
ratio < 0.98 and q < 0.98,
f"ratio={ratio:.4f} q={q:.3f}"))
# 4. q_eff -> 1 and ratio -> ~1 as U grows (uncles recover forks).
base = dict(n_nodes=1000, stake_dist="uniform", latency=8, uncle_strategy="oldest",
k=K, epochs=EPOCHS, genesis_d_factor=0.5)
r0, _ = tail_mean(SimConfig(max_uncles=0, **base), "ratio")
r4, _ = tail_mean(SimConfig(max_uncles=4, **base), "ratio")
qe4, _ = tail_mean(SimConfig(max_uncles=4, **base), "q_eff")
results.append(check("uncles recover accuracy (q_eff->1, |ratio-1| shrinks)",
qe4 > 0.99 and abs(r4 - 1) < abs(r0 - 1) and abs(r4 - 1) < 0.03,
f"ratio U0={r0:.4f} -> U4={r4:.4f}; q_eff(U4)={qe4:.4f}"))
print()
n_pass = sum(results)
print(f"{n_pass}/{len(results)} checks passed")
return 0 if n_pass == len(results) else 1
if __name__ == "__main__":
raise SystemExit(main())