105 lines
4.7 KiB
Python
Raw Normal View History

2026-07-30 18:57:10 +02:00
#!/usr/bin/env python
"""Full-scale bootstrap study: block production self-stabilises from any genesis guess (fig1).
Runs at the TRUE security parameter k = 2160, under the Blend transport, at N = 1 000 and
N = 5 000, WITH and WITHOUT uncle references (U = 2 vs U = 0) so the cold-start behaviour of
the deployed configuration is measured, not extrapolated, and the role of uncles during
bootstrap is visible. genesis_d_factor = initial D_est / true stake (0.01x .. 2x).
Writes runs/bootstrap_fullscale/results.parquet and renders fig1_bootstrap (block-production
rate and D_est/D per epoch; solid = U 2, dashed = U 0; one colour per genesis guess).
"""
from __future__ import annotations
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
import numpy as np
import pandas as pd
from joblib import Parallel, delayed
from tsi_sim.config import SimConfig
from tsi_sim.engine import run_trajectory
from tsi_sim.plotting import style
F = 1.0 / 30.0
EPOCHS = 12
# gdf 0.01 floods epoch 0 with ~100x blocks (memory-heavy); run it only at N = 1000.
GRID = [(1000, gdf, rep) for gdf in (0.01, 0.1, 0.5, 1.0, 2.0) for rep in range(3)] + \
[(5000, gdf, rep) for gdf in (0.1, 1.0, 2.0) for rep in range(2)]
def _one(n: int, gdf: float, u: int, rep: int) -> list[dict]:
cfg = SimConfig(n_nodes=n, k=2160, stake_dist="pareto", genesis_d_factor=gdf,
topology="blend", degree=6, blend_hops=3, blend_delay_max=8.0,
link_latency_dist="geo", link_latency_mean=0.5,
max_uncles=u, uncle_window=300, epochs=EPOCHS, replicate=rep)
rows = run_trajectory(cfg)
for r in rows:
r["gdf"] = gdf
r["u"] = u
return rows
def fig1(df: pd.DataFrame) -> None:
import matplotlib.pyplot as plt
style.apply_style()
d = df[df.n_nodes == 1000]
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(8.2, 6.2), sharex=True)
gdfs = sorted(d.gdf.unique())
for i, gdf in enumerate(gdfs):
for u, ls in ((2, "-"), (0, "--")):
Round-4 TSI report review: apply findings, editorial pass, code + figure fixes Applied the reconstructed round-4 review to the TSI parameter-selection report set (reports/tsi) and executed the follow-ups. Report (reports/tsi): - Applied the must+should findings across README + parts 1-4: cross-part numeric corrections, figure-caption fixes, spec reconciliation, and cross-file companions (hops-degradation and notch/reward numbers, tip-agreement ordering, density-window timing, VRF -> ZK Proof-of-Leadership, w_u window/reward gloss). - Editorial pass for timeless voice (no "now adopted / merged / coin" narration) and a gentle spec-safety framing (recommendations are thresholds; the protocol's MAX_UNCLES=4 sits safely above them). - Added the fork-rate-vs-scale table (6.10), defined "grinding gain", promoted the clock-skew study to its own paragraph, added the correlated-latency caveat, and moved fig27/fig28 beside their discussion. - Documented the Blend cascade in 2: hops propagate over the shared gossip graph (not direct links), the final broadcast comes from the last relay, relays are blind forwarders. Simulator (tools/simulators/tsi/tsi-sim-pernode): - Docstring/dead-code fixes: theory.block_count_ceiling (legacy framing), measure, reorg (catch-up reading), metrics (removed two dead helpers), config (fixed_point 10^-6; clock_skew_max/lottery_chunks documented inert), stake_vs_delay. - Generator correctness + regenerated figures: figures_pernode.CONFIG_COLS now exhaustive (f no longer pooled); rho_boundary_analysis SEM across replicates + hollow floored markers + de-hardcoded ell_mean (measured from the run's graph); appendix_fluct per-N sigma + ~18x title (figB2); bootstrap_dynamics driving estimate so fig1 epoch-0 matches genesis. - pytest: 186 passed; report links 528/0 dangling. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-31 13:13:03 +02:00
# Both panels are indexed by the estimate that DROVE each epoch's production: the
# start-of-epoch estimate `mean_ratio_in` (block rate depends on it, and at epoch 0 it
# IS the genesis guess, matching the legend). Plotting end-of-epoch `mean_ratio` here
Review fixes + high-precision design-band delay study Acts on a correctness/completeness review of the countable uncle model and its report material. Correctness fixes in the report: - s3.4 quoted 0.998 for W_abs=10 at the 8s budget; the run says 0.9963. - s1 claimed both models >= 0.996 at U >= 1; countable U=2 delta=8 is 0.9955. Corrected to >= 0.995. - The s3.2 table presented two cells (U=1 at delta 16 and 32) as model differences. They are not resolvable: t = 0.46 and 0.47 over 5 replicates. The table now carries +-SEM and a t per cell. - s3.4 claimed the ~7-block-interval floor "carries over unchanged". Accuracy is still climbing past W=7 at every delay (8s: 0.989 -> 0.996), so the claim is dropped. The 32s curve is non-monotonic with replicate SD up to 0.22 and is now flagged as noise, not a trend. - 1-r was attributed to the first-fork restriction alone; it is the combined first-fork and capacity loss, which this measurement cannot separate. Hedged to match fig32's own axis label. Completeness: the U=0 negative control was swept but never reported. With no uncles the two models are identical by construction, yet they differ by -0.23 at delta_max=32 (t=2.1) because they draw independent RNG streams. That is the noise floor the rest of the grid must clear, and it is now in s3.2, s9, fig30 and the config header. New study (configs/fine-delay.yaml, scripts/plot_fine_delay.py, s3.2a, fig34/fig35): the design band delta_max 1-5 at 40 replicates, both models. Findings: every U >= 1 cell of both models lands in 0.998-1.001, flat in delay, while U=0 decays 0.810 -> 0.640. No individual cell resolves a model difference (widest 95% CI +-0.15pp; max t=2.59 vs Bonferroni 2.94 over 15 cells). Pooled across uncle caps the first-fork cost is monotone in delay and separates from zero only at delta_max=5 (-0.0014 +- 0.0007, t=3.7) -- below 0.15% everywhere in the band, against +-0.9% per-epoch sampling noise. Code: - deep_ref_share is identically 0 on every real countable run: for a chain block B the producer's chain below B is the counting chain below B, so the counting-side parent-on-chain re-check cannot reject what selection emitted. It is a drift alarm, not a rate. Documented as such in measure.py, the plot docstring and the config header, and pinned by a new end-to-end test. - Removed annotate_uncles: a second countable implementation that production never called, while carrying most of the selection test coverage. Tests now drive select_uncles_at_production through an annotate_via_production replay helper -- same assertions, live path. - Added tests for the two previously uncovered branches of the live selection: the pmin/below chain walk that resolves parent-on-chain for candidates whose parent sits below the window, and the occupied-slot exclusion built from the chain walk. - theory.q_effective and theory.window_miss_prob were unused and untested. Now used (the prediction figure reconstructs q_u through the identity the report quotes) and tested. The window_miss_prob test records that its "~ e^-W" docstring is the f->0 limit: the true decay is e^-1.017W at f=1/30, 16% off by W=10. - Shared sem()/recovery_rate() moved into figures_pernode.py; fig30 and fig33 regenerated with SEM error bars and the U=0 control curve. - Fixed the pre-existing E501 in bootstrap_dynamics.py; ruff clean. Report prose reworked to read standalone: the countable model is described as the rules under analysis and the former model as a labelled "unrestricted" comparison baseline, with no dated banners and no round-to-round narration. Tests: 209 passed (was 202). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 20:48:38 +02:00
# would show the already-updated value at epoch 0 and offset the panels by one epoch.
2026-07-30 18:57:10 +02:00
s = (d[(d.gdf == gdf) & (d.u == u)]
Round-4 TSI report review: apply findings, editorial pass, code + figure fixes Applied the reconstructed round-4 review to the TSI parameter-selection report set (reports/tsi) and executed the follow-ups. Report (reports/tsi): - Applied the must+should findings across README + parts 1-4: cross-part numeric corrections, figure-caption fixes, spec reconciliation, and cross-file companions (hops-degradation and notch/reward numbers, tip-agreement ordering, density-window timing, VRF -> ZK Proof-of-Leadership, w_u window/reward gloss). - Editorial pass for timeless voice (no "now adopted / merged / coin" narration) and a gentle spec-safety framing (recommendations are thresholds; the protocol's MAX_UNCLES=4 sits safely above them). - Added the fork-rate-vs-scale table (6.10), defined "grinding gain", promoted the clock-skew study to its own paragraph, added the correlated-latency caveat, and moved fig27/fig28 beside their discussion. - Documented the Blend cascade in 2: hops propagate over the shared gossip graph (not direct links), the final broadcast comes from the last relay, relays are blind forwarders. Simulator (tools/simulators/tsi/tsi-sim-pernode): - Docstring/dead-code fixes: theory.block_count_ceiling (legacy framing), measure, reorg (catch-up reading), metrics (removed two dead helpers), config (fixed_point 10^-6; clock_skew_max/lottery_chunks documented inert), stake_vs_delay. - Generator correctness + regenerated figures: figures_pernode.CONFIG_COLS now exhaustive (f no longer pooled); rho_boundary_analysis SEM across replicates + hollow floored markers + de-hardcoded ell_mean (measured from the run's graph); appendix_fluct per-N sigma + ~18x title (figB2); bootstrap_dynamics driving estimate so fig1 epoch-0 matches genesis. - pytest: 186 passed; report links 528/0 dangling. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-31 13:13:03 +02:00
.groupby("epoch").agg(rate=("n_blocks", "mean"), ratio=("mean_ratio_in", "mean")))
2026-07-30 18:57:10 +02:00
rate = s.rate / (10 * int(2160 / F)) # blocks per slot
ax1.plot(s.index, rate, ls, color=style.OKABE_ITO[i], lw=1.4, ms=3,
marker="o" if u == 2 else None,
label=f"{gdf:g}×" if u == 2 else None)
ax2.plot(s.index, s.ratio, ls, color=style.OKABE_ITO[i], lw=1.4, ms=3,
marker="o" if u == 2 else None)
ax1.axhline(F, color="0.5", lw=0.9, ls=":")
ax1.text(EPOCHS - 0.4, F * 1.25, "target f", fontsize=8, color="0.4", ha="right")
ax1.set_yscale("log")
ax1.set_ylabel("block production (blocks / slot)")
ax1.set_title("Bootstrap at full scale (k = 2160, Blend, N = 1000): "
"solid = U 2, dashed = U 0")
ax1.legend(fontsize=8, title="genesis D̂ / D", ncols=5)
ax2.axhline(1.0, color="0.5", lw=0.9, ls=":")
ax2.set_yscale("log")
ax2.set_xlabel("epoch")
ax2.set_ylabel(r"$\hat D / D$")
style.save(fig, Path(__file__).resolve().parents[1] / "report-figures" / "fig1_bootstrap",
provenance="scripts/bootstrap_dynamics.py (k=2160)")
plt.close(fig)
def main() -> None:
out = Path(__file__).resolve().parents[1] / "runs" / "bootstrap_fullscale"
out.mkdir(parents=True, exist_ok=True)
jobs = [(n, g, u, r) for (n, g, r) in GRID for u in (0, 2)]
results = Parallel(n_jobs=3, backend="loky", inner_max_num_threads=1)(
delayed(_one)(n, g, u, r) for n, g, u, r in jobs)
df = pd.DataFrame([row for traj in results for row in traj])
df.to_parquet(out / "results.parquet", index=False)
fig1(df)
# settle epochs: first epoch with block rate within 10% of f, per (n, gdf, u)
el = 10 * int(2160 / F)
df["rate"] = df.n_blocks / el
st = (df.assign(ok=lambda x: (x.rate - F).abs() <= 0.1 * F)
.groupby(["n_nodes", "gdf", "u", "replicate"])
.apply(lambda g: int(g[g.ok].epoch.min()) if g.ok.any() else np.nan,
include_groups=False))
print("settle epoch (first epoch within 10% of f):")
print(st.groupby(["n_nodes", "gdf", "u"]).mean().round(2).to_string())
print(f"wrote {out/'results.parquet'} ({len(df)} rows) and fig1_bootstrap")
if __name__ == "__main__":
main()