Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
152a84f
Refactor UK event construction helpers while preserving AB2025 outputs
MaxGhenis Oct 9, 2026
3ad688b
Add accounted fiscal-event replay pipeline on the pinned UK bundle
MaxGhenis Oct 9, 2026
aeeeb79
Align event reversals and year evidence with processed fiscal parameters
MaxGhenis Oct 9, 2026
9338fdc
Harden replay accounting, provenance, completion, and interrupted res…
MaxGhenis Oct 9, 2026
09a9a85
Record accounted 2026 replay evidence and direct-head scope limits
MaxGhenis Oct 9, 2026
ac147ef
Protect scope and incidence caveats in replay reports
MaxGhenis Oct 9, 2026
1cbb9e9
Use source-head scope reasons in event comparisons
MaxGhenis Oct 9, 2026
0d45b40
Add isolated offline Modal adapter for certified UK event replays
MaxGhenis Oct 9, 2026
d82e8ef
Use uv catalog supporting the pinned Linux Python release
MaxGhenis Oct 9, 2026
4a231d9
Pin available Linux Python patch for the isolated replay worker
MaxGhenis Oct 9, 2026
7636cbd
Allow complete offline event runs and handle interrupted Modal calls
MaxGhenis Oct 9, 2026
a195b2c
Record actual byte-identical offline national replay repeats
MaxGhenis Oct 9, 2026
7a384a4
Correct fiscal-event source classifications and retain every costing …
MaxGhenis Oct 9, 2026
68b420f
Record final replay regression and classification checks
MaxGhenis Oct 9, 2026
6c59a3d
Preserve source-head scope reasons in staged event rows
MaxGhenis Oct 9, 2026
64b8e55
Record fresh VAT execution under corrected event registry
MaxGhenis Oct 9, 2026
8788976
Record ten reproduced UK model investigations and integrated evidence
MaxGhenis Oct 9, 2026
d463b53
Reconcile OBR head costings with original fiscal-event horizons
MaxGhenis Oct 9, 2026
fc7c254
Validate and adopt offline Modal event results through a reusable CLI
MaxGhenis Oct 9, 2026
62738eb
Document certified SDLT and CGT construction data flows
MaxGhenis Oct 9, 2026
ba9527c
Add the Autumn Budget 2024 results and updates left uncommitted when …
MaxGhenis Oct 9, 2026
b469ffc
Regenerate the Autumn Budget 2024 comparison and summary from the ado…
MaxGhenis Oct 9, 2026
2c231b4
Complete the four remaining recent-event replays on the certified bun…
MaxGhenis Oct 10, 2026
133613e
Fix the PR review findings and rerun three events (#156)
MaxGhenis Oct 10, 2026
35549bf
Apply ruff format and relabel completed replays (#156)
MaxGhenis Oct 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
# pypdf + pyyaml are NOT optional extras: the HMT lane's strongest
# honesty guarantee (every registry title and figure anchor verbatim
# in the real PDF) runs as a test, and a bare env silently skipped it.
- run: uv run --with pytest --with pypdf --with pyyaml --with numpy --with psutil pytest tests/ -q
- run: uv run --with pytest --with hypothesis --with pypdf --with pyyaml --with numpy --with psutil pytest tests/ -q
env:
PYTHONPATH: .
- run: git diff --exit-code
Expand Down
25,716 changes: 25,716 additions & 0 deletions data/uk/events/autumn_budget_2024_measures.json

Large diffs are not rendered by default.

27,289 changes: 27,289 additions & 0 deletions data/uk/events/autumn_statement_2023_measures.json

Large diffs are not rendered by default.

25,777 changes: 25,777 additions & 0 deletions data/uk/events/spring_budget_2023_measures.json

Large diffs are not rendered by default.

19,009 changes: 19,009 additions & 0 deletions data/uk/events/spring_budget_2024_measures.json

Large diffs are not rendered by default.

11,649 changes: 11,649 additions & 0 deletions data/uk/events/spring_statement_2025_measures.json

Large diffs are not rendered by default.

31 changes: 30 additions & 1 deletion data/uk/obr_divergence_axes.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,43 @@
"_schema": {
"description": "Per-measure decomposition registry for the OBR costings divergences (#59). Consumed by pipeline/decompose_uk_obr_divergence.py together with results/uk/obr_costings/COMPARISON.csv (PR #56). Components decompose gap = PE - OBR in GBP under the announced-measure positive_gain_to_exchequer orientation, per FY. `residual` is NOT an axis: it was both a registry axis and the quantity the pipeline computes, so a component could be filed under the name of the thing it was meant to explain. Statuses: computed = the recipe is EXECUTED by the pipeline from the comparison rows on every run (no hard-coded value, so it cannot drift from the data it claims); sized = per-FY GBP values quoted VERBATIM from a fetched primary source (no arithmetic beyond unit scaling); derived = values computed from quoted numbers, with the derivation formula stated and its confounds named; unsized = sizing recipe only. The pipeline computes residual = gap - sum(sized + derived) and classifies every valued component by direction: explains_gap (same sign as the gap) or masks_gap (opposite sign - the axis hides part of a larger like-for-like disagreement, so the raw gap UNDERSTATES the divergence; the record then carries divergence_understated=true and like_for_like_gap_gbp). Any unsized component forces decomposition_status=partial and the remainder is residual_plus_unsized; explained_share is emitted only for complete decompositions with no masking components. JSON not YAML: CI installs pytest only.",
"axes": [
"population_vintage",
"behavioural_adjustment",
"baseline_vintage",
"cy_proxies_fy",
"head_scope",
"construction_scope",
"joint_behavioural_and_scope"
],
"sign_convention": "component value = contribution to (PE - OBR); positive means the axis pushes PE above OBR"
"sign_convention": "component value = contribution to (PE - OBR); positive means the axis pushes PE above OBR",
"event_replay_axes": {
"population_vintage": {
"description": "OBR costed a measure using the forecast and population assumptions at its fiscal event. The certified replay uses one populace-uk-2023 population calibrated to later targets, uprated by policyengine-uk 2.89.2. This axis is distinct from the policy counterfactual in baseline_vintage.",
"status": "unsized",
"basis": "data/uk/certified_bundle.json; docs/uk_replay/RECIPE.md, Years and worlds",
"recipe": "Track 2 requires independently certified event-vintage rules and per-year populations. Track 1 tags this axis on every source comparison row and does not estimate or repair it. A tag is not a sized explanation."
},
"baseline_vintage": {
"description": "A reversal or delta is executed relative to the certified engine's world, whereas OBR costed against the event's announcement counterfactual.",
"status": "unsized",
"basis": "pipeline/compute_uk_event.py; each saved artifact baseline_world and reform_world"
},
"behavioural_adjustment": {
"description": "Static versus behavioural costing differences remain unsized. The replay retains the pinned employer-NIC employee_incidence=1 wage adjustment holding employer cost fixed; it is not a fixed-wage static costing. Published OBR direct measure costings can include behavioural responses but exclude separately reported indirect macroeconomic effects; incidence and head scope must be distinguished.",
"status": "unsized",
"basis": "pipeline/compute_uk_event.py; docs/uk_replay/RECIPE.md, Axis tagging; results/uk/events/SOURCE_HEAD_AUDIT.md; existing employer-NIC employee_incidence_and_base component"
},
"cy_proxies_fy": {
"description": "The replay combines calendar-year Y population inputs with government policy parameters annualized from the pinned engine's 30 April snapshot, as a proxy for published fiscal year Y-(Y+1). This does not reconstruct fiscal-year population inputs or part-year commencements after that policy snapshot.",
"status": "unsized",
"basis": "pipeline/compute_uk_event.py; source_rows.fy in data/uk/events/*_measures.json"
},
"head_scope": {
"description": "The row's mapped household variables are a documented counterpart to the OBR head, whose other channels or tax units may differ.",
"status": "unsized",
"basis": "pipeline/stage_uk_event.py; each source row's head_variables and artifact head_effects"
}
}
},
"measures": {
"autumn_budget_2024__employer_nics_package": {
Expand Down
71 changes: 71 additions & 0 deletions docs/uk_replay/CONSTRUCTION_AUDIT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# SDLT and CGT construction audit

The pinned formulas and certified inputs identify two limits relevant to the
Autumn Budget 2024 comparisons. Their national contributions remain **unsized**.
These are construction and data-flow observations, not additional model-error
diagnoses or an explained share. The
[receipt](../../results/uk/events/CONSTRUCTION_AUDIT.json) retains full source
file SHA-256 values, line-numbered excerpts, input column names and aggregates.

For SDLT, the bundle supplies `property_purchased` and
`other_residential_property_value`, but no direct main/additional purchase-price
fields. `additional_residential_property_purchased.py:12–17` multiplies the stock
of other residential property by that purchase flag. The flag's documentation
says whether all property wealth was bought in the year; its default is false.
The transaction tax formula applies the additional-property scale to the
resulting price. This uses a transaction-price proxy based on flagged wealth.
It does not charge SDLT on every household's property stock.

In the base-year inputs, 20,080 of 535,080 household rows have the flag set;
1,740 also have positive other-property wealth. The weighted purchaser count
is 943,619.65, within 28,840,551.18 weighted households. Weighted other-property
wealth is £1,140.779bn overall and £94.693bn after applying the purchase flag.
These aggregates precede the England/Northern Ireland eligibility mask,
minimum-price thresholds, annual uprating and reform aggregation. Raw row
counts include 267,540 capital-gains clone households. They are not counts of
independent observations or a national SDLT effect.

`economic_assumptions.py:35–52` copies the base population into subsequent
years. The uprating table grows other-property values with per-capita GDP but
does not change the purchase flag. This path supplies no annual purchaser
resampling or direct replacement with observed transaction-price flows. Its
relevance belongs to `construction_scope`, `head_scope` and
`population_vintage`; the audit does not size their contributions.

For CGT, `capital_gains_tax.py:34–55` takes one pooled gains amount, subtracts
the annual exemption, allocates gains by income bands and applies the main
basic/higher/additional rate parameters. The formula has no asset-type, BADR,
Investors' Relief or carried-interest branch. The bundle supplies one
`capital_gains` column, without gain-type allocation. A reversal of the main
rate parameters therefore cannot isolate announcement-affected gain types
through these inputs. This is a scope limitation, without a quantified
residual attribution.

The CGT replay is static. The loader moves the supplied gains into
`capital_gains_before_response` (`simulation.py:191,548–563`), so direct input
does not bypass the response calculation. Instead, the pinned
`capital_gains_responses/elasticity.yaml:2–3` sets elasticity to zero, and
`capital_gains_behavioural_response.py:17–18` returns zero at that setting.
The managed wrapper forwards the registry reform without an elasticity
override. Gains are subsequently uprated with per-capita GDP. This supports
the `behavioural_adjustment` tag alongside the construction and vintage axes.

The [source-head audit](../../results/uk/events/SOURCE_HEAD_AUDIT.md) separately
establishes that the small OBR CGT-head amount is a genuine disaggregated
original-event costing. The OBR whole-package yield also includes other tax
and spending heads. Neither the pooled-gains observation nor the SDLT input
proxy resolves the remaining numerical differences by itself.

Reproduce the inspection from the repository root in the pinned environment:

```bash
.venv-replay/bin/python -m pipeline.audit_uk_event_constructions --dataset data/uk/policyengine-local/policyengine_uk_data/storage/populace_uk_2023.h5
```

Use `--dataset` to point to another local copy of the same certified bytes,
and `--output` for an isolated receipt. The script hashes the dataset before
opening HDF5 and checks the installed package metadata. It reads source files
and aggregate inputs only; it imports no PolicyEngine package, constructs no
simulation and uses no network or compute-provider calls. Source hashes and
script hash in the receipt allow the inspection to be checked against the
same pinned files.
80 changes: 80 additions & 0 deletions docs/uk_replay/DETERMINISM.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Observed byte determinism

Two actual fresh replays of
`autumn_budget_2024__private_school_vat_20pct`, calendar year 2026, on Modal
produced exactly the same numerical artifact bytes as the original shared-Mac
run and its preserved canonical observation. All four SHA-256 values are:

```text
48666307c1ff045f07b39510c1dc22ab46cbd4d814320c34572d69e0a55ea931
```

The recorded gain to the Exchequer is £1,292,278,978.386702. No fields differ,
and no rounding or normalization was used in the comparison. The
[committed verification receipt](../../results/uk/events/DETERMINISM_VERIFICATION.json)
retains the original hash observation, canonical path, uploaded input
commitments, full Modal request/runtime/output receipts and inspected run
metadata.

The observation used registry SHA-256
`77b9087c3aeb9858f53dbb965d6e59632b5e75e5f05ff74774eec95e026b5d8b`.
An [immutable artifact copy](../../results/uk/events/determinism/private_school_vat_2026_registry_77b9087c.json)
preserves those exact bytes. Later classification corrections change registry
identity and artifact metadata, so this proof remains tied to its original
frozen inputs rather than asserting a future canonical file has the same SHA.

Each remote run computed a fresh certified baseline and alternate; neither
used `--resume`. Observed timings in seconds are retained exactly:

| Run | Baseline extraction | Alternate | Outer execution |
|---|---:|---:|---:|
| Modal repeat 1 | 70.739016909 | 124.811115315 | 212.35146286399998 |
| Modal repeat 2 | 82.78241082900001 | 134.181459868 | 237.268073717 |

Both runtimes were Linux x86_64 with Python 3.12.13,
policyengine 5.0.2, policyengine-core 3.27.1 and policyengine-uk 2.89.2.
Runtime network access was blocked and one worker was used. The uploaded
bundle's SHA-256 was verified before engine import and again by the managed
runner; the receipt binds the request, frozen inputs and returned file hashes.

Each request and artifact grid contains exactly this one pair. The
registry's full Autumn Budget 2024 grid contains thirty pairs, and both remote
manifests correctly record `full_event_complete=false`. This is observed
determinism for the selected pair; event coverage and subsequent reruns are
tracked separately. The inner run log's generic "local managed simulation"
label describes the reused CLI; the outer Modal receipt identifies where it
actually executed.

To reproduce later, use fresh output and download names, follow the control
environment setup in [RECIPE.md](RECIPE.md), and respect the shared limit of
two simultaneous simulations:

```bash
PYTHONPATH=. .venv-replay-checks/modal-control/bin/python -m pipeline.modal_uk_event --event autumn_budget_2024 --measures autumn_budget_2024__private_school_vat_20pct --years 2026 --output-dir .venv-replay-checks/modal/ab24/another_repeat --download-root .venv-replay-checks/modal-downloads/another_repeat --execute
```

Compare the downloaded artifact's bytes or SHA-256 with
`results/uk/events/autumn_budget_2024/autumn_budget_2024__private_school_vat_20pct_2026.json`
using the same registry identity and frozen environment. For the original
registry recorded here, use the immutable artifact copy linked above.
Keep operational run logs and receipts separate from the deterministic
numerical artifact.

## Current registry execution

A fresh third Modal execution uses the corrected registry SHA-256
`89784d34920fb2d6aae702fb4028f84d7bf87f1694df77a0c9c5d255edbcdd34`.
Its [receipt](../../results/uk/events/DETERMINISM_CURRENT_REGISTRY.json) and
[immutable artifact](../../results/uk/events/determinism/private_school_vat_2026_registry_89784d34.json)
preserve artifact SHA-256
`4aa3bc9f868865aa47e5d5f384930e74fde425e0dd443ab3e2ad4354b7943fc9`.
The only field differing from the historic artifact is `registry_sha256`;
every numerical field remains identical. No rounding or normalization was
used, and all returned-file, request and input-manifest hashes were checked.

This request again covers one of thirty pairs and records
`full_event_complete=false`. Baseline extraction took 77.743541202 seconds,
the alternate took 133.628836524 seconds, and the inner runner took
224.84171834 seconds. A second fresh execution under these same current inputs
is still required for a current-registry byte-determinism observation; the
full-event run is tracked separately until its artifacts return.
Loading
Loading