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

102 lines
4.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Relative stake estimate vs network delay (fig16).
Shows the report's central relationship: the recovered *relative stake* ``D̂/D`` as a function of the
mean block-visibility delay ``D_vis`` (seconds), one curve per uncle cap ``U``. Accuracy holds near
the ceiling ``c(f)`` while the load ``ρ = f·D_vis`` stays below ``⌈U⌉``, then collapses — so larger
delay needs more uncles. Blend transport, f = 1/30 (30 s blocks); delay swept via the
per-hop blending budget ``blend_delay_max``.
Run: python scripts/stake_vs_delay.py (writes runs/stake_vs_delay.parquet + fig16)
"""
from __future__ import annotations
from pathlib import Path
import numpy as np
import pandas as pd
from tsi_sim import topology
from tsi_sim.config import SimConfig
from tsi_sim.engine import run_trajectory
from tsi_sim.plotting import style
HERE = Path(__file__).resolve().parent.parent
RUNS = HERE / "runs"
FIGS = HERE / "report-figures"
RUNS.mkdir(exist_ok=True)
FIGS.mkdir(exist_ok=True)
F = 1.0 / 30.0
HOPS = 3
DELAYS = [1.0, 4.0, 8.0, 12.0, 16.0, 20.0, 26.0, 32.0, 40.0] # per-hop blending budget (s)
UNCLES = [0, 1, 2, 3]
BASE = dict(n_nodes=800, k=48, epochs=16, stake_dist="uniform", topology="blend", degree=8,
link_latency_mean=0.3, link_latency_dist="geo", blend_hops=HOPS, uncle_window=300,
genesis_d_factor=0.5, f=F)
def d_vis(delay: float) -> float:
"""Mean visibility delay D_vis = hops·δ/2 + (hops+1)·_mean, _mean = mean shortest-path."""
cfg = SimConfig(**BASE, blend_delay_max=delay, max_uncles=1)
pl = topology.build_path_latency(cfg, np.random.default_rng(np.random.SeedSequence(0)))
off = pl[~np.eye(pl.shape[0], dtype=bool)]
l_mean = float(off[np.isfinite(off)].mean())
return HOPS * delay / 2.0 + (HOPS + 1) * l_mean
def sweep() -> pd.DataFrame:
rows = []
for delay in DELAYS:
dv = d_vis(delay)
for U in UNCLES:
reps = []
for r in range(5):
df = pd.DataFrame(run_trajectory(SimConfig(
replicate=r, blend_delay_max=delay, max_uncles=U, **BASE)))
reps.append(df[df.epoch >= 8].mean_ratio.mean())
ratio = float(np.mean(reps))
sem = float(np.std(reps) / np.sqrt(len(reps)))
rows.append(dict(delay=delay, d_vis=dv, rho=F * dv, U=U, ratio=ratio, sem=sem))
print(f"delay={delay:4.0f}s D_vis={dv:5.1f} rho={F*dv:4.2f} U={U}: D̂/D={ratio:.3f}")
out = pd.DataFrame(rows)
out.to_parquet(RUNS / "stake_vs_delay.parquet")
return out
def fig16(df: pd.DataFrame) -> None:
import matplotlib.pyplot as plt
style.apply_style()
fig, ax = plt.subplots(figsize=(7.2, 4.4))
ax.axhline(1.0, color="0.5", lw=0.9, ls="--", label="exact recovery (1.0)")
for i, U in enumerate(UNCLES):
s = df[df.U == U].sort_values("d_vis")
ax.errorbar(s.d_vis, s.ratio, yerr=s["sem"], fmt="-o", ms=4, capsize=2,
color=style.OKABE_ITO[i], label=f"U = {U}")
# rho = 1, 2, 3 boundaries: D_vis = k/f <-> rho = f*D_vis = k
for k in (1, 2, 3):
dv = k / F
if dv <= df.d_vis.max() * 1.02:
ax.axvline(dv, color="0.7", lw=0.7, ls=":")
ax.text(dv, 0.32, f"ρ={k}", rotation=90, va="bottom", ha="right", fontsize=7,
color="0.4")
ax.set_xlabel(r"mean block-visibility delay $D_{\rm vis}$ (s) [load $\rho = f\,D_{\rm vis}$]")
ax.set_ylabel(r"relative stake estimate $\hat D / D$")
ax.set_title(r"Recovered relative stake vs delay (blend, $f=1/30$): "
r"$U$ must grow with $\rho=\lceil f D_{\rm vis}\rceil$")
ax.set_ylim(0.3, 1.02) # bounded by 1: cap at the exact-recovery bound, no above-1 headroom
ax.legend(fontsize=8, loc="lower left")
style.save(fig, FIGS / "fig16_stake_vs_delay", provenance="scripts/stake_vs_delay.py")
plt.close(fig)
def main() -> None:
print("=== relative stake estimate vs delay ===")
df = sweep()
fig16(df)
print("wrote fig16_stake_vs_delay")
if __name__ == "__main__":
main()