117 lines
5.1 KiB
Python
Raw Normal View History

2026-07-30 18:52:01 +02:00
"""Analytic sanity checks: simulator vs closed-form theory (``tsi-verify``).
Replicate runs are evaluated across CPU cores. Exits non-zero if any check fails.
"""
from __future__ import annotations
from dataclasses import replace
import numpy as np
import pandas as pd
from joblib import Parallel, delayed
from .config import SimConfig
from .engine import run_trajectory
from .epoch import simulate_epoch
from .rng import seedseq_for
from .stake import make_stake
from .theory import block_count_ceiling, expected_ratio, fixed_point_bias
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, n_jobs: int = -1) -> tuple[float, float]:
"""Mean over ``reps`` replicates of each trajectory's post-burn-in tail mean of ``col``."""
def one(r: int) -> float:
df = pd.DataFrame(run_trajectory(replace(cfg, replicate=r)))
return float(df[col].iloc[BURN:].mean())
vals = np.array(Parallel(n_jobs=n_jobs, backend="loky", inner_max_num_threads=1)(
delayed(one)(r) for r in range(reps)
))
return float(vals.mean()), float(vals.std() / 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)
ss = seedseq_for(cfg)
children = ss.spawn(2)
stake = make_stake(cfg, np.random.default_rng(children[0]))
er = simulate_epoch(cfg, stake, float(stake.sum()), children[1])
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 -> block-count ceiling as U grows (uncles recover forks).
# The residual |ratio-1| ~ 0.017 is the intrinsic winners-vs-active-slots floor
# (density_m counts blocks), NOT a convergence failure -- see check 5.
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}"))
# 5. Full recovery equilibrates at the block-count ceiling -ln(1-f)/f (~1.017), not 1.
ceiling = float(block_count_ceiling(F))
r4_val, se4 = tail_mean(SimConfig(max_uncles=4, **base), "ratio")
results.append(check("full-recovery ratio ~ block-count ceiling",
abs(r4_val - ceiling) < 0.015 + 2 * se4,
f"ratio(U4)={r4_val:.4f} ceiling=-ln(1-f)/f={ceiling:.4f}"))
# 6. Fixed-point mode adds the spec's ~1% f-truncation overestimate.
fp_base = dict(n_nodes=1000, stake_dist="uniform", latency=0, max_uncles=0,
k=K, epochs=EPOCHS, genesis_d_factor=1.0)
r_float, _ = tail_mean(SimConfig(fixed_point=False, **fp_base), "ratio")
r_fixed, _ = tail_mean(SimConfig(fixed_point=True, **fp_base), "ratio")
bias = float(fixed_point_bias(F))
results.append(check("fixed_point mode adds ~1% f-truncation bias",
r_fixed > r_float and abs(r_fixed / r_float - bias) < 0.01,
f"float={r_float:.4f} fixed={r_fixed:.4f} ratio={r_fixed/r_float:.4f} "
f"expected~{bias:.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__": # pragma: no cover
raise SystemExit(main())