research/tools/simulators/tsi/tsi-sim-pernode/scripts/window_scale_analysis.py

74 lines
3.1 KiB
Python
Raw Normal View History

2026-07-30 18:57:10 +02:00
"""Window sufficiency at scale + the W-as-buffer question (fig25, report §3.4).
From the window-scale sweep (N = 1 000 vs 10 000, W = 50..600, delta in {8, 16, 32} s, U in
{1, 2}, k = 256): the window floor's position is N-invariant, a wider window buys back the
near-boundary (rho ~ 1) undershoot at U = 1, and no window fixes sustained overload (rho > U).
Run: python scripts/window_scale_analysis.py
"""
from __future__ import annotations
import sys
from pathlib import Path
import pandas as pd
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from tsi_sim.plotting import style # noqa: E402
HERE = Path(__file__).resolve().parent.parent
RUNS = HERE / "runs"
FIGS = HERE / "report-figures"
BAR = 0.98
def main() -> None:
src = sorted(RUNS.glob("*_window-scale/results.parquet"))[-1]
df = pd.read_parquet(src)
# Early stop terminates each config once it converges (~epoch 16), well short of the
# nominal ``epochs`` (40). A fixed ``epoch >= 20`` tail would drop almost every config;
# take the second half of each config's *actually-run* epochs instead (matches the
# burn_frac=0.5 tail used elsewhere).
keys = ["n_nodes", "blend_delay_max", "uncle_window", "max_uncles", "replicate"]
tail_from = df.groupby(keys).epoch.transform("max") * 0.5
t = df[df.epoch >= tail_from]
eq = (t.groupby(["n_nodes", "blend_delay_max", "uncle_window", "max_uncles"],
as_index=False).mean_ratio.mean())
import matplotlib.pyplot as plt
style.apply_style()
fig, axes = plt.subplots(1, 3, figsize=(13.2, 4.0), sharey=True)
for ax, delay in zip(axes, (8.0, 16.0, 32.0), strict=True):
for n, u in ((1000, 1), (10000, 1), (1000, 2), (10000, 2)):
s = eq[(eq.blend_delay_max == delay) & (eq.n_nodes == n)
& (eq.max_uncles == u)].sort_values("uncle_window")
ax.plot(s.uncle_window, s.mean_ratio,
"-o" if u == 1 else "--s", ms=4, lw=1.3,
color=style.OKABE_ITO[0 if n == 1000 else 1],
label=f"N={n:,}, U={u}" if delay == 8.0 else None)
ax.axhline(BAR, color="0.7", lw=0.8, ls="--")
ax.set_xlabel("uncle window W (slots)")
ax.set_title(f"blending budget δ = {delay:g} s")
axes[0].set_ylabel(r"$\hat D / D$")
axes[0].text(60, BAR + 0.006, "0.98 recovery bar", fontsize=7, color="0.5")
axes[0].legend(fontsize=8, loc="lower right")
fig.suptitle("Window sufficiency at scale: a wider W buys back the ρ ≈ 1 boundary (middle) "
"but cannot fix sustained overload (right)", y=1.03)
style.save(fig, FIGS / "fig25_window_scale",
provenance=f"scripts/window_scale_analysis.py ({src.parent.name})")
plt.close(fig)
print("wrote fig25_window_scale")
for delay in (8.0, 16.0, 32.0):
s = eq[(eq.blend_delay_max == delay) & (eq.max_uncles == 1)]
piv = s.pivot(index="n_nodes", columns="uncle_window", values="mean_ratio")
print(f"\nU=1 δ={delay:g}:")
print(piv.round(3).to_string())
if __name__ == "__main__":
main()