Purge the full visibility chain in Simulation.delete_arrays and give clones private disk views#521
Merged
Merged
Conversation
Clone on-disk cache metadata per holder and purge all branch names visible to the calling simulation so perturbation branches recompute inherited formula values. This fixes the cache-inheritance mechanism behind PolicyEngine/policyengine-us#9086.
This was referenced Jul 19, 2026
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 the mechanism behind PolicyEngine/policyengine-us#9086: behavioral-response measurement branches recorded marginal tax rates of exactly 1.0 for every adult in both the baseline and reform branches, making
relative_wage_change ≡ 0and the substitution channel of labor supply responses (and the capital-gains MTR channel) inert in every behavioral run, while static/root-simulation MTRs stayed correct.What was wrong
Simulation.delete_arrayscalledholder.delete_arrays(period)with no branch name, so onlydefault:-keyed storage was purged. Inside a named branch (the behavioral measurement branches), the purge-then-perturb pattern used bymarginal_tax_rateand friends therefore failed to remove the parent branch's cachedhousehold_net_incomefrom the sub-branch's cloned storage;Holder.get_array's ancestor walk-up served it back, the perturbed input never fed anything, and1 − 0/Δ = 1.0came out for every masked adult.Simulation.derivativehad the same latent bug.Changes
Simulation.delete_arrayspurges the caller's full visibility chain — its own branch name, each ancestor viaparent_branch, anddefault(the existing_get_visible_branch_names, deduplicated) — in the simulation's own private storage. At root the chain is justdefault, so root behavior is unchanged.InMemoryStorage.delete/OnDiskStorage.deletesemantics are untouched: sibling and dispatched branch keys survive (the branch-scoped-delete guarantees from the earlier C2 fix and Dispatched set_input values are dropped after cache invalidation #484 are preserved and now covered by a test).Holder.clonedeep-copied_memory_storagebut shared the sameOnDiskStorageobject, so chain deletion through a clone would have mutated the parent's disk mappings.OnDiskStorage.clone()copies the_files/_enumsmappings over the same storage directory, never owns directory cleanup, and keeps the original cleanup owner alive through a backreference (clone-of-clone propagates the root owner). The.npyfiles themselves remain shared; identical{branch}_{period}writes from two views still target the same path (pre-existing, documented in the docstring). Notememory_configdefaults toNoneand nothing in core/-us construction enables it, so disk storage is inert in standard runs — this change is for correctness when it is enabled.Call-site audit
All 13 simulation-level
delete_arrayscall sites in policyengine-us 1.729.0 (the three MTR formulas, the component-MTR helper, capital-gains responses, the SSA revenue branch calcs, and the NY CTC/EITC retained branches) are purge-for-recompute and want the new semantics; the 11 directHolder.delete_arrayscalls (pre-response input migration insystem.py,BranchedSimulationcleanup) keep their existing single-branch behavior. No caller preserves a cached own/ancestor value across a purge.Tests and checks run
tests/core/test_branch_scoped_delete_arrays.py: nested-branch recompute-after-purge, visibility-chain scope with sibling/dispatch survival,OnDiskStorage.clonemetadata/ownership, cleanup-owner GC lifetime, child disk-delete parent-safety, andderivative()inside a named branch. All 6 fail on unfixed master and pass with this change (verified by revertingpolicyengine_core/to master and rerunning).make formatclean.relative_wage_change−0.0996, where master records 1.0000/1.0000/0.0000.changelog.d/branch-scoped-delete-arrays.fixed.md.Observed while auditing, intentionally not addressed here
purge_cache_of_invalid_valuesand policyengine-usBranchedSimulationcleanup still deletedefaultonly for named simulations (pre-existing).OnDiskStorage.deleteremoves only the exact{branch}_{period}mapping for a containing period (memory backend removes contained subperiods) and does not prune_enums.subsample()rebuilds the baseline branch fromself.clone()(reform system) and assigns the saved baseline system toself.branches["tax_benefit_system"]— a dict key, not the branch; reproduced on the country template and will be filed separately.🤖 Generated with Claude Code