Repository navigation
Conversation
Four structural reforms that pay a benefit (the Texas rebate, the End Child Poverty Act, its Economic Dignity for All Agenda version and the BOOST Act) replaced spm_unit_benefits with copies of its program list. The copies fell behind. Texas dropped 19 state SSI supplements. The three Tlaib reforms dropped 42 supplements, Washington's cash aid and the Washington and West Virginia child care subsidies, and counted ACP and EBB twice, since spm_unit_net_income adds both itself. spm_unit_benefits now reads gov.household.spm_unit_benefits, and each reform adds its own benefit there, and to household_benefits, with add_to_list_parameter. The ECPA and BOOST household totals regain CSFP and household health benefits. BOOST gets a variable for its credit paid through SSA, so the lists can name it. Fixes #10226. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
parameters(year) builds and caches the whole parameter tree for each instant, and the list test draws 21 years, so one run peaked at 7.5 GB. Reading the one list parameter avoids that. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The file builds a full reformed system for each of four reforms, the kind of module that group isolates from the 14.4 GB remaining process. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a Rest group - The add_to_list_parameter property now draws None values (gaps) and values that start on any day, and checks each value's first day and the day before it as well as random days. A local mutation check ran it against six broken helpers: it fails on all six, while the old strategy, which drew no None values, let the helper that stops skipping them through. - test_reform_benefit_lists.py leaves remaining-heavy for its own Rest group, reform-benefit-lists, so its 5.7 GiB peak RSS (local) never adds to the 10.5 GiB remaining-heavy holds (release run 42082143005). remaining-heavy keeps main's files and comment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
marked this pull request as ready for review
November 5, 2026 21:24
… no list in place The property built its expected lists after calling add_to_list_parameter. The parameter holds the drawn member lists themselves, so a helper that sorted the names or extended a member list in place moved the expectation with it and passed. The test now copies the names and every member list before the call, builds the expectation from those copies, and asserts the helper left both unchanged. A clone shares its member lists, so an in-place edit would reach every clone that shares them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… repeated members The helper property read only the parameter itself. A helper that set each value's list directly, skipping Parameter.update, passed it, though the parent node would keep returning its cached old lists to formulas and the parameter would stay unmarked, so a simulation could serve a variable from its macro cache. The property now reads through the parent node before and after the call, and checks the modified flag after any change. It also drew member lists without repeats, which hid a helper that dedupes the member lists in place, and it now asserts that core keeps the drawn lists, so its checks for edits in place cannot pass by default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's #10663 added a parity test that reads spm_unit_benefits' members with the year-less spm_members() of its Python list; the next commit passes it the year this branch's dated parameter needs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's #10663 read spm_unit_benefits' members from its Python list, which had no years. This branch moves the list into a dated parameter, so spm_members takes a year; check each year the other parity tests check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CountryTaxBenefitSystem applies a reform it is given, clears the modified flag of every parameter below the root, then applies the reform again. add_to_list_parameter changed values, and so set the flag, only when a name was missing, and the Texas rebate's household_state_benefits edit skipped its update when the rebate was already listed. So the second application left the lists unmarked: each of the four reforms passed as an object ended with every list it changes, and gov.household, unmarked, while the Texas rebate switched on through its in_effect parameter, which applies the structural reform only after the flags are cleared, kept them. Core skips its macro cache for a variable when a parameter the variable declares carries the flag. add_to_list_parameter now marks the parameter, and each node above it, on every call, and the Texas rebate marks household_state_benefits after its check. No figure changes: core reads the flag only when a simulation turns its macro cache on, which it leaves off by default, and none of the variables that read these lists declares its parameters. The helper property now checks the parameter's and its parent's flags before and after the first call, then clears both and checks that the second call, which adds nothing, sets them again. A new test checks that each reformed system marks exactly the lists its reform changes, and that current law marks none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CountryTaxBenefitSystem resets the modified flag of each parameter and node that parameters.get_descendants() yields, and that generator skips the root itself. Three comments and the helper's docstring said it clears every flag. This changes comments only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
|
#10725 adds |
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.
WIP: four of the six impact pairs are still running (the End Child Poverty Act in 2027 and 2024, EDAA and BOOST in 2027), so the impact section holds the two that finished. The CI figures come from run 42388504545 at 9b1b43d; CI at 2080a2e is running.
Fixes #10226.
Problem
Four structural reforms that pay a benefit replaced
spm_unit_benefitswith their own copies of its program list, because the list lived in Python where a reform could not extend it. A simulation that turns on one of these reforms uses its copy ofspm_unit_benefitsfor every household, so a program the copy lacks drops out of SPM resources in every state. The copies had fallen behind:tx_rebate)ca_state_supplementandwy_sspwa_sfa,wa_rca,wa_child_care_subsidies,wv_child_care_subsidiesacp,ebbcommodity_supplemental_food_program,household_health_benefitsacp,ebbacp,ebbcommodity_supplemental_food_program,household_health_benefitsThe issue asks about the Tlaib copies' other differences. Both trace to baseline changes the copies missed:
spm_unit_broadband_subsidyfrom the baseline'sspm_unit_benefitsand addedacpandebbtospm_unit_net_income. In the Tlaib copies' SPM lists it swappedspm_unit_broadband_subsidyforacpandebb, so under those reforms both count twice: once in the copy and again inspm_unit_net_income.aca_ptc. Add person_receives_aca and assigned ACA PTC variables #7808 (2026-04-06) changed the baseline formula'sdeduct_ptcappend fromaca_ptctoassigned_aca_ptc. The copies keptaca_ptc. This matters only under the UBI Center flat tax'sdeduct_ptcswitch.Change
spm_unit_benefitsnow readsgov.household.spm_unit_benefits, a list parameter holding main's 77 programs in main's order.assigned_aca_ptcunderdeduct_ptc, thenspm_unit_capped_housing_subsidyunlessgov.hud.abolition, in main's order.tests/unit/test_spm_measurement_boundary.pyfails any parameter that names an SPM valuation cap.add_to_list_parameter(inpolicyengine_us/tools/parameters.py) adds names to every dated value of a list parameter.CountryTaxBenefitSystem(reform=R)appliesRtwice, andSimulationandMicrosimulationapply structural reforms once more.tx_rebate, which keeps its 2027-onlyhousehold_state_benefitsentry and so reacheshousehold_benefitsthat way;ecpa_child_benefit, for both End Child Poverty Act versions;boost_middle_class_tax_credit_paid_through_ssa, a new variable: the BOOST credit when SSA pays it, else zero. The old copies counted the credit as a benefit under the same condition.test_aggregate_list_integrity.pydrops its checks of the copies. It now fails any reform that redefines a benefit aggregate and points toadd_to_list_parameter, and it checks that each member of both benefit lists names a variable the model defines and appears only once.Invariants
policyengine_us/tests/test_reform_benefit_lists.pytests these with Hypothesis, over each of the four reforms and current law:add_to_list_parameterkeeps every dated value and the members' order, appends only names a value lacks, leaves gaps (Nonevalues) alone, and changes nothing when run again. It writes new lists and leaves the names it gets and the member lists it reads unchanged. A clone of a parameter shares its member lists, so an edit in place would reach every clone that shares them. It changes values only throughParameter.update, so the parent node, which caches what it returns for each day, returns the new lists, and core marks the parameter modified, a flag a simulation checks before it serves a variable from its macro cache.Parameter.update, and member lists deduplicated in place. The test fails on all eleven, with Hypothesis derandomized as in CI and with fresh draws. Earlier versions let five through: the first drew no gaps and passed the gap mutant; the second built its expectation after the call and passed the sorting and extending mutants; the third read only the parameter, never its node, and drew no repeated members, so it passed the last two.spm_unit_benefits,household_benefits,household_state_benefits) in each year from 2015 to 2035 equals the baseline list followed by that reform's own additions, and nothing else.spm_unit_benefitsandhousehold_benefitsequal the sum of the baseline list's programs and the reform's additions, as the simulation computes each. The SPM sum also adds the capped housing subsidy. A program dropped or counted twice breaks the sum.spm_unit_net_incomeunder current law and under each reform.Baseline neutrality needs its own evidence, because the properties take the baseline parameter as their oracle:
Tests
tests/policy/reform/{tx_rebate,end_child_poverty_act,edaa_end_child_poverty_act,boost}_counts_every_baseline_benefit.yaml: 24 cases, one reform per file, so each file runs in its own subprocess. All 24 pass on this branch, in CI (Baseline (reform), which runs each reform file in its own subprocess). Against main's code (e53b426, run locally from the main tree), 21 fail on the values they check and none errors. The 3 that pass there are the Texas case that checks the rebate adds to the baseline benefits, and EDAA's two household cases, because EDAA's household copy already read the parameter.contrib/states/tx/rebate/tx_rebate.yamlin its Contrib states shard, and the Trump dividend, Alaska energy relief and Head Start composition files in Baseline (reform). Every Baseline and Contrib job passes.test_household_spm_resource_parity.pyreads the SPM list from the parameter plus what the formula appends. In CI (Rest groupremaining-1) its 57 cases pass, as dotest_aggregate_list_integrity.py(25) andtest_spm_measurement_boundary.py, and, inremaining-2,test_rest_python_groups.py(64).test_reform_benefit_lists.pyruns in its own Rest group,reform-benefit-lists, because it builds a full reformed system for each reform. Locally it peaked at 5.7 GiB RSS;remaining-heavyalready peaks at 10.5 GiB on main (release run 42082143005), and a shared process would stack the two on a 16 GB runner. In CI (Rest shard 5) the group ran 21 passed and 1 skipped (current law applies no reform, so the reapply case has nothing to check) in 12 minutes 19 seconds, peaking at 5.96 GiB RSS.remaining-heavypeaked at 10.52 GiB, against 10.48 GiB in main's release run 42082143005. The planner has no timing for the new group yet, so shard 5 ran 37 minutes against the other shards' 20 to 29; main's next release run records the group's time.Impact
Every figure here comes from a full microsimulation of one policy and year on
hf://datasets/policyengine/populace-us/populace_us_2024.h5@populace-us-2024-spm-20260909: 4 runs, one per policy, year and side. The before runs use main at e53b426; the after runs use this branch at ee75695, whose model code matches the head (later commits change only tests and the Makefile). The script,reform_lists.py, records each run's commit and dataset, andrender_reform_lists.pybuilds these tables and checks from its output. Poverty rates give the weighted share of people in SPM poverty, counting everyone under 18 as a child and everyone 65 or older as a senior.2027
Where each change comes from
Each change in an aggregate equals the total of the programs the old copy lacked, minus the total of those it counted that the baseline does not. The copies' gaps account for every dollar, to within a few hundred dollars of float32 rounding:
2027 ($m):
spm_unit_benefitshousehold_benefitsspm_unit_benefitshousehold_benefitsLargest state changes in SPM benefits, 2027 ($m):
state changes sum to +3,071.3; national +3,071.3
Own benefit and poverty detail, 2027
tx_rebate16.16 → 16.16Under the Texas rebate, senior SPM poverty after the fix equals current law's rate. The rebate adds up to $1,500 to each poor senior's resources, but in a run of the Texas households alone (
tx_only_probe.py, which reproduces the national run's Texas SPM benefits, $142.796bn), the closest of the 48 poor Texas senior records falls $1,502 short of its threshold, so none crosses it.The renderer ran 38 checks on these runs, and all pass. Among them: under current law every recorded number, state totals included, matches before and after; each reform's own benefit, market incomes, population,
household_state_benefitsand every program's total match on both sides; each change in net income equals the change in the benefit aggregate it adds; and the branch's list parameter holds the 77 baseline programs plus only the policy's own addition.Conflicts
#9462 (draft), #10086 (draft) and #10569 edit
spm_unit_benefits.pyand all four reform copies. After this lands, each moves its additions intoparameters/gov/household/spm_unit_benefits.yamland drops its reform-copy edits.axiom: n/a: SPM and household benefit aggregate lists; no rule changes
🤖 Generated with Claude Code