2026-07-30 18:57:10 +02:00

35 lines
1.6 KiB
Python

"""Early stop: converge-then-measure truncation that preserves equilibrium statistics."""
from __future__ import annotations
import pandas as pd
from tsi_sim.config import SimConfig
from tsi_sim.engine import ES_MEASURE, ES_MIN_EPOCH, run_trajectory
BASE = dict(n_nodes=300, stake_dist="pareto", topology="blend", degree=6,
link_latency_mean=0.5, link_latency_dist="geo", blend_hops=3, blend_delay_max=4.0,
max_uncles=2, uncle_window=300, k=256, epochs=40, genesis_d_factor=0.5)
def test_early_stop_truncates_and_matches_full_run():
full = pd.DataFrame(run_trajectory(SimConfig(**BASE)))
es = pd.DataFrame(run_trajectory(SimConfig(**BASE, early_stop=True)))
# truncation happened, with room for the measurement budget
assert ES_MIN_EPOCH + 1 <= len(es) < len(full)
# bit-identical prefix (same RNG streams; early_stop is excluded from key())
n = len(es)
assert (full.head(n).mean_ratio.to_numpy() == es.mean_ratio.to_numpy()).all()
# equilibrium agrees: early-stop tail (measurement sample) vs full-run tail
es_tail = es.tail(ES_MEASURE).mean_ratio.mean()
full_tail = full[full.epoch >= 20].mean_ratio.mean()
assert abs(es_tail - full_tail) < 0.015
def test_sawtooth_never_stops_early():
cfg = SimConfig(**{**BASE, "epochs": 24}, early_stop=True,
adversary_frac=0.3, adversary_strategy="withhold",
adversary_period=6, adversary_withhold_epochs=3)
df = pd.DataFrame(run_trajectory(cfg))
assert len(df) == 24 # full budget: detector disabled for schedules