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

64 lines
2.9 KiB
Python

"""Poisson long-tail jitter model (case (b) of the N-scaling study)."""
from __future__ import annotations
import numpy as np
import pandas as pd
import pytest
from tsi_sim import topology as topo
from tsi_sim.config import SimConfig
from tsi_sim.engine import run_trajectory
BASE = dict(n_nodes=200, 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=1, uncle_window=300, k=64, epochs=8, genesis_d_factor=0.5)
def test_poisson_frac_zero_matches_no_jitter_statistics():
"""jitter_frac=0 hits nobody: consensus holds and accuracy matches the jitter-free run."""
off = pd.DataFrame(run_trajectory(SimConfig(**BASE)))
z = pd.DataFrame(run_trajectory(SimConfig(
**BASE, jitter_mean=3.0, jitter_dist="poisson", jitter_frac=0.0)))
assert z.range_ratio.max() == 0.0
assert z.agreement_window.min() == 1.0
# same equilibrium to MC noise (different RNG stream — key includes jitter fields)
assert abs(z[z.epoch >= 4].mean_ratio.mean() - off[off.epoch >= 4].mean_ratio.mean()) < 0.1
def test_poisson_tail_consensus_survives():
"""10% long-tail stragglers (lambda=3 slots): spread stays 0, agreement stays 1."""
df = pd.DataFrame(run_trajectory(SimConfig(
**BASE, jitter_mean=3.0, jitter_dist="poisson", jitter_frac=0.1)))
assert df.range_ratio.max() == 0.0
assert df.agreement_window.min() == 1.0
def test_jitter_fields_validated_and_keyed():
with pytest.raises(ValueError):
SimConfig(jitter_dist="weibull")
with pytest.raises(ValueError):
SimConfig(jitter_frac=1.5)
a = SimConfig(**BASE, jitter_mean=3.0, jitter_dist="poisson", jitter_frac=0.1)
b = SimConfig(**BASE, jitter_mean=3.0, jitter_dist="poisson", jitter_frac=0.05)
c = SimConfig(**BASE, jitter_mean=3.0, jitter_dist="exp", jitter_frac=0.1)
assert len({a.key(), b.key(), c.key()}) == 3
def test_poisson_only_hits_the_requested_fraction():
"""Direct check on one arrival column: ~frac of nodes delayed, by whole slots."""
big = {**BASE, "n_nodes": 2000}
cfg_j = SimConfig(**big, jitter_mean=3.0, jitter_dist="poisson", jitter_frac=0.1)
cfg_0 = SimConfig(**big)
pl = topo.build_path_latency(cfg_j, np.random.default_rng(0))
# same rng seed: relay/mixing draws happen before jitter, so the columns differ by jitter only
col_j = topo.arrival_column(pl, producer=0, slot=100, config=cfg_j,
rng=np.random.default_rng(1))
col_0 = topo.arrival_column(pl, producer=0, slot=100, config=cfg_0,
rng=np.random.default_rng(1))
extra = col_j - col_0
delayed = extra > 0
frac = delayed[1:].mean() # exclude the producer (clamped to its own slot)
assert 0.04 < frac < 0.16 # ~10%, minus the Poisson(3) zeros (~5%)
assert np.allclose(extra[delayed], np.round(extra[delayed])) # whole-slot stragglers