2026-07-30 18:52:01 +02:00

103 lines
5.8 KiB
Markdown
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.

# tsi-sim-mc — Cryptarchia TSI simulator (multicore build)
> **This is the multicore-optimised, reviewed copy of `../tsi-sim/`.** The original is left
> untouched. Versus the original it adds: a **sparse lottery sampler** (~30100× faster per
> epoch), a **hardened multicore sweep** (loky + single-thread BLAS), an **opt-in parallel
> chunked lottery**, an optional **spec fixed-point mode**, corrected figures, config
> validation, and a larger test suite. See "Performance & reproducibility" below.
Monte-Carlo simulation framework used to choose safe values for the **uncle-reference**
parameters of Cryptarchia's Total Stake Inference (TSI):
- `U` — max uncles referenced per block (`MAX_UNCLES`); `U=0` is the no-uncle baseline.
- `W` — uncle reference window in slots (spec default 300).
- swept against network size `N`, stake distribution `S` (uniform / Pareto), and
network **latency** `L` (in slots — deliberately *not* `D`, which denotes the stake estimate).
It measures how well the inferred total active stake `D` tracks the true total stake, and
whether uncle references recover the active slots that network latency loses to forks.
> This lives under a `raw/` docs path, so it is invisible to the repository's
> markdown-lint CI. It is a standalone Python package with its own tooling.
## Model
Reduced **canonical-chain-with-orphans** model: we simulate the global winning-slot
sequence (stake-weighted φ lottery), build a real block tree with latency- and
multi-winner-induced forks, resolve the canonical chain (honest longest-chain), let
canonical blocks reference uncles per the spec's selection rules, and count TSI density
`m = honest-chain blocks + deduplicated referenced uncles` in the measurement window.
All honest nodes converge to the same deep chain (k-finality), so a single per-epoch
`D` is faithful. A full per-node model is the planned next phase (`per_node_dest` flag
scaffolds it).
See the sibling spec `../` and `../../cryptarchia-total-stake-inference.md` for the math.
## Quick start
```bash
make install # create .venv and install (editable) with dev deps
make test # unit tests + fast theory checks
make verify # simulator vs closed-form analytic checks
make smoke # tiny scaled-k sweep + figures (end-to-end smoke test)
make sweep figures # full scaled-k parameter sweep + academic figures
```
Outputs: `results/*.parquet` (one row per config×epoch) and `figures/*.{pdf,png}`
(both git-ignored).
## Scale
True constants (`k=2160`, `f=1/30`) give 648,000-slot epochs. Mean accuracy is provably
`k`-invariant (only variance scales `~1/T`), so sweeps use a **scaled `k`**
(`configs/default.yaml`); the final accuracy/variance figures re-run at true `k`
(`configs/fullscale.yaml`, `make sweep-fullscale`). `configs/smoke.yaml` is a tiny dev grid.
## Performance & reproducibility
- **Sparse lottery (the main win).** The number of slots a node wins is `Binomial(n_slots,
p_i)` and the won slots are a uniform distinct subset — distributionally identical to an
independent Bernoulli per slot, but without the dense `(n_nodes, n_slots)` array that was
~95% of runtime. Full-scale (`k=2160`) epoch: **~3.3 s → ~0.1 s (~30×)**; at `k=256`,
**~0.39 s → ~0.009 s (~40×)**.
- **Multicore across configs (the main lever).** `run_trajectory` is a pure function of a
hash-seeded, immutable `SimConfig`, so the sweep is order-independent and embarrassingly
parallel. `run_sweep` uses joblib's process-based **loky** backend with
`inner_max_num_threads=1` (and the Makefile pins `*_NUM_THREADS=1`) to use all cores
without BLAS oversubscription. `--n-jobs -1` (default) uses every core; `--batch-size 1`
suits the small heavy full-scale grid. Measured on a 14-core box, the full scaled-`k`
sweep (`configs/default.yaml`, 3888 configs) runs in **~58 s parallel vs ~561 s serial
(9.7×)**.
- **Opt-in within-config parallelism.** `lottery_chunks > 1` splits the per-slot lottery
across slot-chunks with independent `SeedSequence.spawn` children. After the sparse fix the
lottery is a small fraction of an epoch, so this rarely helps — it exists for a single
isolated config with an enormous `n_slots`. **Reproducibility caveat:** the chunked stream
differs from the serial stream and *changes with `n_chunks`*, so `lottery_chunks` must be a
pinned, recorded parameter, never derived from the core count.
- **RNG reproducibility.** Every draw is a deterministic spawn off `SeedSequence(hash(config))`
— child 0 draws stake, child `e+1` drives epoch `e`, which spawns lottery/aux sub-streams.
Results are identical regardless of parallel scheduling order. The sparse sampler consumes
the RNG differently from the original dense one, so committed baselines here were
regenerated against the sparse sampler.
## Faithfulness notes (from review)
- `update_D(..., fixed_point=True)` mirrors the spec's integer `f`-truncation
(`int(f·1000)/1000 = 0.033`), reproducing the on-chain estimator's ~1% systematic
overestimate. Default `False` keeps the exact-`f`, analysis-faithful behaviour.
- TSI counts *blocks* (wins), so at full uncle recovery the estimate equilibrates at the
**block-count ceiling** `-ln(1-f)/f ≈ 1.017`, not 1.0 — a deterministic overshoot, not
noise. Figures overlay this ceiling; `theory.block_count_ceiling` computes it.
## Layout
```
src/tsi_sim/ constants config rng stake lottery latency blocktree uncles tsi epoch
engine metrics theory sweep verify plotting/{style,figures,make_figures}
scripts/ run_sweep.py make_figures.py verify.py (thin shims; installed as
tsi-sweep / tsi-figures / tsi-verify)
configs/ smoke.yaml default.yaml fullscale.yaml
tests/ test_{lottery,uncles,tsi_counting,blocktree,config,rng,stake,theory,
latency,theory_convergence}.py
```