Repository navigation
Remove the dead obr_elasticities labour supply switch - #2413
Merged
Merged
Conversation
gov.simulation.labour_supply_responses.obr_elasticities ("Use the OBR's
progression elasticities for the labour supply responses") has had no
reader since it was added in #993: setting it changed no output. The OBR
elasticities are applied by Simulation.apply_dynamics under
gov.dynamic.obr_labour_supply_assumptions, which this leaves unchanged.
Add a code-health test that runs the elasticity-based response and the
OBR dynamics on a small household, records every parameter read, and
fails if any parameter under gov.simulation.labour_supply_responses or
gov.dynamic was never read. A control parameter nothing reads must stay
unread, so the recorder cannot pass by counting the whole tree.
Fixes #2412
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Register gov.simulation.labour_supply_responses.obr_elasticities in REMOVED_PARAMETERS, the repo's registry for removed parameters, so a reform or saved policy that still names it (the app-v2 parameter tree listed it) fails with a message pointing to gov.dynamic.obr_labour_supply_assumptions instead of a bare lookup error. The stub is hidden from the app (economy and household false). add_removed_parameter_aliases was hard-wired to the State Pension age node; it now places each stub under its own parent, and each entry carries its own label. check_parameter_not_removed checks a legacy labor_supply_responses path as its labour_supply_responses equivalent, so the legacy alias gives the same directions at every entry point. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Walk every path under gov, the legacy labor_supply_responses alias included, and check that check_parameter_not_removed rejects a path exactly when a removed-parameter stub sits there, and that every registered path has a stub. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- add_removed_parameter_aliases raises a ValueError telling you to delete the YAML when a path in REMOVED_PARAMETERS still holds a parameter, instead of an AttributeError at import. - Note on RemovedParameterNode that it reads as an empty node at an instant, so its parent can still be read. - The OBR elasticities message says apply_dynamics acts on a simulation with a baseline. - The read recorder patches the classes with MonkeyPatch.context(), so it leaves no attribute behind; the recorder test checks that. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
marked this pull request as ready for review
October 14, 2026 19:19
Collaborator
Author
|
Merge audit, head
🤖 Generated with Claude Code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2412
What
gov.simulation.labour_supply_responses.obr_elasticities("Use the OBR's progression elasticities for the labour supply responses") is a switch that nothing reads. It has had no reader since #993 added it in October 2024, so setting it to 1 changed no output. This PR:Deletes the YAML.
Registers the path in
REMOVED_PARAMETERS(utils/parameters.py), the repo's registry for removed parameters. A reform or saved policy that still names it, under the current or legacylabor_supply_responsespath, now fails with directions instead of a bareCould not find the parameter …error:The stub left at the path is hidden from the app (
economyandhouseholdfalse). If a path is listed inREMOVED_PARAMETERSwhile its YAML still exists, the system now fails to build with aValueErrorsaying to delete the YAML. Before, that case failed at import with anAttributeError. To support this,add_removed_parameter_aliases, which was hard-wired to the State Pension age node, now places each stub under its own parent. Each entry carries its own label, and the label of the existing State Pension age stubs is unchanged.check_parameter_not_removedchecks a legacylabor_supply_responsespath as itslabour_supply_responsesequivalent.Adds a guard so dead switches cannot come back (
tests/code_health/test_labour_supply_parameters_are_read.py). Every parameter undergov.simulation.labour_supply_responsesand undergov.dynamicmust be read by a formula or by the dynamics.The working switch,
gov.dynamic.obr_labour_supply_assumptions, and the OBR dynamics (dynamics/labour_supply.py,dynamics/progression.py) are unchanged.Why delete rather than alias
The issue's rule was to delete unless an app still references the parameter. Nothing does. I searched for
obr_elasticitiesunder both paths at origin HEAD on 2026-10-14: policyengine-app-v2, policyengine.py, policyengine-api, policyengine-api-v2, policyengine-api-v2-alpha, policyengine-uk-data, microcosm and the archived policyengine-app (v1) returned 0 hits. The one org-wide code-search hit,OBR_ELASTICITIES_URLin uk-childcare-100k-threshold, is a JS constant for a link to an OBR PDF. Within this package,git grepfinds no reader, andgit log -Sfinds no commit that ever added one.Saved policies are a separate surface. app-v2's parameter tree (
buildParameterTree.ts) lists any parameter flaggedeconomyorhousehold, so a user could have set this one; search hid it because it had no label. Such a policy used to run with no effect. It now fails with the directions above, which is why the registry entry is part of this PR.How the guard works
Over a small household it runs both labour supply paths: the elasticity-based response (
employment_income_behavioral_response, with non-zero elasticities and a basic rate rise), andSimulation.apply_dynamicswith the OBR assumptions on. It records every parameter read on the parameter tree itself, so a read counts whether a formula makes it throughparameters(period)or the dynamics make it throughsim.tax_benefit_system.parameters. It patches three hooks for the duration of the run:ParameterNodeAtInstant;parameter(instant)call;Building a node at an instant counts as no read. A control parameter injected into both runs' trees must stay unread, so the guard cannot pass by counting the whole tree.
Invariants (stated, then executed)
gov.simulation.labour_supply_responsesandgov.dynamicis read by a formula or by the dynamics in the scenario.gov, legacy aliases included,check_parameter_not_removedrejects the path exactly when a removed stub sits there, and every registered path has a stub. The walk excludes the tree's internalbaselinecopy, whosebaseline.-prefixed stub paths the check does not reject. That gap predates this PR (the State Pension age stubs have it too), and a write that reaches one of those stubs still raises with directions throughRemovedParameterNode.update.Simulationconstructor paths raisesValueErrorwith directions, under both paths. Neighbouring LSR parameters stay reformable under both paths.Mutation checks (run on scratch
git archiveexports)origin/main(switch still present)['gov.simulation.labour_supply_responses.obr_elasticities']gov.simulation.labour_supply_responses.dead_switchandgov.dynamic.dead_dynamic_switchrelative_income_changestops readingbounds.income_changebounds.income_changecheck_parameter_not_removedImpact
No model output changes. No code read the parameter:
git grepfinds no reader, and the guard records no read of it onmain. No microsimulation was run, because there is no number to report.Tests run locally
aca90a49c, withuv run --frozen --extra dev pytest -n 4: 131 passed across the guard, the removal tests,test_removed_state_pension_age_parameters.pyandtest_behavioral_responses.py.d9706642f: the 74 non-microsimulation Python test files that touch labour supply, parameters, scenarios or metadata. 4821 passed, 68 skipped, 0 failed (75 minutes, 4 workers). Later commits add only tests and the review fixes below.76f6aba56.Independent review
Every Subfleet lane was closed (Codex until about 19:30Z, Claude unavailable), so the rules' fallback reviewer, an Opus 5.5 agent, reviewed
76f6aba56: APPROVE with five nits. Its checks:reset_parametersthenprocess_parameters, deepcopy, pickle andcapital_gains_projection.build_parametershides the stub.The four code nits are fixed in
aca90a49c:apply_dynamicsneeds a baseline;MonkeyPatch.context().The fifth nit was this section. The same reviewer then re-reviewed the delta and gave APPROVE at
aca90a49c. It re-ran the restored-YAML case and an error raised inside the recorder block, and 131 tests passed. Reports are in~/reviews/uk-obr-elasticities-2412/(review-opus-76f6aba56.md,review-opus-aca90a49c.md).Notes
Simulation.apply_dynamicsor setsgov.dynamic.obr_labour_supply_assumptions.Reproducer:
policyengine_uk/tests/code_health/test_labour_supply_parameters_are_read.py::test_every_labour_supply_parameter_is_readaxiom: n/a: a simulation switch with no policy content; it encodes no statutory provision.
🤖 Generated with Claude Code