Skip to content

feat: compute PIP daily-living and mobility amounts from flags#56

Merged
vahid-ahmadi merged 3 commits into
mainfrom
vahid/pip-from-flags
Jun 1, 2026
Merged

feat: compute PIP daily-living and mobility amounts from flags#56
vahid-ahmadi merged 3 commits into
mainfrom
vahid/pip-from-flags

Conversation

@vahid-ahmadi
Copy link
Copy Markdown
Contributor

Summary

Addresses part of #44. PIP amount fields are now computed from the existing eligibility flags + a new PipParams parameter struct. Stacked on #55.

Before: setting pip_dl_enh: true on a synthetic household produced £0 PIP because pip_daily_living was only ever populated from FRS recorded amounts.

After:

from policyengine_uk_compiled import Simulation
sim = Simulation.from_situation({
    "people":     {"p": {"age": 35, "pip_dl_enh": True, "pip_mob_std": True}},
    "benunits":   {"b": {"members": ["p"]}},
    "households": {"h": {"members": ["p"], "region": "London"}},
}, year=2025)
sim.run_microdata().benunits.loc[0, "baseline_total_benefits"]
# £7,259.20 (= £5,740.80 DL enhanced + £1,518.40 mobility standard,
#            plus modelled means-tested benefits as applicable)

Reforms flow through too: doubling daily_living_enhanced_weekly doubles the synthetic household's PIP DL amount.

What's included

Engine (src/parameters/mod.rs, src/variables/benefits.rs):

  • PipParams struct with the four 2025/26 weekly rates
  • pip_daily_living_amount(p, params) and pip_mobility_amount(p, params) helpers:
    • Recorded amount > 0 → pass through unchanged (preserves FRS calibration; an FRS-claimant on partial-year PIP keeps their reported amount)
    • Amount 0 + flag set → compute from params.pip × 52
    • No params loaded or no flag → return 0
  • passthrough_benefits now uses the helpers, so PIP from flags flows into total_benefits and downstream net income

Parameters (parameters/2025_26.yaml): rates from 7 April 2025 per gov.uk/pip/what-youll-get under WRA 2012 s.79 / SI 2013/377:

component std enh
daily living £73.90/wk £110.40/wk
mobility £29.20/wk £77.05/wk

Python wrapper (models.py, __init__.py): new PipParams class exposed under Parameters.pip.

Rust unit tests (8): enhanced/standard/recorded-override/no-flag/no-params and reform-scaling for both DL and mobility, plus a passthrough-flow test on a benunit.

YAML policy-test cases (4) in tests/policy/pip.yaml: DL+mob combinations end-to-end through the binary, with would_claim_*: false on the benunit to isolate PIP from modelled means-tested benefits.

Verified locally

  • cargo test: 149 passing (141 + 8 new)
  • pytest interfaces/python/tests: 79 passing
  • python -m policyengine_uk_compiled.yaml_tests tests/policy: 21/21
  • End-to-end: synthetic household with pip_dl_enh + pip_mob_std produces £7,259.20 passthrough benefit (= £5,740.80 + £1,518.40, matching the 2025/26 weekly rates exactly)

Stacking

vahid/pip-from-flagsvahid/lbtt-ltt (#55) ← vahid/yaml-test-harness (#54) ← vahid/parity-harness (#53) ← vahid/from-situation (#52). Five-deep stack; once each merges in order, this rebases onto main.

Out of scope (follow-ups for #44)

  • Same flag-based computation for DLA, AA, and Carer's Allowance (each has its own flag set on Person)
  • Reform scaling of FRS-recorded amounts (mirroring the state-pension old_basic_pension_weekly baseline-scaling pattern)
  • PIP eligibility derivation from FRS disability indicators (currently the flags are read directly from FRS recorded benefit amounts in src/data/clean.rs)
  • LCWRA / disability premium passporting beyond what already exists

Test plan

  • CI passes (cargo + pytest + parity smoke + YAML cases)
  • Manual sanity-check: setting pip_dl_enh: true on a synthetic household yields ~£5,741 baseline_total_benefits in addition to any modelled benefits
  • FRS households unchanged (recorded amounts pass through, no scaling)

🤖 Generated with Claude Code

Until now, PIP amount fields (`pip_daily_living`, `pip_mobility`) were
only populated from FRS recorded values; setting an eligibility flag on
a synthetic household built via `from_situation` produced £0 PIP, and
PIP-rate reforms had no effect even on FRS data when the recorded amount
sat outside the modelled rate.

This change adds:
- `PipParams` Rust struct (and Python wrapper class) with the four PIP
  weekly rates: daily-living standard/enhanced and mobility standard/enhanced
- 2025/26 rates per gov.uk/pip/what-youll-get sourced under Welfare
  Reform Act 2012 s.79 / SI 2013/377
- `pip_daily_living_amount` and `pip_mobility_amount` helpers in
  `src/variables/benefits.rs` that:
  - Pass through any FRS-recorded amount unchanged (preserves existing
    calibration behaviour)
  - Otherwise compute from the eligibility flag × the rate parameter
  - Return 0 when neither holds or `params.pip` is unset
- `passthrough_benefits` now uses these helpers, so PIP from flags flows
  into total_benefits and downstream household net income

Tests:
- 8 Rust unit tests covering the std/enh/recorded-override/no-flag/no-
  params/reform-scaling paths
- 4 YAML policy-test cases covering the same paths end-to-end

Stacked on #55 (LBTT/LTT).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vahid-ahmadi vahid-ahmadi force-pushed the vahid/pip-from-flags branch from 710db92 to 5b4b4cb Compare May 29, 2026 09:17
vahid-ahmadi added a commit that referenced this pull request May 29, 2026
Extends the pattern from #56 (PIP) to:
- DLA care component (low/mid/high) — SSCBA 1992 Sch.2 para.2
- DLA mobility component (low/high) — SSCBA 1992 Sch.2 para.3
- Attendance Allowance (low/high) — SSCBA 1992 s.64

Synthetic households that set `dla_care_*` / `dla_mob_*` / `aa_*`
eligibility flags now produce non-zero amounts via the new
`DlaParams` and `AaParams` structs (with 2025/26 weekly rates from
gov.uk). FRS-recorded amounts continue to pass through unchanged.

Adds:
- 2025/26 rates in `parameters/2025_26.yaml`:
  DLA care low/mid/high £29.20/£73.90/£110.40 weekly,
  DLA mob low/high £29.20/£77.05 weekly,
  AA low/high £73.90/£110.40 weekly
- Helpers `dla_care_amount`, `dla_mobility_amount`,
  `attendance_allowance_amount` in `src/variables/benefits.rs`
- 10 Rust unit tests (recorded-override / no-flag / per-band-rate /
  passthrough flow)
- 4 YAML policy-test cases under `tests/policy/dla_aa.yaml`
- Python wrapper exposure (`DlaParams`, `AaParams`, `Parameters.dla`,
  `Parameters.aa`)

Stacked on #56.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vahid-ahmadi vahid-ahmadi changed the base branch from vahid/lbtt-ltt to main May 29, 2026 09:20
@vahid-ahmadi
Copy link
Copy Markdown
Contributor Author

Rebased onto main — ready to merge.

Previously stacked behind the LBTT/PIP chain; now sits directly on main and is mergeable independently. The PIP daily-living/mobility helpers are correct: enhanced-before-standard precedence, recorded-amount passthrough (FRS values preserved), graceful no-params handling, weekly×52. The four 2025/26 weekly rates match gov.uk exactly. Fully additive — Option param with serde(default), so existing param files are unaffected. Verified: Rust builds, PIP unit tests pass, Python wrapper imports cleanly. Test coverage is strong (8 unit tests + YAML cases).

@vahid-ahmadi vahid-ahmadi merged commit d655bc8 into main Jun 1, 2026
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