Conversation
policyengine-core 3.32.12 (PolicyEngine/policyengine-core#556) makes Simulation.get_branch share the simulation's cached arrays with the new branch and copy each one only on first read, instead of deep-copying every array up front. Marginal tax rates, labour supply responses and the capital gains marginal tax rate all branch, so they stop paying for copies their branches never read. Raise the floor from 3.32.9 and relock (uv.lock moves core 3.32.9 -> 3.32.12; its entry for this package catches up from 2.102.3 to 2.104.7). The one behavioural difference core documents is a write in place into a cached array after branching, which a branch that has not read that array yet would now see. Extend the run-time half of the cached-array guard test to run the branching code paths (marginal tax rates, labour supply responses, capital gains realisation response) with every stored array read-only, so such a write fails the suite. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
What
policyengine-core>=3.32.9(set in Require policyengine-core 3.32.9 and stop exporting HF_TOKEN in CI #1898) to>=3.32.12, and relocks. Theuv.lockdiff against main is the core version and hashes, the specifier, and the lock's ownpolicyengine-ukentry caught up with pyproject's version. uv 0.12.13 also wanted to rewrite two equivalentcffimarkers underargon2-cffi-bindings; they are left as they were, anduv lock --lockedpasses.policyengine_uk/tests/code_health/test_cached_arrays_not_written_in_place.py, from Copy cached arrays before writing in scenario modifiers #1988). They run the marginal tax rates (earnings and capital gains), the labour supply responses and the capital gains realisation response with each stored array made read-only.Why
policyengine-core 3.32.12 (released 2026-10-02) includes PolicyEngine/policyengine-core#556. Before it,
Simulation.get_branchdeep-copied every cached array into the new branch. Now the branch starts with read-only views of the simulation's arrays and copies each one only the first time it reads it.Simulation.clone()still copies everything. This package branches simulations in:marginal_tax_rateandmarginal_tax_rate_wrt_employer_cost;marginal_tax_rate_on_capital_gainsand the capital gains realisation response;tax_credits_applicable_income.The floor rises, rather than only the lock, so that pip installs, which ignore
uv.lock, also get this. #2034's verification measured a labour-supply reform on the Enhanced FRS (2024, maximum resident set size under/usr/bin/time -l):The 3.32.10 and 3.32.11 releases in between allow pytest 9 and warn when a restricted Hugging Face download has no
HUGGING_FACE_TOKEN. Thepolicyenginepackage (6.2.1) pins exact versions of both core and this package in its extras, so the new floor does not change what it installs.Results unchanged
Core documents one behavioural difference. Code that writes in place into a simulation's cached array after branching (
x[mask] = 0,x += 1) now also changes what a branch reads, if the branch has not read that array yet. #1988 removed the writes of this kind that its static scan finds, and runs that scan in CI. The new run-time cases add a check on the branching paths themselves. Each of four in-place writes injected after aget_branchcall (marginal rate, capital gains marginal rate, labour supply, capital gains response) fails its case withValueError: output array is read-only.Each comparison below is two real
Microsimulationruns on the same commit, in two environments that differ only in policyengine-core (3.32.9 and 3.32.12; same numpy 2.1.3, pandas 2.3.1, Python 3.13.9). They used a private copy of the Enhanced FRS 2024-25, whose hash is unchanged by every run. Every array kept (household, benefit unit and person level) must have the same dtype, shape and bytes, and every weighted total must be exactly equal. Arrays include:household_net_income,gov_spending,household_benefits,household_tax, HBAI income;HOUSEHOLD_BENEFIT_VARIABLES;marginal_tax_rateadult_1_pay_rise,adult_2_pay_risemarginal_tax_rate_wrt_employer_cost,marginal_tax_rate_on_capital_gainsadult_1_employer_cost_mtr,adult_2_employer_cost_mtrbaseline,lsr_measurementoriginally_split_income,split_incomeBaseline totals, identical on both versions (£bn):
household_net_incomegov_spendinghousehold_benefitshousehold_taxuniversal_creditstate_pensionpension_credithousing_benefitchild_benefitpipA CRC audit of every stored array on 3.32.12 found no array written in place after it was stored: 12,583 arrays stored across 2 branches in the
marginal_tax_raterun and 28,859 across 7 branches in the labour-supply run, 0 written in place.The labour-supply run creates and reads its branches, but on main its responses are exactly zero in every year, on both versions: the baseline side of the measurement is a branch of the reform simulation (
simulation.get_branch("baseline")), which carries the reform's parameters, the pattern #1803 fixed for capital gains. That is a separate fix. The capital gains realisation response run is a branching path whose output does depend on its branches: −£0.87bn in 2025 to −£1.10bn in 2030, identical on both versions.Memory
Peak memory under
/usr/bin/time -lon the same Enhanced FRS copy, one process at a time. Max RSS is reported alongside peak memory footprint, because under memory pressure macOS compresses a process's older pages and max RSS under-reports. GB = 10^9 bytes.marginal_tax_ratemarginal_tax_rateTwo repeats per 2024 row (shown as
a / b), one run per 2025-2030 row; macOS memory pressure was normal (level 1) before every run, and after every run but one, which ended at level 2 (warning). Wall time is not compared: the host was heavily loaded, and loading the same dataset alone took between 12 and 43 seconds across runs.Tests
uv run --frozen --extra dev policyengine-core test policyengine_uk/tests/policy -c policyengine_uk: 1,381 passed with core 3.32.12 (pre-merge head f383f6e), and 1,381 passed on main at the time (908da84, core 3.32.9).uv run --frozen --extra dev pytest policyengine_uk/tests/withHUGGING_FACE_TOKENset, so the microsimulation tests ran: 470 passed, 1 skipped (policyengine_bundlesis not installed), 2 xfailed (pre-merge head f383f6e).uv run --frozen: 35 passed in 66 s. On the pre-merge head the new cases also pass with core 3.32.9.get_branchinmarginal_tax_rate, inmarginal_tax_rate_on_capital_gains, in the labour supply response, and between the two capital gains measurements each fails the matching case (ValueError: output array is read-only).Invariants
axiom: n/a: dependency floor and tests only, no policy change.
🤖 Generated with Claude Code