Repository navigation
Conversation
Reform.__init__ already clones the baseline's tree, so the deep copy in Reform.modify_parameters paid for a second full copy, threw away what apply() had changed, and carried the baseline's cached nodes into the reform. Reform now inherits TaxBenefitSystem.modify_parameters. That method now drops every node cached at an instant once the modifier finishes, in the tree it handed over and any tree it got back, so edits made without Parameter.update read fresh on both paths a reform runs. Fixes #618 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add tests for the system cache being rebound after the modifier, a read through the system inside a modifier, the return value, a walk that stops at the nodes below, and edits made before a modifier raises. Warm the tree inside apply() so the instance path exercises the walk as well. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
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 #618
Reform.modify_parametersdeep-copiedbaseline.parametersalthoughReform.__init__had already cloned them. That copy threw away whatapply()had changed before it, and it carried the baseline's cached nodes with it, so an edit that bypassedParameter.updatecould read the baseline's values. In-place reforms had the same stale read.What changes
Reformno longer overridesmodify_parameters. It inheritsTaxBenefitSystem.modify_parameters, so a modifier gets the reform's own tree on both paths a reform runs.TaxBenefitSystem.modify_parametersdrops every node cached at an instant once the modifier finishes, even if it raises: in the tree it handed over, in any tree the modifier returned, and in the system's own cache. It rebinds that system cache rather than clearing it, as it did before, because policyengine-us shares the dict between SPM policies.ParameterNode.clear_descendant_cachesdoes the walk, brackets included. A loaded bracket has noparent, soParameter.updateclears the bracket and nothing above it.The cache design stays as it is, for #621. This PR changes when
modify_parametersclears caches, and nothing else about them. That includes one change on the in-place path: a modifier that returns something other than a tree now resets the system cache too, where master returned before resetting it.Behaviour changes for reforms built as instances
apply()stay, and successivemodify_parameterscalls build on each other, asapply_reform_setalready did.apply_reform_setdid.modify_parametersstill returns aValueErrorhere rather than raising it.modify_parameterson a reform instance returns the reform, notNone, asTaxBenefitSystem.modify_parametersdoes. The annotation now says so.apply()sees if it catches the exception. With oneupdateand a raise, master's instance read 600 and the branch reads 1, asapply_reform_setdoes on both.parentset on brackets, because clones set it and a deep copy of a loaded tree left itNone. Anupdateinside a bracket now clears caches up to the root on this path.ct_hb5114replaces its list throughupdate.Direct edits with no later
modify_parameters, such asReform.from_dictapplied in place to a loaded tree's bracket, can still read stale values. That stays with #621.Invariants
For any baseline read at any instants, and any reform whose
apply()edits the tree directly, reads it, calls modifiers ending in any way and uses the dict form, on either path:modify_parametershas run last, the system, its tree and each bracket read as a tree never read at all, given the same edits. That holds for a modifier that raises as well.apply_reform_setleave the same tree.Two exclusions are deliberate: the direct edit above, and in-place edits of nested metadata or list values.
Tests
tests/core/test_reform_modify_parameters.py: 49 examples, including the template reform on both paths, warm and cold edits, brackets through the dict form, returned and dropped trees, a raising modifier, the return value, the rebound system cache and reforms of reforms.tests/core/test_reform_modify_parameters_property.py: 6 Hypothesis properties, 200 derandomized examples each.master.Nonereturned and edits rolled back on a bad return. Reviewers predicted the 4 that first survived, and the tests that catch them are in this PR. A 22nd mutant cloned the tree after the edits, which keeps them, so it matched the fix and I dropped it.test_reforms.py,test_vectorial_parameter_node_copy.pyandtest_tracing_parameter_isolation.pyno longer describe the deep copy or clear caches by hand.Differential against
masterSame model commits on both sides: policyengine-us 2.90.2 at
469ad12cb9, policyengine-uk 2.170.0 at2da212abc, master at3c89556f2.policyengine-us, 85 cases: the
Reformobjects in the 25 modules that use modifiers, as an instance and in place, the contrib switches, and hand-written bracket and rate reforms. Each case compares the tree digest and household outputs.VariableNameConflictErrorfromut_hb210_s2.set_parameteron a bracket as an instance. The trees match, but master's household still read baseline tax (income tax 1,712). The branch reads -688, which is what the in-place path reads on both Cores.policyengine-uk, 25 cases:
TypeErroron master and now work, matching the in-place result.Test suites on this branch:
Timing
Building a
Reformon policyengine-us (156,949 nodes, Python 3.14.7). Median of 5 rounds, wall / CPU seconds, master and branch running side by side on a loaded host.set_parameter, read at 32 instantsThe YAML runner pauses GC while it builds a reform (#643), which is what the two paused rows show. The no-modifier row is the clone both Cores share. Peak memory for the run falls from 5.0 GB to 2.6 GB.
The in-place path did no walk on master, so it gets slower:
modify_parameterscall on a tree read at 16 instants costs 0.13 s of CPU, up from nothing measurable.Simulationmakes 6 calls and costs 2.01 s of CPU, against 2.00 s on master. A walk of the tree takes a median 1.71 ms.Not run
The full policyengine-us and policyengine-uk suites: I ran the affected files only, at
ea99d5d5f. The commit after it changes only tests, a docstring and a return annotation. The one US failure,TestSocialSecurityInputStateMachine, fails the same way on both Cores here, because it looks for Core's files in site-packages and my install is editable. CI'sTest Core and country packagesjob passes.axiom: n/a: engine change in Core, no country policy.
🤖 Generated with Claude Code