Marcin Pawlowski de618d7ea8
The sec 8.4 capstone was still reporting pre-countable numbers
Re-running the capstone under both anchors turned up something worse than the
anchor question. The uncle-anchored arm should have bit-reproduced the
committed capstone, since its key is unchanged -- and it did not. Bisecting
against the pre-session source showed my changes are clean (identical
trajectory, epochs=21, tail 0.994358 either way); the committed parquet is
simply stale. It is dated 24 July and has no uncle_model column at all, so it
predates the countable redesign. sec 8.4's headline table has been reporting
UNRESTRICTED-model numbers ever since that landed. sec 9's "all studies re-run"
note covers the slot-counting fix of 23/24 July, not the countable redesign of
4 August, and the capstone was missed.

Corrected, countable model, delta_max = 8, U = 2, W = 10, k = 2160, 8 reps:
  honest    D-hat/D 0.996 (was 1.001)   p_ref 0.944 (was 1.000)
  30% suppress  0.994 (was 0.998)       p_ref 0.936 (was 0.990)
Lower, and p_ref materially lower, because the first-fork restriction makes
some orphans unreferenceable that the earlier model counted. No recommendation
changes: accuracy sits inside the +-0.9% per-epoch noise floor below the hard
ceiling of 1, consensus is exact, p_ref stays far above the ~0.3 the soft rule
needs. sec 6.8's p_ref quotes came from the same stale run and are corrected too.

The anchor result the re-run was for: parent-anchored gives 0.996 honest --
unchanged -- but 0.974 under the 30% suppression adversary, with p_ref 0.875.
The effects compound: a tighter effective window leaves an orphan fewer chances
to be referenced before its PARENT ages out, and a suppressing adversary
removes some of those chances. Neither isolated sweep shows it, because sec 6.12's
honest arms hold the adversary at zero and its adversarial arms use a slack
cap. That is what the capstone is for, and it is the strongest argument for
pairing the anchor change with W = 12 rather than adopting it at W = 10.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 11:06:39 +02:00

65 lines
3.1 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.

#!/usr/bin/env python
"""Capstone: the recommended configuration end-to-end at true k=2160 (report §8).
One config — f=1/30, W=300, U=2, β=1, degree 6, Blend 3 hops × 8 s, Pareto stake — run honest
and under a 30 % uncle-suppression adversary, confirming accuracy, consensus, fork rate, reorg
depth, and the emergent reference rate p_ref ALL hold together. Writes runs/capstone.parquet.
"""
from __future__ import annotations
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
import pandas as pd
from joblib import Parallel, delayed
from tsi_sim.config import SimConfig
from tsi_sim.engine import run_trajectory
REC = dict(n_nodes=1000, stake_dist="pareto", topology="blend", degree=6,
link_latency_mean=0.5, link_latency_dist="geo", blend_hops=3, blend_delay_max=8.0,
max_uncles=2, uncle_window=300, uncle_strategy="oldest", k=2160, epochs=40,
genesis_d_factor=0.5, early_stop=True)
def _one(adv: float, rep: int, anchor: str = "uncle") -> list[dict]:
cfg = SimConfig(**REC, adversary_frac=adv, adversary_strategy="suppress",
uncle_window_anchor=anchor, replicate=rep)
rows = run_trajectory(cfg)
for r in rows:
r["adv"] = adv
r["anchor"] = anchor
return rows
def main() -> None:
out = Path(__file__).resolve().parents[1] / "runs"
# Both window anchors: the spec's uncle-anchored rule, and the §6.12 proposal. The capstone
# is the "whole recipe together" check, so a change to any rule in the recipe has to be run
# through it rather than argued from the isolated sweeps.
jobs = [(a, r, w) for w in ("uncle", "parent") for a in (0.0, 0.3) for r in range(8)]
res = Parallel(n_jobs=4, backend="loky", inner_max_num_threads=1)(
delayed(_one)(a, r, w) for a, r, w in jobs)
df = pd.DataFrame([row for traj in res for row in traj])
df.to_parquet(out / "capstone.parquet", index=False)
print("=== Capstone: recommended config, all metrics together (equilibrium tail) ===")
for (anchor, adv), g in df.groupby(["anchor", "adv"]):
# Per-REPLICATE tail: early_stop ends replicates at different epochs, so a per-arm cut
# (epoch >= arm_max//2) would silently drop any replicate that stopped before the cut
# and skew the tail toward the slow-converging ones. The report's §8.4 numbers are the
# per-replicate aggregation; keep this printout matching them.
t = pd.concat([r[r.epoch >= r.epoch.max() // 2] for _, r in g.groupby("replicate")])
per_rep = t.groupby("replicate").fork_rate.mean()
sem = per_rep.std(ddof=1) / (len(per_rep) ** 0.5)
print(f"{anchor:>6}-anchored, adversary {adv:.0%}: D̂/D {t.mean_ratio.mean():.4f} "
f"range_ratio {t.range_ratio.max():.4f} agreement {t.agreement_window.min():.4f} "
f"fork_rate {per_rep.mean():.3f}+-{sem:.3f}(SEM over {len(per_rep)} reps) "
f"max_reorg_depth {t.max_reorg_depth.max()} p_ref {t.p_ref.mean():.3f}")
print(f"wrote {out/'capstone.parquet'} ({len(df)} rows)")
if __name__ == "__main__":
main()