Skip to content

Simulation engine: axes sweeps + dynamics hook#81

Open
vahid-ahmadi wants to merge 1 commit into
mainfrom
vahid/sim-axes-dynamics
Open

Simulation engine: axes sweeps + dynamics hook#81
vahid-ahmadi wants to merge 1 commit into
mainfrom
vahid/sim-axes-dynamics

Conversation

@vahid-ahmadi

Copy link
Copy Markdown
Contributor

Addresses #49.

Scope

Two new, fully-tested engine capabilities, built on top of the existing Simulation::branch + Comparison primitive (reused, not duplicated):

Axes (parameter / input sweeps) — src/engine/axes.rs

Mirrors PolicyEngine's "axes" concept. An Axis is (name, min, max, count, setter):

  • Axis::on_people(...) / Axis::on_people_where(..., select, ...) — sweep an input field (e.g. employment income £0→£200k) over all / selected people.
  • Axis::on_parameters(...) — sweep a parameter value (e.g. the basic income-tax rate).

Simulation::run_axis(&axis) linearly spaces the range, applies the setter per step, runs the engine in parallel with rayon, and returns one AxisStep { index, value, results } per point — the per-step results that back marginal-tax-rate, cliff-edge, and budget-constraint charts.

Dynamics hook — src/engine/dynamics.rs

A clean Dynamics trait for a behavioural-response pass applied between the baseline run and the reform run; apply_dynamics(passes, ...) threads passes in order over the input frame (empty = static run).

  • LabourSupplyDynamics — formalises the existing OBR Slutsky-decomposition apply_labour_supply_responses behind the trait, so the labour-supply formulas finally have a hook. No logic duplicated; honours labour_supply.enabled.
  • TakeUpDynamics { target_rate, benefits } — a deterministic benefit take-up campaign that flips would_claim_* flags for benefit units whose migration_seed is below target_rate (consistent with the engine's existing deterministic take-up gate).

Additive only — existing engine behaviour and main.rs are unchanged.

Tests

10 new unit tests (cargo test201 passed, 0 failed across both test binaries):

  • Earnings sweep yields monotone non-decreasing net income and monotone income tax.
  • Parameter sweep: raising the basic rate raises tax.
  • on_people_where only touches the selected household.
  • value_at range / single-step edge case.
  • Dynamics: empty pass is identity; labour-supply pass raises earnings on an NI cut and is a no-op when disabled; take-up campaign raises benefit spending and is a no-op at rate 0.

Remaining

  • Branched simulations — already existed (Simulation::branch + Comparison); reused here.
  • Axes — done.
  • Labour-supply dynamics — formula already existed; now formalised behind the Dynamics trait.
  • Take-up dynamics — minimal deterministic implementation done; stochastic take-up and a full apply_dynamics ordering wired into main.rs (income → MTR → response → recompute) remain follow-up slices.

🤖 Generated with Claude Code

Implement axes (parameter/input sweeps) and a Dynamics trait for
behavioural-response passes, building on the existing branch primitive.

- Axis::on_people / on_people_where / on_parameters + Simulation::run_axis
  run the engine across a linearly spaced range in parallel (rayon),
  returning per-step AxisStep results for MTR / cliff-edge / budget curves.
- Dynamics trait + apply_dynamics thread behavioural passes between baseline
  and reform; LabourSupplyDynamics wraps the existing OBR labour-supply
  formulas, TakeUpDynamics models a deterministic benefit take-up campaign.

Additive only; existing engine and main.rs behaviour unchanged. Adds 10
unit tests (monotone net income / income tax across an earnings sweep,
parameter sweep raises tax, selective sweep, dynamics direction + no-op).

Addresses #49.

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