Conversation
A branch copies its parent's cached arrays when it is created and never sees the parent's later calculations. The itemizing, not_itemizing, no_salt, DE/VA EITC refundability, ID aged-or-disabled, AL 2020 IRC, NY pre-ARPA EITC and pre-TCJA CTC branches were kept for the life of a simulation, so in a second year they answered from a copy taken in the first, before any of the second year had been calculated. A simulation that calculates only that year creates them from its current state instead, so the later year came out differently: on a 3,000-household Enhanced CPS subsample, 124 tax units' ctc_limiting_tax_liability and one tax unit's income tax in 2025. get_branch_for_period shares a branch within a period, as before, and creates it again from the parent when the period changes. Single-year results are bitwise unchanged on that subsample, and year two of a two-year simulation now matches a 2025-only simulation bitwise. The regression test calculates 2024 and then 2025 or 2027 in one simulation and compares against single-year simulations; it also covers the policyengine-core carry-over defect (age at a twelfth of its value). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #9738 +/- ##
===========================================
- Coverage 100.00% 96.13% -3.87%
===========================================
Files 4 13 +9
Lines 76 259 +183
Branches 2 13 +11
===========================================
+ Hits 76 249 +173
- Misses 0 6 +6
- Partials 0 4 +4
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Targeted YAML run on bb0aa30 with policyengine-core #557: 371 files, 1803 passed, 0 failed (22.6 min). The files were every YAML under |
Review of bb0aa30: the age defect needs a month of 2024 after January (January ties the year input on start), and the branch_period assertion alone does not prove the branch was replaced. Assert identity too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Independent review (Codex lane) on bb0aa30: REQUEST_CHANGES for one P3 docstring correction. It confirmed the rest:
Addressed in 7d7f145: the docstring now says "a month of 2024 after January", and the test now also asserts that the later year's branches are new objects, not only re-tagged. The core pin bump follows once policyengine-core#557 is released. |
|
This PR's regression tests ( |
|
Like-for-like PE-US benchmark: core #557 vs #562. Setup: Enhanced CPS 2024, 3,000-household subsample (fixed seed), policyengine-us main 20ccd5a with and without policyengine-us#9738. Each cell compares all 24 benchmark arrays bitwise (income tax, itemization, both branch liabilities, CTC, EITC, DE/ID/VA and state income tax, household net income, SNAP and more).
So on this model the two core PRs are interchangeable. Both remove the age/12 error, both leave single-year results bitwise unchanged, and both need PE-US #9738 for the second year to match exactly. They differ in scope:
Scripts and arrays: |
The age cases need a policyengine-core with the carry-over fix (policyengine-core#557 or #562), so this PR could not merge before that release. But #562 should land only with or after this PR: under it, three-year policyengine-us runs that keep formula branches across years raise in 2027. So test the branch fix on its own, with ages given for every year so no age is carried over: on current core it fails on unpatched formulas (tax_liability_if_itemizing, ctc_limiting_tax_liability) and passes here. The age cases skip, by behaviour, while the installed core still carries a month's twelfth into a later year; drop the guard when the core minimum includes the fix. With core #557 or #562 all 7 pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of a31b2bc: 3.32.12 (published today) still carries the twelfth; the fixture name read as the opposite of what it gates; the changelog named only some of the branches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of a31b2bc: the ten state call sites had no test (patch coverage 23%; CI turns itemization branching off for coverage, so the federal sites don't run there either). Delaware and Virginia EITC refundability, Idaho aged/disabled and New York pre-TCJA CTC households now calculate an earlier and a later year in one simulation and must match a later-year-only simulation, with each branch created again and tagged with the later period. The 2021-only Alabama 2020-IRC and New York pre-ARPA EITC branches are checked to record their period. Ages are given for every year so these run on any core. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
CI follow-up. The two exit-143 shards are not caused by this PR.
Codecov patch was red because nothing covered the ten state call sites, and CI turns itemization branching off for the coverage run. d6ab9b7 adds tests:
Ages are given for every year, so these run on any core. |
codecov/project was red: the touched AL and NY formulas had lines no test reached (AL's non-2021 return, NY's post-2024 early return and non-pre-TCJA path). A new test asserts the al_2020_irc branch is not created for 2022, and the pre_tcja_ctc branch is not created for 2025 (post-2024 credit) or when pre-TCJA rules are off; together the tests now cover all lines and branches of those three files. Also correct the state fixture's docstring: the household reaches each state's branches, which is what the tests assert, but is not claimed eligible for Idaho's aged credit (review of d6ab9b7). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Can merge before the core fix. It should, because PolicyEngine/policyengine-core#562 is to land only with or after this PR: under #562, three-year runs that keep formula branches across years raise in 2027.
WIC benefit valuetoWIC#562). They skip themselves, detected by behaviour, while the installed core still carries a month's twelfth forward.What and why
A
Microsimulationthat calculated 2024 and then 2025 gave badly wrong 2025 results. On the full Enhanced CPS, 2025 federal income tax was $3.9bn after 2024 had been calculated. On a 3,000-household subsample it was $31.6bn, against $1,846.9bn in a 2025-only simulation, and state income tax was −$19.9bn. Two defects caused it.1. Ages at a twelfth of their value (policyengine-core, fixed in #557 or #562)
monthly_agereadsageper month, and core caches each month value as a twelfth of the year's age. Core 3.24.0 through 3.32.12 carried the latest-starting cached period of any unit into a later year. So once any month after January of 2024 had been calculated, every person's 2025 age came out as age/12, which broke dependency, filing status, AGI and everything downstream. Single-year datasets such as the Enhanced CPS hold inputs for one year only, so they are exposed. Entity-level datasets (the Populace default) are extended to every year at load and are not.2. Formula branches kept across periods (this PR)
A branch copies its parent's cached arrays when it is created and never sees the parent's later calculations. These branches were kept for the life of a simulation:
itemizing,not_itemizing,no_salt;al_2020_irc,ny_pre_arpa_eitc,pre_tcja_ctc.In a second year they answered from a copy taken in the first year, before any of the second year existed. A 2025-only simulation creates them from its 2025 state instead. With core fixed, this still left 124 tax units'
ctc_limiting_tax_liabilityand one tax unit's income tax different in 2025 on the subsample.get_branch_for_period(policyengine_us/tools/period_branch.py) shares a branch within a period exactly as before, and creates it again from the parent when the period changes. It also releases the previous period's branch, so a two-year run forked 24 times instead of 39.Invariants
Evidence
Enhanced CPS 2024, 3,000-household subsample with a fixed seed, policyengine-core #557. Each row compares all 24 benchmark arrays (income tax, itemization, branch liabilities, CTC, EITC, state taxes, household net income, SNAP and more):
Full Enhanced CPS, all households, with core #557 and this branch:
Test plan
policyengine_us/tests/core/test_multi_year_simulation.py(new, 7 tests). Three households: an IL family in paid childcare, a CA itemizing couple, a TX retiree. Inputs are for 2024; the branch test also gives ages for every year. The two-year tests calculate 2024, then 2025 or 2027, in one simulation and compare 14 variables against single-year simulations. The monthly-age tests check thatagesurvives.test_formula_branches_are_created_again_for_a_later_yeargives ages for every year, so it isolates the branch fix on any core. It also checks that the itemization branches are new objects for the later year.requires_core_carry_over_fix) until the installed core has the carry-over fix. Core 3.32.12, released today, still has the bug. The guard's removal is tracked as a follow-up task.tax_liability_if_itemizingandctc_limiting_tax_liabilityWIC benefit valuetoWIC#562policyengine_us/tests/test_ctc_itemizing_branch_cycle.pypasses.371 YAML files (1,803 tests) covering itemization, CTC, and DE, VA, ID, AL and NY income tax pass; see the comment below.
Single-year results are bitwise identical to main on 3,000 Enhanced CPS households (24 arrays, 2024 and 2025).
ruff format --checkandruff checkpass with the locked ruff 0.15.5.Changelog fragment in
changelog.d/.axiom: n/a: simulation branch lifecycle (microsim infrastructure); no policy rule changes
🤖 Generated with Claude Code