What the engine is
The Simulation Engine turns the National Capability Index from a static position into a dynamic one. A nation is a player. It starts from its 2026 ratings across the 73 capability groups of the National Capability Framework, earns credits each year and decides where to invest them. The world answers through the Capability Dependency Graph, decay, shocks and upside events.
The engine has two layers, kept apart on purpose:
- The game model: a formal, parameterised description of one year of a nation’s capability. It is the intellectual asset, and the thing to be defended and refined with the expert network.
- The harness: Python that plays the game without people, thousands of times, under controlled experiments. It writes a complete record of every run, checks it and reports on it. The improvement loop reads that record and proposes how the model should change.
Nothing the engine produces is a forecast. Every output is a distribution over runs of a model whose parameters are, in this version, structural judgements. The outputs serve to compare policies, to test the model and to find where it is wrong.
The parts
The diagram above shows the components and how data moves between them. In words, a run goes from left to right:
| Part | What it does | Where it is |
|---|---|---|
| Inputs | The framework’s 73 groups, the Index’s 2026 ratings for 197 nations, the scenario library, the dependency graph and the parameter seed. | data/reference/, data/capability/, data/scenarios/, data/graph/, data/sim/seed/ |
| Parameter tables | The seed compiled to versioned Parquet tables, with confidence and provenance on every row. | data/sim/*.parquet, built by pipeline/sim/world/tables.py |
| World loader | Reads the tables once and compiles one nation’s world to arrays: costs, decay, vulnerability, credits, its exposures and the graph’s lag kernels. | pipeline/sim/world/loader.py |
| Engine | One pure function, step, that advances a state by one year given the moves and the events. No files, no randomness of its own. |
pipeline/sim/engine/ |
| Policies | Who plays the nation: hold, random, greedy marginal, mirror a peer, scripted, and replay for the probes. All share one interface, so a solver can be dropped in. | pipeline/sim/policies/ |
| Scenarios | What the world does: no events, the stochastic library, scripted events, or a single stress event. | pipeline/sim/scenarios/ |
| Experiments and runner | YAML experiments in batch directories. The runner plays each across its seeds, runs the counterfactual probes and writes the run record, one process per experiment. | pipeline/sim/experiments/ |
| Run record | Eight Parquet tables per experiment, nothing summarised away, with the experiment’s lineage and a frozen copy of every table it used. | results/<batch>/<experiment>/ |
| DuckDB and the query pack | Views over every run and over the loop’s history, and the named SQL queries that every report, check and page uses. | loop/sim.duckdb, pipeline/sim/report/queries/ |
| Checks and summaries | The automated checks (invariants, plausibility, saturation, degeneracy, dead parameters, calibration, regression, performance) and the experiment and batch summaries. | pipeline/sim/report/ |
| Narrator | A report for every experiment, written from the queries, every figure citing its query. | pipeline/sim/loop/narrate.py |
| Diagnosis | Turns the checks into findings at five layers, each with evidence, a hypothesis, a proposed change, a verification and a risk. | pipeline/sim/loop/diagnose.py |
| AI reviewer | A language model reads the batch and judges every layer: parameters too high or too low, mis-shaped mechanics, and the methodology itself. | pipeline/sim/loop/ai.py |
| Master recommendation | One package of changes to run next, chosen and ordered from every recommendation of the cycle. | pipeline/sim/loop/master.py |
| Human gate | A person approves, edits or rejects the master recommendation. Nothing changes until they do. | loop/approvals.yaml |
| Ledger and history | The running state of the loop, and every finding, recommendation, check, summary and narrative of every cycle, as Parquet. | loop/ |
| Site export | The JSON the runs and improvement loop pages are built from. | src/data/sim/ |
The data
Inputs, owned elsewhere
- The framework: 73 capability groups in nine domains and three dimensions, synced from nationalcapability.org. Each group’s position in every state vector is fixed by its
node_index. - The 2026 ratings: every nation’s score on every group, 0 to 20, from the National Capability Index. These are the starting scores. They are inputs, set by the Index’s methodology, and the engine never tunes them.
- The scenario library: ten downside and ten upside scenarios. Each has a loading by domain, a likelihood band, a duration and the buffer or required groups the scenario methodology sets out. The scenario tests’ own reading of the loadings becomes the shock and upside libraries.
- The Capability Dependency Graph: the edges between groups, each with a weight, an elasticity bracket and a lag. It is reviewed and versioned separately (Capability graph).
The parameter tables
Every number in the model lives in a table, never in code, so that the loop can change a value without touching the engine and a reviewer can see what changed. The tables are built from one reviewable seed, data/sim/seed/params_v0_1.yaml. The seed sets values by domain class, then overrides them group by group, each with a rationale.
| Table | One row per | Columns |
|---|---|---|
cost_params |
group and mode | base cost per level, convexity, delivery years, whether the mode is open, confidence, provenance, defensible, rationale |
decay_params |
group | decay when built, bought and partnered; maintenance per level; confidence; provenance; note |
vulnerability_params |
group | base vulnerability, depth sensitivity, mode sensitivity |
model_params |
scalar | the frontier, the frontier premium, maintenance share, upstream discount, depth gain and erosion, propagation floor, carry decay, the credit function’s coefficients, chunk sizes, moves per round, shock and upside magnitudes, absorption, credit effects |
credit_params |
nation or global coefficient | each nation’s base credits, and the global coefficients of the credit function |
shock_library, upside_library |
event | groups hit and their load points, magnitude range, duration, yearly probability, credit effect, buffer or required groups |
shock_exposure |
event and nation | the nation’s exposure, from the scenario tests |
nation_states |
nation, year and group | score, depth, mode shares, provenance |
parameter_versions |
version | what changed, when, and the finding and evidence behind it |
Confidence is low or medium throughout version 0.1. Five groups carry defensible = false, because no cost, decay or vulnerability can be argued for them in a single-nation model: Nuclear Deterrence, Early Childhood Development, Diaspora & Global Communities, Monetary & Currency Capability, and Economic Leverage & Dependence. These are questions for the framework review, not values to tune.
The run record
Every experiment writes these tables to results/<batch>/<experiment>/. Each row carries its batch and experiment, so DuckDB reads every run of every batch as one table.
| Table | One row per | What it holds |
|---|---|---|
trajectory |
run, year, group | score, depth, mode shares, and what moved it: levels invested, decay, events, propagation |
decisions |
run, year, move | the action, group, chunk, credits, levels, whether it was paid for, the policy’s reason, its value estimate and its three best alternatives |
events |
run, year, event | the event, its type, the magnitude drawn, duration, credit effect, and the damage to each group after scaling |
propagation |
run, year, edge | points the edge delivered this year and points it still owes: the ledger that traces a change through the graph |
credits |
run, year | income and each credit group’s share of it, maintenance needed and paid, spending by mode, carried in and out, lost to shocks |
tiers |
run, year | each domain’s tier, the Tier 1 count, the rank and the tier rank |
objectives |
probe, run, year | every objective along the path, for the base run and every probe |
run_summary |
probe, run | the horizon, concentration of spending, saturation and the largest move without a shock |
Beside them sit manifest.parquet (the versions of the model, graph, parameters and shock library, the policy, nation, horizon, seeds, timestamps, git commit and run time), config.yaml, and params/, a frozen copy of every table the experiment ran on. Any run can be reproduced without the live tables.
The game model
One round is one year. The state of a nation is its score on each group (0 to 20), its depth on each (0 to 1, how institutionalised the capability is), its mode shares on each (how much was built, bought or partnered), its credits, and three queues: investments still to arrive, propagating changes still travelling the graph, and events still landing. A round runs in a fixed order.
1. Income
Credits come in at base_credits × (E / E₀)^η × (1 + event credit effects). Here E is a weighted mean of four groups: Fiscal Capacity & Public Finance (0.4), Economic Output & Productivity (0.3), Financial System Depth & Access (0.2) and Investment Attraction & Projection (0.1). E₀ is its 2026 value.
A nation’s base credits are calibrated so that holding its 2026 position costs a share of its income, leaving headroom that rises with its fiscal capacity. This is a structural calibration. The brief asks for fiscal capacity as a share of GDP, which is not yet in the repository.
2. Maintenance
Every level held costs credits each year, more steeply near the frontier. Maintenance is paid first. Whatever share goes unpaid is the share of every group that decays.
3. Moves
A policy makes up to five moves a year. Each is a chunk of income (1, 2 or 5 per cent) spent to build, buy or partner in one group:
- Build is slow, raises depth and is cheaper where the group’s upstream dependencies are strong.
- Buy is fast and leaves the capability more exposed to shocks.
- Partner sits between.
A level costs base cost × convex(score) × upstream discount. Here convex(s) = 1 + c (s/20)² + premium × max(0, s − 17)², so each level is dearer than the last and steeply so at the frontier. A move is priced at the midpoint of the levels it buys, and arrives over one to six years depending on its mode.
4. Decay
The unmaintained share of each group decays at a rate set by how the capability was acquired. Institutions decay slowly, technology quickly and infrastructure in between, and bought capability decays faster than built.
5. Events
Each shock and upside event in the library may begin each year at its yearly probability, taken from the scenario’s likelihood over ten years. An event cannot begin again while it is still running.
A shock’s damage to a group is its load points (3, 1.5 or 0.75 for a high, medium or low load) times:
- a magnitude drawn for the run,
- the nation’s exposure,
- one minus the share its buffer groups absorb, as in the scenario load test,
- the group’s vulnerability, which falls with depth and rises with the bought share.
The damage is spread over the event’s duration, and a shock that loads the financial domain also cuts income. Upside events are smaller and rarer. They scale with the nation’s readiness on the groups they require, so a breakthrough is likelier where the groundwork is strong.
6. Propagation
Changes from moves and events (not decay, which every group suffers on its own) travel the dependency graph. Each edge passes on its weight times its elasticity times the source’s relative change, spread over the following years by its lag. Elasticities are drawn within their brackets for each run. Scores are clipped to 0 to 20, and a clipped move is not passed on.
7. Objectives
Four objectives are reported, never one:
- the score sum at the horizon;
- the tier rank: the Index’s own Pareto tiering within each domain (tolerance a quarter of a point), ordered lexicographically across domains;
- the resilient score: the mean score sum across runs, so under the distribution of shocks;
- a goal-weighted score for pathway questions.
The capability index (the mean of the dimensions, each the mean of its domains, each the mean of its groups) and the rank on it are reported beside them.
Phase one plays one nation at a time. Every other nation stays at its 2026 rating, so rank and tiers are measured against a still field.
Policies and scenarios
A policy implements one function, choose(state, params, rng), and returns moves, each with a reason code, a value estimate and its alternatives where it has them:
- Hold makes no moves.
- Random draws five.
- Greedy marginal ranks every open group and mode by value per credit. The value is the level itself plus its long-run effect downstream through the graph, less the maintenance it will need, and only what arrives before the horizon counts. It is the bar any solver must beat.
- Mirror a peer closes the largest gaps to the nation five places above it.
- Scripted plays a fixed plan, such as a reconstructed historical policy.
- Replay plays back a recorded run, for the probes.
A scenario implements draw(state, rng). The stochastic scenario draws a random number for every event every year, so that runs stay aligned across policies.
The harness
Experiments and seeds
An experiment names a nation, a horizon, a policy, a scenario, a number of seeds and how elasticities are drawn. A batch is a directory of experiments; an entry may expand over a list of nations and policies.
Each run’s randomness comes from its seed, split three ways: one stream for the graph’s elasticities, one for the policy, one for the scenario. Two policies run with the same seeds therefore face the same shocks (common random numbers). The same inputs always give the same outputs, which the determinism check tests every cycle.
Counterfactual probes
To say what a decision or a shock was worth, every experiment reruns its first twenty seeds with one thing changed:
- no shocks;
- the hold policy;
- each of the five largest moves removed in turn;
- each of the three largest shocks removed in turn;
- the recorded moves replayed unchanged, which must reproduce the run exactly.
The removals replay the remaining moves open-loop, so the difference is the item’s worth with everything else held.
Speed
One step of the engine takes well under a millisecond (about 70 to 130 microseconds with five moves). A thousand runs of thirty years under the greedy policy take a few seconds on one core. A full improvement cycle (25 experiments, 2,000 runs, 3,000 probe runs, the checks, the reports and a sensitivity sweep) takes under three minutes.
Checks, reports and queries
Every batch is checked before anything is written about it. The checks are written to checks.parquet as pass, warn or fail:
| Category | Checks |
|---|---|
| Invariants | credits conserved in every round; scores within 0 to 20; no missing values; the replay probe reproduces the run; the propagation ledger sums to the propagated change |
| Plausibility | no group moves more than 3 points in a year without a shock; no domain gains more than two tiers in five years; no nation outside the top 10 reaches Tier 1 in all nine domains in 20 years; holding does not drop a frontier nation below Tier 3 |
| Saturation | groups driven to 0 or 20 and held there in more than a quarter of runs |
| Degeneracy | spending concentrated in one group; the same move in most rounds; a single-group strategy as the best policy; greedy best on every objective |
| Dead parameters | a parameter whose change of 20 per cent either way moves no objective by more than 0.5 per cent (a scheduled sweep) |
| Calibration | backcast error by nation and domain, calibration and hold-out apart |
| Regression and performance | a median objective that moved more than 5 per cent with no version change; step time against the last batch |
Each experiment’s report (report.md) is written from the query pack. It covers the headline, the starting position, what was decided, what the world did, where it ended, why (from the probes), surprises and a comparison with the hold experiment. Every figure in it names its query: trajectory_by_domain, decision_sequence, spend_by_period, close_calls, top_edges, shock_damage, shock_damage_by_group, tier_transitions, objective_distributions, probe_attribution, outlier_runs, baseline_comparison, backcast_comparison and engine_evolution.
The improvement loop
The model will be wrong, and the loop is how it becomes less wrong. It is recursive, not autonomous. Each cycle runs in six steps:
- Run the cycle batch: the null run and the baselines for five nations under four policies, always with the same seeds, so a difference between cycles is the model’s and not luck’s.
- Record everything above.
- Narrate each experiment.
- Diagnose. The checks become findings. Each states the evidence, a hypothesis, a proposed change, the expected effect, how to verify it and the risk.
- Recommend. Every finding becomes a recommendation at one of five layers, and an AI reviewer adds its own after reading the batch:
- Parameter: a number in the tables is too high or too low. For example, a decay rate is too high, the credit function too low, the shocks too weak.
- Mechanic: a function of the model is mis-shaped, such as how decay, credits, shocks, upsides or propagation work.
- Methodology: the approach itself. For example, whether the Pareto tiering adds anything over the capability index as an objective, whether the policies and experiments are the right ones, and whether the loop’s own sequence is right.
- Harness and data: the code, checks, experiments and data the harness needs, such as the reconstructed historical states that backcasting requires.
- Framework: a group or measure the model cannot represent sensibly. It is flagged for the framework review and never tuned.
- Master and gate. A master recommendation chooses among all of these and orders them into one package to run next: the changes, the experiment that tests them, the criteria for accepting them, and what is deferred and why. A person reviews it and approves, edits or rejects it in
loop/approvals.yaml. The next cycle applies an approved master’s parameter changes as one package, with one version bump recorded against the recommendations it drew on. Changes to mechanics, methodology and the harness go to an agent and a pull request.
The 2026 starting scores are fixed inputs throughout: no recommendation may change them, and none may tune to the hold-out nations.
Everything each cycle produces is kept in loop/ as Parquet: the cycles, findings, recommendations, master recommendations, checks, experiment summaries, narratives, AI insights and parameter judgements. DuckDB reads it beside every run, so the evolution of the engine can be queried as readily as a single run. loop/ledger.md is the running state: read first at the start of a cycle and written last.
The loop stops for a human review when the hold-out backcast error stops improving for three cycles, or when calibration improves while the hold-out worsens. Neither can be judged until historical starting states exist.
How it all runs together
From the repository root, with a Python that has NumPy, pandas, PyArrow, DuckDB and PyYAML:
| Command | What it does |
|---|---|
python -m pipeline.sim params |
Builds the parameter tables from the seed and reports their counts and the groups without a defensible value. |
python -m pipeline.sim sanity |
The null run against its closed form, the spectral radius, a single edge against its closed form, credit conservation and determinism. |
python -m pipeline.sim bench |
Times a step and a thousand runs. |
python -m pipeline.sim run <batch> |
Runs a batch directory and writes its record. |
python -m pipeline.sim report <batch> |
Checks, summaries and reports for a batch. |
python -m pipeline.sim cycle --sensitivity |
One improvement cycle: apply the approved master recommendation, run, check, narrate, diagnose, review and write the next master recommendation. The AI steps run when DEEPSEEK_API_KEY is set. |
python -m pipeline.sim query <name> <batch> <experiment> |
Runs one query of the pack. |
python -m pipeline.sim export |
Writes the site’s JSON from the results and the loop’s history. |
The site is then built as usual (npm run build). Results under results/ are not committed: they are rebuilt exactly from the experiments and seeds. The loop’s history, the parameter tables and the site’s export are committed.
Limitations
- Structural parameters. Every value is a judgement by domain class, not an estimate. The loop exists to test them, and the expert network to replace them.
- No backcast yet. Historical starting states for the calibration nations (South Korea, Estonia, the UAE, Singapore and Ireland) and the hold-outs (Chile and Poland) must be reconstructed before the model can be scored against history.
- One nation at a time. Other nations are frozen at 2026, so the tier rank of a rising nation is measured against a still field.
- Calibrated credits. Income is calibrated to the cost of a nation’s position, not to its fiscal capacity as a share of GDP. This favours nations that already hold a strong position.
- Defaults for depth and mode. Neither has data behind it in 2026.
- A single global graph. The graph has no nation-specific edges, and its own version changes as it is reviewed. Every run records the graph version it used.