mirror of
https://github.com/logos-blockchain/research.git
synced 2026-08-07 03:33:33 +00:00
96 lines
3.6 KiB
Python
96 lines
3.6 KiB
Python
#!/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())
|