mirror of
https://github.com/logos-blockchain/research.git
synced 2026-08-07 11:43:20 +00:00
174 lines
7.6 KiB
Python
174 lines
7.6 KiB
Python
|
|
"""Appendix B: the U=0 estimate fluctuates around 1 — sampling noise, not bias (figB1, figB2).
|
|||
|
|
|
|||
|
|
Data:
|
|||
|
|
(1) clean zero-delay series (full_mesh, L=0, U=0, uniform stakes) at k in {256, 1024, 2160}
|
|||
|
|
-> runs/fluctuation_u0.parquet (this script, --run)
|
|||
|
|
(2) the committed full-scale N=1000 run (regular sub-slot links and blend, U=0, k=2160)
|
|||
|
|
-> per-epoch tails read directly.
|
|||
|
|
|
|||
|
|
Figures:
|
|||
|
|
figB1 — high-precision per-epoch trace of (D_hat/D - 1) in per-mil at k=2160: the clean
|
|||
|
|
zero-delay series and the realistic 0.1-slot direct-gossip series, with the
|
|||
|
|
+-sigma_th = sqrt((1-f)/(f T)) band.
|
|||
|
|
figB2 — left: per-epoch deviation distributions vs k with the 1/sqrt(T) law; right: the
|
|||
|
|
delay progression (0.1 -> 1.0-slot links, blend): mean drops below 1 and
|
|||
|
|
P(D_hat/D > 1) -> 0 as orphan loss takes over.
|
|||
|
|
|
|||
|
|
Run: python scripts/appendix_fluct.py --run (simulate series (1), ~30-60 min)
|
|||
|
|
python scripts/appendix_fluct.py (render figures + print stats)
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import sys
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
import numpy as np
|
|||
|
|
import pandas as pd
|
|||
|
|
|
|||
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|||
|
|
|
|||
|
|
from joblib import Parallel, delayed # noqa: E402
|
|||
|
|
|
|||
|
|
from tsi_sim.config import SimConfig # noqa: E402
|
|||
|
|
from tsi_sim.engine import run_trajectory # noqa: E402
|
|||
|
|
from tsi_sim.plotting import style # noqa: E402
|
|||
|
|
|
|||
|
|
HERE = Path(__file__).resolve().parent.parent
|
|||
|
|
RUNS = HERE / "runs"
|
|||
|
|
FIGS = HERE / "report-figures"
|
|||
|
|
|
|||
|
|
F = 1.0 / 30.0
|
|||
|
|
KS = (256, 1024, 2160)
|
|||
|
|
REPS = 4
|
|||
|
|
EPOCHS = 120
|
|||
|
|
|
|||
|
|
|
|||
|
|
def sigma_theory(k: int) -> float:
|
|||
|
|
t_win = 6 * int(k / F)
|
|||
|
|
return float(np.sqrt((1 - F) / (F * t_win)))
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _one(k: int, rep: int) -> pd.DataFrame:
|
|||
|
|
cfg = SimConfig(n_nodes=400, stake_dist="uniform", topology="full_mesh", latency=0,
|
|||
|
|
max_uncles=0, uncle_window=300, k=k, epochs=EPOCHS,
|
|||
|
|
genesis_d_factor=1.0, replicate=rep)
|
|||
|
|
df = pd.DataFrame(run_trajectory(cfg))
|
|||
|
|
df["k_run"] = k
|
|||
|
|
return df[["k_run", "replicate", "epoch", "mean_ratio", "range_ratio"]]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def run() -> None:
|
|||
|
|
jobs = [(k, r) for k in KS for r in range(REPS)]
|
|||
|
|
parts = Parallel(n_jobs=3, prefer="processes")(delayed(_one)(k, r) for k, r in jobs)
|
|||
|
|
out = pd.concat(parts, ignore_index=True)
|
|||
|
|
out.to_parquet(RUNS / "fluctuation_u0.parquet")
|
|||
|
|
print(f"wrote {len(out)} rows -> runs/fluctuation_u0.parquet")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def figs() -> None:
|
|||
|
|
import matplotlib.pyplot as plt
|
|||
|
|
style.apply_style()
|
|||
|
|
clean = pd.read_parquet(RUNS / "fluctuation_u0.parquet")
|
|||
|
|
full = pd.read_parquet(sorted(RUNS.glob("2026-07-23_*_fullscale-small/results.parquet"))[-1])
|
|||
|
|
u0 = full[full.max_uncles == 0]
|
|||
|
|
|
|||
|
|
# ---- figB1: high-precision traces at k=2160 ----
|
|||
|
|
fig, ax = plt.subplots(figsize=(8.6, 4.0))
|
|||
|
|
s = clean[(clean.k_run == 2160) & (clean.replicate == 0) & (clean.epoch >= 4)]
|
|||
|
|
ax.plot(s.epoch, (s.mean_ratio - 1) * 1e3, "-o", ms=3,
|
|||
|
|
color=style.OKABE_ITO[0], label="zero delay (full mesh), U = 0")
|
|||
|
|
r = (u0[(u0.topology == "regular") & (u0.link_latency_mean == 0.1) & (u0.degree == 6)
|
|||
|
|
& (u0.replicate == 0) & (u0.epoch >= 4)])
|
|||
|
|
ax.plot(r.epoch, (r.mean_ratio - 1) * 1e3, "-s", ms=3,
|
|||
|
|
color=style.OKABE_ITO[1], label="direct gossip, 0.1-slot links, U = 0")
|
|||
|
|
sg = sigma_theory(2160) * 1e3
|
|||
|
|
ax.axhspan(-sg, sg, color="0.9", zorder=0)
|
|||
|
|
ax.axhline(0.0, color="0.5", lw=0.8)
|
|||
|
|
ax.text(119, -sg * 1.45, r"$\pm\sigma_{th} = \sqrt{(1-f)/(fT)}$", fontsize=8,
|
|||
|
|
color="0.4", ha="right")
|
|||
|
|
ax.set_xlabel("epoch")
|
|||
|
|
ax.set_ylabel(r"$(\hat D / D - 1) \times 10^{3}$ (per-mil)")
|
|||
|
|
ax.set_title("U = 0, k = 2160: per-epoch sampling noise around the ≤1 equilibrium")
|
|||
|
|
ax.legend(fontsize=8)
|
|||
|
|
style.save(fig, FIGS / "figB1_fluctuation_trace", provenance="scripts/appendix_fluct.py")
|
|||
|
|
plt.close(fig)
|
|||
|
|
|
|||
|
|
# ---- figB2: sigma vs k (left), delay progression (middle), sigma vs delay/U (right) ----
|
|||
|
|
fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(13.6, 4.0))
|
|||
|
|
for i, k in enumerate(KS):
|
|||
|
|
s = clean[(clean.k_run == k) & (clean.epoch >= 8)]
|
|||
|
|
dev = (s.mean_ratio - 1) * 1e3
|
|||
|
|
ax1.hist(dev, bins=31, density=True, histtype="step", lw=1.4,
|
|||
|
|
color=style.OKABE_ITO[i],
|
|||
|
|
label=f"k={k}: sd {dev.std()/1e3:.4f} (th {sigma_theory(k):.4f})")
|
|||
|
|
ax1.axvline(0, color="0.5", lw=0.8)
|
|||
|
|
ax1.set_xlabel(r"$(\hat D / D - 1) \times 10^{3}$")
|
|||
|
|
ax1.set_ylabel("density")
|
|||
|
|
ax1.set_title(r"noise shrinks as $1/\sqrt{T}$ (window size)")
|
|||
|
|
ax1.legend(fontsize=7)
|
|||
|
|
|
|||
|
|
rows = []
|
|||
|
|
for lat in (0.1, 0.2, 0.5, 1.0):
|
|||
|
|
s = u0[(u0.topology == "regular") & (u0.link_latency_mean == lat)
|
|||
|
|
& (u0.epoch >= 15)].mean_ratio
|
|||
|
|
rows.append(dict(case=f"gossip {lat}", mean=s.mean(), p_gt1=(s > 1).mean(),
|
|||
|
|
lo=s.quantile(0.05), hi=s.quantile(0.95)))
|
|||
|
|
b = u0[(u0.topology == "blend") & (u0.epoch >= 15)].mean_ratio
|
|||
|
|
rows.append(dict(case="Blend", mean=b.mean(), p_gt1=(b > 1).mean(),
|
|||
|
|
lo=b.quantile(0.05), hi=b.quantile(0.95)))
|
|||
|
|
dd = pd.DataFrame(rows)
|
|||
|
|
x = np.arange(len(dd))
|
|||
|
|
ax2.errorbar(x, dd["mean"], yerr=[dd["mean"] - dd.lo, dd.hi - dd["mean"]],
|
|||
|
|
fmt="o", ms=5, capsize=3, color=style.OKABE_ITO[0])
|
|||
|
|
for xi, (_, row) in zip(x, dd.iterrows(), strict=True):
|
|||
|
|
ax2.annotate(f"P(>1)={row.p_gt1:.0%}", (xi, row.hi), textcoords="offset points",
|
|||
|
|
xytext=(0, 6), ha="center", fontsize=7, color="0.35")
|
|||
|
|
ax2.axhline(1.0, color="0.5", lw=0.8, ls=":")
|
|||
|
|
ax2.set_xticks(x, dd.case, rotation=20, ha="right", fontsize=8)
|
|||
|
|
ax2.set_ylabel(r"$\hat D / D$ (U = 0, k = 2160)")
|
|||
|
|
ax2.set_title("orphan loss pulls the mean below 1;\nexcursions above 1 vanish with delay")
|
|||
|
|
|
|||
|
|
# right: per-epoch sigma (within a trajectory) vs case, U=0 vs U=1
|
|||
|
|
def per_epoch_sigma(s: pd.DataFrame) -> float:
|
|||
|
|
return float(s.groupby(["degree", "replicate"]).mean_ratio.std().mean())
|
|||
|
|
|
|||
|
|
cases: list[tuple[str, pd.DataFrame]] = []
|
|||
|
|
for lat in (0.1, 0.2, 0.5, 1.0):
|
|||
|
|
cases.append((f"gossip {lat}",
|
|||
|
|
full[(full.topology == "regular") & (full.link_latency_mean == lat)
|
|||
|
|
& (full.epoch >= 15)]))
|
|||
|
|
for dl in (1.0, 2.0, 3.0):
|
|||
|
|
cases.append((f"blend δ={dl:g}",
|
|||
|
|
full[(full.topology == "blend") & (full.blend_delay_max == dl)
|
|||
|
|
& (full.epoch >= 15)]))
|
|||
|
|
x3 = np.arange(len(cases))
|
|||
|
|
for u, marker, lbl in ((0, "o", "U = 0"), (1, "s", "U = 1")):
|
|||
|
|
sig = [per_epoch_sigma(s[s.max_uncles == u]) for _, s in cases]
|
|||
|
|
ax3.plot(x3, sig, marker, ms=6, ls="-", lw=1.0,
|
|||
|
|
color=style.OKABE_ITO[0 if u else 1], label=lbl)
|
|||
|
|
ax3.axhline(sigma_theory(2160), color="0.5", lw=0.9, ls="--")
|
|||
|
|
ax3.text(0.05, sigma_theory(2160) * 1.15, r"sampling floor $\sigma_{th}$",
|
|||
|
|
fontsize=7, color="0.4")
|
|||
|
|
ax3.set_yscale("log")
|
|||
|
|
ax3.set_xticks(x3, [c for c, _ in cases], rotation=20, ha="right", fontsize=8)
|
|||
|
|
ax3.set_ylabel(r"per-epoch $\sigma$ of $\hat D / D$")
|
|||
|
|
ax3.set_title("Blend delay amplifies U = 0 noise ~17×;\none uncle restores the floor")
|
|||
|
|
ax3.legend(fontsize=8)
|
|||
|
|
style.save(fig, FIGS / "figB2_fluctuation_stats", provenance="scripts/appendix_fluct.py")
|
|||
|
|
plt.close(fig)
|
|||
|
|
|
|||
|
|
# ---- stats for the appendix text ----
|
|||
|
|
print("=== clean zero-delay series ===")
|
|||
|
|
for k in KS:
|
|||
|
|
s = clean[(clean.k_run == k) & (clean.epoch >= 8)].mean_ratio
|
|||
|
|
print(f"k={k}: mean={s.mean():.5f} sd={s.std():.5f} (th {sigma_theory(k):.5f}) "
|
|||
|
|
f"P(>1)={(s > 1).mean():.2f} min={s.min():.4f} max={s.max():.4f}")
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
if "--run" in sys.argv:
|
|||
|
|
run()
|
|||
|
|
else:
|
|||
|
|
figs()
|