Skip to content

Reform engine: structural reforms beyond YAML parameter overlay#80

Open
vahid-ahmadi wants to merge 1 commit into
mainfrom
vahid/reform-engine-structural
Open

Reform engine: structural reforms beyond YAML parameter overlay#80
vahid-ahmadi wants to merge 1 commit into
mainfrom
vahid/reform-engine-structural

Conversation

@vahid-ahmadi

Copy link
Copy Markdown
Contributor

Addresses #41

Scope

Adds a Rust reform abstraction (src/reforms/structural.rs) that goes beyond the existing YAML parameter overlay, mirroring the structural half of PolicyEngine's Python Reform system.

The new StructuralReform enum supports:

  • Parametric — wraps the existing parameter-overlay Reform, so structural and parametric changes compose (re-applies the reform's neutralise: list).
  • Neutralise — force a named benefit variable's output to zero everywhere.
  • OverrideOutput — replace a named variable's per-benunit output with a custom closure fn(old_value, &BenUnit) -> new_value (the output-level analogue of swapping a compute_* formula).
  • Compose — apply any number of structural reforms in sequence via .then(...) (flattens nested composes).

Integration

Structural reforms are applied as a post-compute transform on the finalised reform-side SimulationResults — the least-invasive hook, identical to the one the existing Reform::apply_to_results neutralisation already uses. Each variant computes a per-benunit delta against the named field, applies it, and reconciles benunit total_benefits plus the household income aggregates (net_income, net_income_ahc, extended_net_income, equivalised_*) by the same delta. The baseline run is never touched.

A small example registry (registry::disable_simulated_benefits, neutralise_benefit, halve_child_benefit, by_name) is provided, analogous to the Python policyengine_uk/reforms/ examples.

The existing parameter-override path and YAML neutralise: path are unchanged; all prior reform tests still pass.

Tests

11 new unit tests in src/reforms/structural.rs covering: neutralise zeroes a variable and reconciles aggregates; output-override applies a closure (decrease and increase); compose applies both reforms in sequence and flattens; disable_simulated_benefits zeroes means-tested benefits; validation rejects unsupported targets and recurses into composes; the parametric variant re-applies its neutralise list; registry lookup. cargo build and cargo test both pass (200 passed, 0 failed).

Remaining

  • Full mid-pipeline formula replacement — rewriting the body of a compute_* function before downstream variables read it (so the change propagates through tapers/caps) is out of scope here; this slice covers leaf-output override where the delta is reconciled afterwards.
  • Python wrapper exposure — dispatching a Python-authored Reform subclass into this Rust abstraction is not wired up; no CLI flag routes into StructuralReform yet (the API is exercised by unit tests).
  • Variables outside NEUTRALISABLE_BENEFITS (state pension, benefit cap, person-level tax) are excluded because of cross-variable feedback.

🤖 Generated with Claude Code

…pose)

Address #41 by adding `src/reforms/structural.rs`: a `StructuralReform`
enum that supports neutralising a named benefit variable, overriding its
computed output with a custom closure, composing reforms in sequence, and
wrapping the existing parameter-overlay `Reform`. Applied as a post-compute
transform on the finalised reform-side results, reconciling aggregates by
delta. Includes an example registry and unit tests. The existing parameter
override and YAML neutralise paths are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant