Correctness pass: derive rho in code, and the absolute vs-1.0 test

Two findings from re-reviewing the fine-delay section.

1. The rho values I put in s3.2a were wrong. The report derives
   rho = f*D_vis with D_vis = hops*delta_max/2 + (hops+1)*ell_mean from
   a MEASURED ell_mean (1.211 slots at N=1000/degree=6), not from the
   link_latency_mean parameter (0.5). Hand-substituting a guessed 1.5
   inflated every value by ~0.04: the band is rho 0.21-0.41, not
   0.25-0.45.

   To stop that recurring, graph_ell_mean moves out of
   rho_boundary_analysis.py into figures_pernode.py, joined by a new
   rho_for() that both scripts and any future quotation go through;
   plot_fine_delay.py now prints the derived rho per delay.

   This also exposed an inconsistency in the existing s3.2 table, which
   rounded delta_max=4 to "rho ~ 0.4" while s3.2a called the same cell
   0.36 and prose elsewhere already used 0.56 for delta_max=8. The s3.2
   column now carries the derived values (0.36/0.56/0.96/1.76).

2. Testing each cell against the exact target 1.0 -- the same question
   the gap test asks, without reference to the other model --
   corroborates the first-fork onset independently. Unrestricted: 1/15
   cells below 1 (t=-2.09, chance). Countable: 4/15, and not scattered
   -- delta_max=4 at U=1, and ALL THREE caps at delta_max=5 (-0.0012 to
   -0.0019, t=-2.5..-3.7). A shortfall appearing at every cap at once,
   only at the top of the band, only under the restricted model, is the
   first-fork cost seen absolutely.

   That makes "one uncle slot is sufficient -- not approximately,
   exactly" too strong as I had written it. s3.2a now states the
   residual (0.1-0.2% at the top of the band, zero below delta_max=3),
   reconciles it with the s1 headline, and notes that since all three
   caps show the same shortfall the residual is not a capacity limit.
   The bound quoted in s1 moves from "below 0.15%" to "<= 0.2%".

Also adds the new run directories to s9's canonical list, which covered
every other study but not these.

Tests: 209 passed. ruff clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marcin Pawlowski 2026-08-05 10:34:33 +02:00
parent ac6a309e58
commit 15e876e90a
No known key found for this signature in database
6 changed files with 92 additions and 40 deletions

View File

@ -8,7 +8,7 @@
---
> **The uncle-reference model analysed here.** Uncle references are *counting-only* — they never affect a block's validity ([§6.7](tsi-report-3-robustness-and-incentives.md#s6-7)[§6.8](tsi-report-3-robustness-and-incentives.md#s6-8)) — and are deduplicated by **slot**, not by block ([§8.5](#s8-5)). The reference window is derived from a **window absorption parameter**, `w_u = W_abs/f` with `W_abs = 10` expected block-intervals and bound `W_abs ≤ 0.6·k`, which is the `W = 300 = 10/f` sizing rule of [§3.4](tsi-report-2-accuracy-and-design.md#s3-4). One structural constraint shapes the results: only the **first block of a fork** — one whose parent lies on the referencing chain — is countable, which is the price of verifying every reference from chain data alone. This report calls that the **countable** model, and measures it against an **unrestricted** baseline in which any orphan in the window is referenceable at any fork depth ([§2.1](tsi-report-2-accuracy-and-design.md#s2-1), [§3.2](tsi-report-2-accuracy-and-design.md#s3-2), [§9](tsi-report-4-reproducibility-and-appendices.md#s9)). The result: **in the design regime `ρ < 1` the two are practically identical.** A dedicated 40-replicate sweep of the operating band (`δ_max` 15, `ρ ≈ 0.250.45`) puts every `U ≥ 1` cell of both models in 0.9981.001, resolves no difference in any individual cell (widest 95 % CI ±0.15 pp), and bounds the pooled first-fork cost below **0.15 %** — rising monotonically with delay and separating from zero only at the top of the band ([§3.2a](tsi-report-2-accuracy-and-design.md#s3-2a)). Differences appear only at `ρ ≥ 1`: the restriction trims ~1.4 % at `ρ ≈ 1`, and under overload (`ρ ≈ 1.8`) recovery saturates near 0.95 where the unrestricted model reaches ~1.00. Deep forks are unrecoverable by construction, so past `ρ ≈ 1` the ceiling is set by the fork *depth* profile and **no uncle cap buys it back** — a second, independent reason for the [§8](#s8) "keep `ρ < 1`" rule. The closed-form accuracy with the measured effective utilization, `log(1f)/log(1f/q_u)`, predicts every countable cell to within 0.2 %.
> **The uncle-reference model analysed here.** Uncle references are *counting-only* — they never affect a block's validity ([§6.7](tsi-report-3-robustness-and-incentives.md#s6-7)[§6.8](tsi-report-3-robustness-and-incentives.md#s6-8)) — and are deduplicated by **slot**, not by block ([§8.5](#s8-5)). The reference window is derived from a **window absorption parameter**, `w_u = W_abs/f` with `W_abs = 10` expected block-intervals and bound `W_abs ≤ 0.6·k`, which is the `W = 300 = 10/f` sizing rule of [§3.4](tsi-report-2-accuracy-and-design.md#s3-4). One structural constraint shapes the results: only the **first block of a fork** — one whose parent lies on the referencing chain — is countable, which is the price of verifying every reference from chain data alone. This report calls that the **countable** model, and measures it against an **unrestricted** baseline in which any orphan in the window is referenceable at any fork depth ([§2.1](tsi-report-2-accuracy-and-design.md#s2-1), [§3.2](tsi-report-2-accuracy-and-design.md#s3-2), [§9](tsi-report-4-reproducibility-and-appendices.md#s9)). The result: **in the design regime `ρ < 1` the two are practically identical.** A dedicated 40-replicate sweep of the operating band (`δ_max` 15, `ρ ≈ 0.210.41`) puts every `U ≥ 1` cell of both models in 0.9981.001, resolves no difference in any individual cell (widest 95 % CI ±0.15 pp), and bounds the first-fork cost at **≤ 0.2 %** — indistinguishable from zero through `δ_max` ≤ 3 and rising monotonically with delay thereafter ([§3.2a](tsi-report-2-accuracy-and-design.md#s3-2a)). Differences appear only at `ρ ≥ 1`: the restriction trims ~1.4 % at `ρ ≈ 1`, and under overload (`ρ ≈ 1.8`) recovery saturates near 0.95 where the unrestricted model reaches ~1.00. Deep forks are unrecoverable by construction, so past `ρ ≈ 1` the ceiling is set by the fork *depth* profile and **no uncle cap buys it back** — a second, independent reason for the [§8](#s8) "keep `ρ < 1`" rule. The closed-form accuracy with the measured effective utilization, `log(1f)/log(1f/q_u)`, predicts every countable cell to within 0.2 %.
<a id="s1"></a>
## 1. Executive summary

View File

@ -116,12 +116,12 @@ The U = 0 under-count **deepens with N** (more nodes → more concurrent proposa
| `δ_max` (s) | ρ | U | countable | unrestricted | gap (t) |
|---|---|---|---|---|---|
| 4 | ≈ 0.4 | 1 / 2 / 4 | 0.9979 ± 0.0015 / 0.9982 ± 0.0014 / 0.9995 ± 0.0012 | 0.9981 ± 0.0006 / 0.9985 ± 0.0012 / 0.9992 ± 0.0013 | *none resolved* (0.10.2) |
| 8 | ≈ 0.6 | 1 / 2 / 4 | 0.9965 ± 0.0015 / 0.9955 ± 0.0017 / 0.9963 ± 0.0015 | 0.9986 ± 0.0017 / 0.9993 ± 0.0016 / 0.9989 ± 0.0017 | *none resolved* (1.01.6) |
| 16 | ≈ 1.0 | 1 | 0.9647 ± 0.0065 | 0.9610 ± 0.0043 | *not resolved* (0.5) |
| 16 | ≈ 1.0 | 2 / 4 | 0.9862 ± 0.0010 / 0.9911 ± 0.0032 | 0.9999 ± 0.0005 / 1.0013 ± 0.0011 | 0.014 (12.1) / 0.010 (3.0) |
| 32 | ≈ 1.8 | 1 | 0.608 ± 0.069 | 0.570 ± 0.042 | *not resolved* (0.5) |
| 32 | ≈ 1.8 | 2 / 4 | 0.9343 ± 0.0015 / 0.9517 ± 0.0007 | 0.9774 ± 0.0064 / 1.0020 ± 0.0010 | 0.043 (6.5) / 0.050 (41.0) |
| 4 | ≈ 0.36 | 1 / 2 / 4 | 0.9979 ± 0.0015 / 0.9982 ± 0.0014 / 0.9995 ± 0.0012 | 0.9981 ± 0.0006 / 0.9985 ± 0.0012 / 0.9992 ± 0.0013 | *none resolved* (0.10.2) |
| 8 | ≈ 0.56 | 1 / 2 / 4 | 0.9965 ± 0.0015 / 0.9955 ± 0.0017 / 0.9963 ± 0.0015 | 0.9986 ± 0.0017 / 0.9993 ± 0.0016 / 0.9989 ± 0.0017 | *none resolved* (1.01.6) |
| 16 | ≈ 0.96 | 1 | 0.9647 ± 0.0065 | 0.9610 ± 0.0043 | *not resolved* (0.5) |
| 16 | ≈ 0.96 | 2 / 4 | 0.9862 ± 0.0010 / 0.9911 ± 0.0032 | 0.9999 ± 0.0005 / 1.0013 ± 0.0011 | 0.014 (12.1) / 0.010 (3.0) |
| 32 | ≈ 1.76 | 1 | 0.608 ± 0.069 | 0.570 ± 0.042 | *not resolved* (0.5) |
| 32 | ≈ 1.76 | 2 / 4 | 0.9343 ± 0.0015 / 0.9517 ± 0.0007 | 0.9774 ± 0.0064 / 1.0020 ± 0.0010 | 0.043 (6.5) / 0.050 (41.0) |
**The `U = 0` rows are a negative control.** With no uncles the two models are identical by construction — no reference is ever taken, so any measured gap is pure between-run noise (the two models draw independent RNG streams by design, so the comparison is unpaired). That control reads **+0.010 (t = 0.8) at `δ_max = 4`** but **0.230 (t = 2.1) at `δ_max = 32`**, where single replicates range from 0.05 to 0.60. Read the table against that floor: at `δ_max = 32` a gap must clear ~0.2 to mean anything on a single-replicate basis, which is exactly why the `U = 1` cells there are reported as unresolved while the tightly-clustered `U = 2` and `U = 4` cells are not.
@ -144,29 +144,31 @@ So: **at the operating loads (`ρ < 1`) no difference between the models is dete
**The question.** [§3.2](#s3-2) samples the mixing delay at 4/8/16/32. That resolves the overload regime, but it leaves the band the parameters are actually chosen in — the low-delay end, where every operating point sits — measured at four-fold spacing and five replicates, which is enough to say "no difference detected" and nothing more. How accurate is TSI across `δ_max` = 15, and how large a first-fork cost can be *excluded* there?
**The finding.** With one uncle slot the estimate is exact across the whole band: every `U ≥ 1` cell under both referencing models lands in **0.9981.001**, flat in delay. Without uncles the same band decays steeply, 0.810 → 0.640. Through `δ_max ≤ 4` the two referencing models are **indistinguishable**, and the first-fork restriction begins to cost something measurable only at the top of the band.
**The finding.** One uncle slot holds the estimate at the true stake across the whole band: every `U ≥ 1` cell under both referencing models lands in **0.9981.001**, flat in delay, while the uncle-free baseline decays 0.810 → 0.640. The two referencing models are **indistinguishable through `δ_max` ≤ 3**; at the top of the band the first-fork restriction becomes measurable and costs **0.10.2 %**. Two independent tests — the model-vs-model gap, and each model against the exact target 1.0 — agree on that onset.
`configs/fine-delay.yaml` spends replicates instead of range — `δ_max` ∈ {1,2,3,4,5}, `U` ∈ {0,1,2,4}, **40 replicates** per cell (8× [§3.2](#s3-2)), N = 1 000, blend, 3 hops — run under both models ([§9](tsi-report-4-reproducibility-and-appendices.md#s9)). The band spans `ρ ≈ 0.25` to `0.45`, entirely inside the design regime.
`configs/fine-delay.yaml` spends replicates instead of range — `δ_max` ∈ {1,2,3,4,5}, `U` ∈ {0,1,2,4}, **40 replicates** per cell (8× [§3.2](#s3-2)), N = 1 000, blend, 3 hops — run under both models ([§9](tsi-report-4-reproducibility-and-appendices.md#s9)). The band spans `ρ ≈ 0.21` to `0.41`, entirely inside the design regime.
| `δ_max` (s) | ρ | `U=0` countable / unrestricted | `U≥1` countable | `U≥1` unrestricted | pooled gap (t) |
|---|---|---|---|---|---|
| 1 | ≈ 0.25 | 0.810 / 0.806 | 0.9994 0.9999 | 0.9990 1.0010 | 0.0004 ± 0.0007 (1.1) |
| 2 | ≈ 0.30 | 0.762 / 0.763 | 0.9996 1.0002 | 0.9996 1.0003 | +0.0000 ± 0.0008 (0.0) |
| 3 | ≈ 0.35 | 0.716 / 0.723 | 0.9995 1.0002 | 0.9992 1.0004 | 0.0001 ± 0.0008 (0.2) |
| 4 | ≈ 0.40 | 0.664 / 0.668 | 0.9988 0.9996 | 0.9995 0.9999 | 0.0005 ± 0.0008 (1.3) |
| 5 | ≈ 0.45 | 0.640 / 0.623 | 0.9982 0.9988 | 0.9995 1.0003 | **0.0014 ± 0.0007 (3.7)** |
| 1 | ≈ 0.21 | 0.810 / 0.806 | 0.9994 0.9999 | 0.9990 1.0010 | 0.0004 ± 0.0007 (1.1) |
| 2 | ≈ 0.26 | 0.762 / 0.763 | 0.9996 1.0002 | 0.9996 1.0003 | +0.0000 ± 0.0008 (0.0) |
| 3 | ≈ 0.31 | 0.716 / 0.723 | 0.9995 1.0002 | 0.9992 1.0004 | 0.0001 ± 0.0008 (0.2) |
| 4 | ≈ 0.36 | 0.664 / 0.668 | 0.9988 0.9996 | 0.9995 0.9999 | 0.0005 ± 0.0008 (1.3) |
| 5 | ≈ 0.41 | 0.640 / 0.623 | 0.9982 0.9988 | 0.9995 1.0003 | **0.0014 ± 0.0007 (3.7)** |
**No individual cell resolves a model difference.** Across the 15 `U ≥ 1` cells the widest 95 % CI half-width is ±0.0015, one cell clears `t = 2` (0.75 are expected to by chance), and its `t = 2.59` does not survive the Bonferroni threshold of 2.94 for 15 tests. Read cell by cell, the honest statement is that any difference is **smaller than ±0.15 pp**.
**Pooled, a small delay-dependent cost appears.** The three uncle caps are independent measurements of the same underlying difference, so inverse-variance pooling across them buys ~√3 in precision. Pooled per delay, `δ_max` 14 stay unresolved (|t| ≤ 1.3) while **`δ_max = 5` resolves at 0.0014 ± 0.0007 (t = 3.7)** — surviving correction for the five delays tested. Over the whole band the pooled gap is 0.00048 ± 0.00033 (t = 2.8), and 11 of 15 cells are negative. So the first-fork restriction does cost something, in the direction theory predicts, and the cost is **monotone in delay and below 0.15 % everywhere in this band** — negligible against the ±0.9 % per-epoch sampling noise of [Appendix B](tsi-report-4-reproducibility-and-appendices.md#sB). (The pooling was chosen after inspecting the per-cell results; the per-delay trend, not the whole-band figure, is the defensible claim.)
**The absolute test agrees.** Asking the same question without reference to the other model — is each cell's equilibrium exactly 1.0? — reproduces the onset independently. Under the **unrestricted** model 1 of 15 cells sits significantly below 1 (`t` = 2.09, consistent with chance). Under the **countable** model 4 of 15 do, and they are not scattered: `δ_max = 4` at `U = 1` (0.0012, `t` = 2.6) and **all three uncle caps at `δ_max = 5`** (0.0012 to 0.0019, `t` = 2.5 to 3.7). A shortfall that appears at every cap simultaneously, only at the top of the band, and only under the restricted model, is the first-fork cost — the same effect the gap test resolves at `δ_max = 5`, seen from the absolute side.
**The negative control passes.** The `U = 0` arms — identical models by construction — show |gap| ≤ 0.016 with max `t` = 1.26, i.e. no spurious signal, but a 95 % CI of ±0.025: **17× wider than the entire `U ≥ 1` range.** The unrecovered regime is intrinsically noisy, which is precisely why the model comparison has to be made where uncles are active.
![Fig 34 — design-regime accuracy, δ_max 15, countable (solid) vs unrestricted (dashed) per uncle cap, error bars = replicate SEM over 40 replicates: every U ≥ 1 curve is pinned at 1.000 across the band under both models, while U = 0 (the negative control) decays 0.81 → 0.64.](report-figures/fig34_fine_delay_accuracy.png)
![Fig 34 — design-regime accuracy, δ_max 15, countable (solid) vs unrestricted (dashed) per uncle cap, error bars = replicate SEM over 40 replicates: every U ≥ 1 curve sits at 1.000 across the band under both models (to within 0.2 %), while U = 0 — the negative control — decays 0.81 → 0.64.](report-figures/fig34_fine_delay_accuracy.png)
![Fig 35 — the countable unrestricted gap with 95% CIs, zoomed to the U ≥ 1 scale, with the inverse-variance pooled estimate in black: no single cell resolves, the pooled trend is monotone in delay, and only δ_max = 5 separates from zero (0.0014 ± 0.0007).](report-figures/fig35_fine_delay_gap.png)
**What this settles.** One uncle slot is sufficient — not approximately, exactly — everywhere in the operating band, under either referencing model; the [§3.3](#s3-3) `U ≥ ⌈ρ⌉` rule has margin to spare at `ρ ≤ 0.45`. And the first-fork restriction, which [§3.2](#s3-2) shows costing 1.4 % at `ρ ≈ 1` and 5 % under overload, costs **less than 0.15 %** anywhere a deployment should be operating.
**What this settles.** One uncle slot is sufficient everywhere in the operating band under either referencing model — the [§3.3](#s3-3) `U ≥ ⌈ρ⌉` rule has margin to spare at `ρ ≤ 0.41`, and raising `U` past 1 buys nothing here (at `δ_max = 5` all three caps sit at the same 0.10.2 % shortfall, so the residual is *not* a capacity limit). And the first-fork restriction, which [§3.2](#s3-2) shows costing 1.4 % at `ρ ≈ 1` and 5 % under overload, costs **at most 0.2 %** anywhere a deployment should be operating — an order of magnitude below the ±0.9 % per-epoch sampling noise of [Appendix B](tsi-report-4-reproducibility-and-appendices.md#sB), and below the ~1 % fixed-point rounding bias of [Appendix A](tsi-report-4-reproducibility-and-appendices.md#sA). The [§1](tsi-report-1-overview-and-recommendations.md#s1) statement that one uncle restores the estimate to the true stake holds at the precision that matters; this section puts the residual at 0.10.2 % at the top of the band rather than zero.
<a id="s3-3"></a>
### 3.3 One uncle is not always enough — the load `ρ`

View File

@ -46,14 +46,14 @@ Sweep studies are committed configs, run with `make <name>` (writes a dated `run
| U = 0 fluctuation series (zero delay, k ∈ {256, 1024, 2160}) | `scripts/appendix_fluct.py --run` (`runs/fluctuation_u0.parquet`) | [Appendix B](#sB) |
| CI smoke grid + analytic sanity checks | `configs/smoke.yaml`; `scripts/verify.py` (`make verify`) — validation only, no figures | — |
| **countable vs unrestricted referencing** (accuracy over delay × U; measured `q_u`/recovery `r`) | `configs/countable-vs-old.yaml` run twice — default and with `--old`; figures + significance table by `scripts/plot_countable_vs_old.py` | [§2.1](tsi-report-2-accuracy-and-design.md#s2-1), [§3.2](tsi-report-2-accuracy-and-design.md#s3-2) |
| **fine delay band** (δ_max 15 at 40 replicates; tight CI on the model gap in the design regime) | `configs/fine-delay.yaml` run twice — default and with `--old`; figure + table by `scripts/plot_fine_delay.py` | [§3.2a](tsi-report-2-accuracy-and-design.md#s3-2a) |
| **fine delay band** (δ_max 15 at 40 replicates; tight CI on the model gap in the design regime) | `configs/fine-delay.yaml` run twice — default and with `--old`; figures + significance tables (model gap, and each model vs the exact 1.0 target) by `scripts/plot_fine_delay.py` | [§3.2a](tsi-report-2-accuracy-and-design.md#s3-2a) |
| **window absorption sweep** (`W` in expected block-intervals, `w_u = W/f` derived) | `configs/absorption-window.yaml`; figure by `scripts/plot_countable_vs_old.py` | [§3.4](tsi-report-2-accuracy-and-design.md#s3-4) |
**Uncle-model convention.** The simulator's default is the **countable** model — first-fork candidates only, derived window `w_u = W/f`, occupied-slot exclusion, per-reference counting rules ([§2.1](tsi-report-2-accuracy-and-design.md#s2-1)). The **unrestricted** baseline is preserved in the code and selected with `--old` on `tsi-sweep`/`tsi-verify`. Its RNG key is byte-identical to the pre-restriction key, so `--old` **bit-reproduces the earlier runs**: a `rho-boundary` cell (δ_max = 8, U = 2, k = 256, N = 1 000) re-run under `--old` matches the committed `2026-07-27_195627_rho-boundary` parquet with `max |Δ| = 0` on every epoch and every metric. Studies in the table above that predate the countable default were produced under the unrestricted model and reproduce exactly under `--old`; the comparison rows quantify where the two models differ, and in the design regime (`ρ < 1`) no difference is resolvable, so those findings carry over unchanged.
Because the two models draw independent RNG streams, every countable-vs-unrestricted comparison is **unpaired**, and its resolution is set by the replicate spread rather than by the effect size. Each comparison sweep therefore includes a `U = 0` arm as a **negative control**: with no uncles the models are identical by construction, so the measured `U = 0` gap is a direct reading of the noise floor at that delay and replicate count. At `δ_max = 32` with 5 replicates that floor is ≈ 0.23 in `D̂/D` — larger than several real effects elsewhere in the grid — which is why [§3.2](tsi-report-2-accuracy-and-design.md#s3-2) reports a `t` statistic per cell and why the design regime is measured separately at 40 replicates ([§3.2a](tsi-report-2-accuracy-and-design.md#s3-2a)).
All studies were **re-run on 2026-07-23/24 with the corrected slot-counting mechanism** ([§2.1](tsi-report-2-accuracy-and-design.md#s2-1)) and the early-stop optimisation; the resilient batch is `scripts/run_all_reruns.sh` (per-step log in `runs/rerun_status.log`). Canonical run directories (latest): fullscale N=5000/10000 = `2026-07-24_094519_fullscale`; fullscale N=1000/2000 = `2026-07-23_171803_fullscale-small`; uncle-window = `2026-07-24_001456`; window-uncles = `2026-07-24_014240`; block-rate = `2026-07-24_043943`; blend-hops-delay = `2026-07-24_064052`; window-scale = `2026-07-24_085234`; latency-shape = `2026-07-24_090014_expdist`; stake-tail = `2026-07-24_090044_pareto133`; heterogeneous-start = `2026-07-24_090114_default`; N-scaling = the `nscaling-{a,b}` + `nscaling32-{a,b}` runs; adversary grids = `runs/adversary_grid/`; jitter = `runs/jitter_grid/`; bootstrap = `runs/bootstrap_fullscale/`; fluctuation = `runs/fluctuation_u0.parquet`; fork-rate = `runs/fork_rate_vs_delay.parquet`; ρ-boundary = `2026-07-27_195627_rho-boundary`.
All studies were **re-run on 2026-07-23/24 with the corrected slot-counting mechanism** ([§2.1](tsi-report-2-accuracy-and-design.md#s2-1)) and the early-stop optimisation; the resilient batch is `scripts/run_all_reruns.sh` (per-step log in `runs/rerun_status.log`). Canonical run directories (latest): fullscale N=5000/10000 = `2026-07-24_094519_fullscale`; fullscale N=1000/2000 = `2026-07-23_171803_fullscale-small`; uncle-window = `2026-07-24_001456`; window-uncles = `2026-07-24_014240`; block-rate = `2026-07-24_043943`; blend-hops-delay = `2026-07-24_064052`; window-scale = `2026-07-24_085234`; latency-shape = `2026-07-24_090014_expdist`; stake-tail = `2026-07-24_090044_pareto133`; heterogeneous-start = `2026-07-24_090114_default`; N-scaling = the `nscaling-{a,b}` + `nscaling32-{a,b}` runs; adversary grids = `runs/adversary_grid/`; jitter = `runs/jitter_grid/`; bootstrap = `runs/bootstrap_fullscale/`; fluctuation = `runs/fluctuation_u0.parquet`; fork-rate = `runs/fork_rate_vs_delay.parquet`; ρ-boundary = `2026-07-27_195627_rho-boundary`. Referencing-model studies: countable-vs-unrestricted = `2026-08-04_103536_cvo-countable` / `2026-08-04_104010_cvo-old`; window absorption = `2026-08-04_104633_absorption-window`; fine delay band = `2026-08-04_191441_fine-countable` / `2026-08-04_195353_fine-old`.
Figures are in `report-figures/` (`fig1``fig29`, plus [Appendix B](#sB)'s `figB1``figB2`; numbering is generation order, not order of appearance). Committed generators: `fig1` (bootstrap, k=2160) by `scripts/bootstrap_dynamics.py`; `fig2`,`fig4`,`fig5`,`fig17``fig22` by `scripts/regenerate_extra_figs.py` from the latest sweeps (`fig3` hops×delay×U grid by `scripts/hops_delay_grid.py`, `fig6` (block-rate `U_min` grid + ρ-collapse) rendered ad hoc from `runs/2026-07-24_043943_block-rate` with no committed generator; `fig26` deficit-vs-ρ by `scripts/rho_boundary_analysis.py`) (fullscale-derived `fig17``fig20` pool both sizes in that run, N = 5 000 and N = 10 000 — the generators filter on stake_dist/topology/degree/init_dest only, never on `n_nodes`); `fig8`,`fig9` by `scripts/adversary_figs.py` from `runs/adversary_grid/`; `fig10``fig12` by `scripts/dynamic_withhold.py`; `fig13``fig15` by `scripts/selfish_mining.py`/`selfish_rewards.py`/`reward_mandate.py`; `fig16` by `scripts/stake_vs_delay.py`; `fig23``fig24` by `scripts/nscaling_analysis.py`; `fig25` by `scripts/window_scale_analysis.py`; `fig27``fig28` by `scripts/reorg_depth.py` (fork rates via `--measure`; private-chain model `src/tsi_sim/reorg.py`); `fig29` by `scripts/churn.py`; `figB1``figB2` by `scripts/appendix_fluct.py`. `fig7` (feedback fixed-point) is an analytic overlay; `fig30``fig33` (countable-vs-unrestricted accuracy, `q_u`-prediction check, recovery rate, absorption-window sweep) by `scripts/plot_countable_vs_old.py` from the `cvo-countable`/`cvo-old`/`absorption-window` runs; `fig34``fig35` (design-regime accuracy and the model gap with 95 % CIs) by `scripts/plot_fine_delay.py` from the `fine-countable`/`fine-old` runs. Every figure type the per-node simulator generates appears in this report, and the fork-rate/reorg-depth study closes the previous reproducibility gap for the adversarial figures (`fig8`,`fig9` now have committed generators from `runs/adversary_grid/`).

View File

@ -34,7 +34,7 @@ import numpy as np
import pandas as pd
from tsi_sim.plotting import style
from tsi_sim.plotting.figures_pernode import equilibrium, sem
from tsi_sim.plotting.figures_pernode import equilibrium, rho_for, sem
DELAY = "blend_delay_max"
# Normal approximation: with 40 replicates per arm the t-quantile is within ~2% of 1.96,
@ -56,6 +56,20 @@ def _cells(df: pd.DataFrame) -> pd.DataFrame:
mean_q_eff=("mean_q_eff", "mean"))
def vs_one(cells: pd.DataFrame) -> pd.DataFrame:
"""Test each U >= 1 cell against the exact target 1.0.
An independent read on the same question the gap test asks: the report's claim is that
uncle recovery restores the equilibrium to EXACTLY the true stake, so a systematic
shortfall across uncle caps is the first-fork cost seen from the absolute side rather
than differentially. 40 replicates give ~0.0005 resolution, enough to see 0.1%.
"""
u = cells[cells.max_uncles > 0].copy()
u["dev"] = u.mean_ratio - 1.0
u["t"] = u.dev / u.sem_ratio.replace(0, np.nan)
return u.sort_values(["max_uncles", DELAY])
def gaps(cnt: pd.DataFrame, old: pd.DataFrame) -> pd.DataFrame:
"""countable - unrestricted per cell, with the unpaired SE and 95% CI half-width."""
m = cnt.merge(old, on=[DELAY, "max_uncles"], suffixes=("_c", "_o"))
@ -150,10 +164,18 @@ def main() -> None:
out = Path(args.out)
out.mkdir(parents=True, exist_ok=True)
cnt, old = _cells(_load(args.countable)), _cells(_load(args.old))
cnt_raw = _load(args.countable)
cnt, old = _cells(cnt_raw), _cells(_load(args.old))
g = gaps(cnt, old)
prov = "tsi-sim-pernode fine-delay.yaml (+--old)"
# rho per delay, derived (never hand-substituted) — these are the report's axis labels.
delays = sorted(cnt[DELAY].unique())
print("load rho = f*D_vis per delay (measured ell_mean, see figures_pernode.rho_for):")
print(" " + " ".join(f"delay={d:g}: rho={r:.3f}"
for d, r in zip(delays, rho_for(cnt_raw, delays), strict=True)))
print()
written = []
written += style.save(fig_accuracy(cnt, old), out / "fine_accuracy_vs_delay", prov)
written += style.save(fig_gap(g), out / "fine_gap_vs_delay", prov)
@ -193,6 +215,18 @@ def main() -> None:
print(f"\nU=0 negative control (true gap = 0): |gap| up to {ctl.gap.abs().max():.4f}, "
f"max t = {ctl.t.max():.2f}, 95% CI +-{ctl.ci95.max():.4f} "
f"-> control {'PASSES' if ctl.t.max() < 2 else 'FAILS'}")
# Absolute test: does uncle recovery actually land on 1.0? Same question as the gap
# test, asked without reference to the other model.
for lbl, cells in (("countable", cnt), ("unrestricted", old)):
v = vs_one(cells)
lo = v[v.t <= -2]
print(f"\nvs exact 1.0, {lbl}: {len(lo)}/{len(v)} cells significantly BELOW 1")
if len(lo):
for d, s in lo.groupby(DELAY):
caps = "/".join(f"U={int(x)}" for x in sorted(s.max_uncles))
print(f" delay={d:>4g}: {caps} dev {s.dev.min():+.5f}..{s.dev.max():+.5f} "
f"t {s.t.min():.2f}..{s.t.max():.2f}")
print(f"wrote {len(written)} files -> {out}")

View File

@ -21,33 +21,14 @@ import pandas as pd
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from tsi_sim.config import SimConfig # noqa: E402
from tsi_sim.plotting import style # noqa: E402
from tsi_sim.topology import build_path_latency # noqa: E402
from tsi_sim.plotting.figures_pernode import graph_ell_mean # noqa: E402
HERE = Path(__file__).resolve().parent.parent
RUNS = HERE / "runs"
FIGS = HERE / "report-figures"
def graph_ell_mean(df: pd.DataFrame) -> float:
"""``ell_mean`` — the mean shortest-path (gossip) latency of the run's OWN peering graph.
Measured from the run's recorded ``(n_nodes, degree, link_latency_mean, link_latency_dist)``
rather than hardcoded, so the rho axis stays correct if any of those change. It is a statistical
property of the random d-regular geo graph (seed-invariant to <1% at this N), so one
representative draw suffices. Post-processing only this rebuilds the latency *graph* to read
off its mean, and never touches or re-runs the simulation.
"""
row = df.iloc[0]
cfg = SimConfig(n_nodes=int(row.n_nodes), degree=int(row.degree), topology="blend",
link_latency_mean=float(row.link_latency_mean),
link_latency_dist=str(row.link_latency_dist), k=int(row.k))
pl = build_path_latency(cfg, np.random.default_rng(0))
n = pl.shape[0]
return float(pl[~np.eye(n, dtype=bool)].mean())
def load() -> pd.DataFrame:
src = sorted(RUNS.glob("*_rho-boundary/results.parquet"))[-1]
df = pd.read_parquet(src)

View File

@ -48,6 +48,41 @@ def recovery_rate(q, q_u):
return (q_u - q) / np.maximum(1.0 - q, 1e-12)
def graph_ell_mean(df: pd.DataFrame) -> float:
"""``ell_mean`` — the mean shortest-path (gossip) latency of the run's OWN peering graph.
Measured from the run's recorded ``(n_nodes, degree, link_latency_mean,
link_latency_dist)`` rather than hardcoded, so any derived quantity stays correct if
those change. A statistical property of the random d-regular geo graph (seed-invariant
to <1% at these N), so one representative draw suffices. Post-processing only: this
rebuilds the latency *graph* to read off its mean and never re-runs the simulation.
"""
from ..config import SimConfig
from ..topology import build_path_latency
row = df.iloc[0]
cfg = SimConfig(n_nodes=int(row.n_nodes), degree=int(row.degree), topology="blend",
link_latency_mean=float(row.link_latency_mean),
link_latency_dist=str(row.link_latency_dist), k=int(row.k))
pl = build_path_latency(cfg, np.random.default_rng(0))
n = pl.shape[0]
return float(pl[~np.eye(n, dtype=bool)].mean())
def rho_for(df: pd.DataFrame, delay) -> np.ndarray:
"""Load ``rho = f * D_vis`` with ``D_vis = hops*delta_max/2 + (hops+1)*ell_mean``.
Every report quotation of ``rho`` must come through here. ``ell_mean`` is the MEASURED
mean gossip latency (1.21 slots at N=1000/degree=6), not the per-link
``link_latency_mean`` parameter (0.5) using the latter understates ``rho`` by ~0.1,
and hand-substituting a guessed value is how wrong axis labels get into a report.
"""
f = float(df.f.iloc[0])
hops = int(df.blend_hops.iloc[0])
ell = graph_ell_mean(df)
return f * (hops * np.asarray(delay, dtype=float) / 2.0 + (hops + 1) * ell)
def _lat_axis(topo: str) -> tuple[str, str]:
"""(dataframe column, axis label) for the dominant latency knob of a graph topology."""
if topo == "blend":