mirror of
https://github.com/logos-blockchain/research.git
synced 2026-08-07 11:43:20 +00:00
162 lines
7.2 KiB
Python
162 lines
7.2 KiB
Python
|
|
"""N-scaling of the one-uncle boundary: figures fig23/fig24 + §3.8 numbers.
|
|||
|
|
|
|||
|
|
Combines the direct ladder (nscaling-a/b + 32k tiers, N = 1k..32k) with the exact topology
|
|||
|
|
probe (l_mean(N, degree) to N = 10^6) and the load law rho = f*D_vis:
|
|||
|
|
fig23 — U=1 accuracy vs N per degree, case (a) vs (b), at the 8-s blending budget.
|
|||
|
|
fig24 — the ladder collapsed onto rho (validating that N enters only via l_mean), with the
|
|||
|
|
probe's rho(N) curves extrapolating each degree to 10^6 and the U=1 boundary marked.
|
|||
|
|
|
|||
|
|
Run: python scripts/nscaling_analysis.py
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
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 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
|
|||
|
|
HOPS = 3
|
|||
|
|
|
|||
|
|
|
|||
|
|
def load_ladder() -> pd.DataFrame:
|
|||
|
|
parts = []
|
|||
|
|
for stem in ("nscaling-a", "nscaling-b", "nscaling32-a", "nscaling32-b"):
|
|||
|
|
cands = sorted(RUNS.glob(f"*_{stem}/results.parquet"))
|
|||
|
|
if not cands:
|
|||
|
|
raise SystemExit(f"missing run for {stem}")
|
|||
|
|
d = pd.read_parquet(cands[-1])
|
|||
|
|
d["case"] = "b" if stem.endswith("-b") else "a"
|
|||
|
|
parts.append(d)
|
|||
|
|
df = pd.concat(parts, ignore_index=True)
|
|||
|
|
# Per-trajectory 50% burn-in (matches the report's line-55 convention). A global
|
|||
|
|
# epoch.max()//2 threshold discards the early-stopping large-N tiers entirely, which
|
|||
|
|
# dropped deg6/deg8's N=32000 points and left deg4's on a single replicate.
|
|||
|
|
_keys = ["case", "n_nodes", "degree", "blend_delay_max", "max_uncles", "replicate"]
|
|||
|
|
tail = df[df.epoch >= df.groupby(_keys).epoch.transform("max") // 2]
|
|||
|
|
eq = tail.groupby(["case", "n_nodes", "degree", "blend_delay_max", "max_uncles",
|
|||
|
|
"replicate"], as_index=False).mean_ratio.mean()
|
|||
|
|
return eq
|
|||
|
|
|
|||
|
|
|
|||
|
|
def lmean_fits(probe: pd.DataFrame) -> dict[int, tuple[float, float]]:
|
|||
|
|
"""Per-degree log-fit l_mean ~ a*ln(N) + b from the exact probe."""
|
|||
|
|
lm = probe.groupby(["n", "degree"], as_index=False).l_mean.mean()
|
|||
|
|
fits = {}
|
|||
|
|
for deg in sorted(lm.degree.unique()):
|
|||
|
|
s = lm[lm.degree == deg]
|
|||
|
|
a, b = np.polyfit(np.log(s.n), s.l_mean, 1)
|
|||
|
|
fits[int(deg)] = (float(a), float(b))
|
|||
|
|
return fits
|
|||
|
|
|
|||
|
|
|
|||
|
|
def rho_of(eq: pd.DataFrame, probe: pd.DataFrame) -> pd.DataFrame:
|
|||
|
|
# fitted l_mean covers every ladder N (the probe grid is log-spaced, not the ladder's)
|
|||
|
|
fits = lmean_fits(probe)
|
|||
|
|
m = eq.copy()
|
|||
|
|
m["l_mean"] = [fits[int(d)][0] * np.log(n) + fits[int(d)][1]
|
|||
|
|
for d, n in zip(m.degree, m.n_nodes, strict=True)]
|
|||
|
|
m["d_vis"] = HOPS * m.blend_delay_max / 2.0 + (HOPS + 1) * m.l_mean
|
|||
|
|
m["rho"] = F * m.d_vis
|
|||
|
|
return m
|
|||
|
|
|
|||
|
|
|
|||
|
|
def fig23(eq: pd.DataFrame) -> None:
|
|||
|
|
import matplotlib.pyplot as plt
|
|||
|
|
style.apply_style()
|
|||
|
|
fig, axes = plt.subplots(1, 2, figsize=(9.6, 4.2), sharey=True)
|
|||
|
|
for ax, case, title in ((axes[0], "a", "case (a): geo delays"),
|
|||
|
|
(axes[1], "b", "case (b): + 10% Poisson(3) stragglers")):
|
|||
|
|
s = eq[(eq.case == case) & (eq.blend_delay_max == 8.0) & (eq.max_uncles == 1)]
|
|||
|
|
for i, deg in enumerate((4, 6, 8)):
|
|||
|
|
g = s[s.degree == deg].groupby("n_nodes").mean_ratio.agg(["mean", "sem"])
|
|||
|
|
ax.errorbar(g.index, g["mean"], yerr=g["sem"], fmt="-o", ms=4, capsize=2,
|
|||
|
|
color=style.OKABE_ITO[i], label=f"degree {deg}")
|
|||
|
|
u2 = eq[(eq.case == case) & (eq.blend_delay_max == 8.0) & (eq.max_uncles == 2)]
|
|||
|
|
g2 = u2.groupby("n_nodes").mean_ratio.mean()
|
|||
|
|
ax.plot(g2.index, g2.values, ":", color="0.5", lw=1.2, label="U = 2 (all degrees)")
|
|||
|
|
ax.axhline(0.98, color="0.75", lw=0.8, ls="--")
|
|||
|
|
ax.text(1100, 0.982, "0.98 recovery bar", fontsize=7, color="0.5")
|
|||
|
|
ax.set_xscale("log")
|
|||
|
|
ax.set_xlabel("network size N")
|
|||
|
|
ax.set_title(title)
|
|||
|
|
ax.legend(fontsize=8, loc="lower left")
|
|||
|
|
axes[0].set_ylim(top=1.01) # bounded by 1: cap at the exact-recovery bound (above-1 is noise)
|
|||
|
|
axes[0].set_ylabel(r"stake estimate accuracy $\hat D / D$")
|
|||
|
|
fig.suptitle(r"U = 1 erodes with network size (blend, $\delta_{max}$ = 8 s, f = 1/30)",
|
|||
|
|
y=1.02)
|
|||
|
|
style.save(fig, FIGS / "fig23_nscaling_u1", provenance="scripts/nscaling_analysis.py")
|
|||
|
|
plt.close(fig)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def fig24(m: pd.DataFrame, probe: pd.DataFrame) -> None:
|
|||
|
|
import matplotlib.pyplot as plt
|
|||
|
|
style.apply_style()
|
|||
|
|
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(9.6, 4.2))
|
|||
|
|
# left: ladder accuracy vs rho, all (N, degree, delay) cells collapse onto one curve
|
|||
|
|
for case, marker, lbl in (("a", "o", "case (a)"), ("b", "s", "case (b)")):
|
|||
|
|
s = m[(m.case == case) & (m.max_uncles == 1)]
|
|||
|
|
g = s.groupby(["n_nodes", "degree", "blend_delay_max"]).agg(
|
|||
|
|
rho=("rho", "mean"), acc=("mean_ratio", "mean")).reset_index()
|
|||
|
|
ax1.scatter(g.rho, g.acc, s=18, marker=marker, alpha=0.75, label=lbl)
|
|||
|
|
ax1.axvline(1.0, color="0.6", lw=0.8, ls=":")
|
|||
|
|
ax1.text(0.98, 0.05, r"$\rho=1$", rotation=90, fontsize=7, color="0.4",
|
|||
|
|
va="bottom", ha="right", transform=ax1.get_xaxis_transform())
|
|||
|
|
ax1.set_xlabel(r"load $\rho = f\,D_{vis}(N, d, \delta)$")
|
|||
|
|
ax1.set_ylabel(r"$\hat D / D$ at U = 1")
|
|||
|
|
ax1.set_ylim(top=1.01) # bounded by 1: cap at the exact-recovery bound
|
|||
|
|
ax1.set_title("ladder collapses onto the load law")
|
|||
|
|
ax1.legend(fontsize=8)
|
|||
|
|
# right: probe rho(N) per degree to 1M, delta=8
|
|||
|
|
lm = probe.groupby(["n", "degree"], as_index=False).l_mean.mean()
|
|||
|
|
for i, deg in enumerate((4, 6, 8)):
|
|||
|
|
s = lm[lm.degree == deg].sort_values("n")
|
|||
|
|
rho = F * (HOPS * 8.0 / 2.0 + (HOPS + 1) * s.l_mean)
|
|||
|
|
ax2.plot(s.n, rho, "-o", ms=4, color=style.OKABE_ITO[i], label=f"degree {deg}")
|
|||
|
|
ax2.axhline(1.0, color="0.6", lw=0.8, ls=":")
|
|||
|
|
ax2.axhline(0.96, color="tab:red", lw=0.8, ls="--")
|
|||
|
|
ax2.text(1.0e6, 0.955, "measured U=1 failure (ρ=0.96, §3.6)", fontsize=7, color="tab:red",
|
|||
|
|
ha="right", va="top")
|
|||
|
|
ax2.set_xscale("log")
|
|||
|
|
ax2.set_xlabel("network size N")
|
|||
|
|
ax2.set_ylabel(r"load $\rho$ at $\delta_{max}$ = 8 s")
|
|||
|
|
ax2.set_title(r"exact probe: $\rho(N)$ to $10^6$ nodes")
|
|||
|
|
ax2.legend(fontsize=8, loc="upper left")
|
|||
|
|
style.save(fig, FIGS / "fig24_nscaling_probe", provenance="scripts/nscaling_analysis.py")
|
|||
|
|
plt.close(fig)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def main() -> None:
|
|||
|
|
probe = pd.read_parquet(RUNS / "topology_probe.parquet")
|
|||
|
|
eq = load_ladder()
|
|||
|
|
m = rho_of(eq, probe)
|
|||
|
|
fig23(eq)
|
|||
|
|
fig24(m, probe)
|
|||
|
|
# §3.8 numbers
|
|||
|
|
print("=== U=1 accuracy vs N (delta=8) ===")
|
|||
|
|
t = eq[(eq.blend_delay_max == 8.0) & (eq.max_uncles == 1)]
|
|||
|
|
print(t.groupby(["case", "degree", "n_nodes"]).mean_ratio.mean().round(3).to_string())
|
|||
|
|
print("\n=== probe rho(delta=8) at 1M ===")
|
|||
|
|
lm = probe.groupby(["n", "degree"]).l_mean.mean().reset_index()
|
|||
|
|
one = lm[lm.n == 1_000_000]
|
|||
|
|
for _, r in one.iterrows():
|
|||
|
|
rho = F * (HOPS * 4.0 + 4 * r.l_mean)
|
|||
|
|
print(f"degree {int(r.degree)}: l_mean={r.l_mean:.2f} rho={rho:.3f}")
|
|||
|
|
for d, a, b in ((4, 0.326, -0.23), (6, 0.192, -0.06), (8, 0.143, 0.00)):
|
|||
|
|
nstar = np.exp(((0.96 / F - HOPS * 4.0) / 4 - b) / a)
|
|||
|
|
print(f"degree {d}: U=1 failure crossing at N* ≈ {nstar:.2e}")
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|