Repository navigation
Conversation
The SPM subtracts what parents paid for child care and lists no child care benefit among its noncash resources. PolicyEngine subtracted pre-subsidy expenses and added only the Colorado, Washington and West Virginia subsidies to resources, so a subsidy in the other 48 states lowered SPM resources. Net child_care_subsidies from the expense instead, and take the three states out of spm_unit_benefits and the four reform copies of that list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add YAML cases for the formula's edges and for New York, Texas, Alaska, Washington, West Virginia and Colorado through their real programs, and property tests for the invariants. The whole-model form of "a subsidy never lowers SPM resources" fails where SNAP, TANF and credits fall by more than the expense, so the property holds those fixed and one test pins the intended case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The differential passed on the old code because its identity holds when the expense does not move. It now also requires the fall the Census definition gives, with explicit examples in states that left the subsidy out and states that counted it as a resource. Income comparisons get a tolerance above float32 rounding, which tripped the exact comparison at the fixture's $400,000 of earnings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
marked this pull request as ready for review
November 1, 2026 18:15
Reviews by GPT-6.1 Sol and Opus 5.5 found three problems at 343f8fe. - Before 2024 the SPM expense has no formula, so removing the Colorado, Washington and West Virginia subsidies from resources made them reach no SPM path. A 2023 Washington subsidy lowered SPM income. Those three now stay resources until 2024, and years before 2024 match main. - The 2024 program block left out the DC, Tennessee and Texas subsidies, which pay in 2024. All 51 now start at 2024, the first year the SPM expense reads the list, and the test checks every block from then on. - The formula read the pre-subsidy input and ignored a direct out-of-pocket amount. It now reads childcare_expenses, the model's out-of-pocket variable, for a unit with a child. Also add paying-subsidy cases for 2024 and 2025, correct the intended violation's wording, record that datasets fill the pre-subsidy input from reported spending, and route the property tests to the group for retained full systems. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The baseline keeps Colorado, Washington and West Virginia subsidies as resources before 2024, because the SPM expense has no formula then. The four reform copies of spm_unit_benefits dropped them in every year, so each override now restores what main carried: all three in the Texas rebate, Colorado in the Tlaib copies. Qualify the docstring's identity for a direct childcare_expenses input, and correct the integration YAML header. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Nov 1, 2026
The list test reads list literals only, so it could not see the pre-2024 clause in the four reform copies. A 2024 case in each reform's YAML file now fails if the clause loosens and a subsidy counts as a resource on top of the lower expense. Also qualify the docstring for person-level pre-subsidy inputs and for the Tlaib copies, which carry only Colorado's subsidy before 2024, and note in the integration header that one case sets no care hours. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…idies # Conflicts: # policyengine_us/tests/policy/baseline/household/income/spm_unit/spm_unit_benefits.yaml
…idies Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Datasets fill spm_unit_pre_subsidy_childcare_expenses from the ASEC's SPM_CHILDCAREXPNS, which already holds what parents paid after any subsidy, so netting a modeled subsidy from it counted the subsidy twice. Over a dataset the expense now keeps the reported amount under baseline policy, and a reform moves it by the change in subsidy: min(W + max(E - (S1 - S0), 0), C). Household calculations keep the input as a price and net the subsidy, as before. Add properties for both rules and a differential between them, and a microsimulation test over the shipped population. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…olicy side Main's #9698 now branches a reform's baseline arm again after the constructors move the pre-response inputs. Over a dataset, the SPM expense reads its baseline subsidy from that arm, so pin it there: - Add tests/core/test_reform_baseline_arm_inputs.py. Under a reform that changes nothing, the reform and its arm match a plain run on 102 households of raw columns, two families in every state, and on a situation that names New York only through state_code_str. With _rebuild_baseline_branch disabled, both cases fail. - Keep the parent's raw earnings and hours in the repeal test, which then fails in New York without the rebuild. - Set work expenses and the earnings cap on the policy side only in property 8, with an example that fails if the rule reads either from the arm. - Drop the dead branch_name guard, document the entry points that build no arm, and word the reported-spending reading as an inference from the ASEC question. - Update _baseline_measurement_branch's docstring, which still said the arm keeps the raw inputs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The docstring still named _rebuild_baseline_arm, which this branch dropped when it merged main. Main's #9698 calls it _rebuild_baseline_branch. Name the moved inputs through PRE_RESPONSE_INPUTS, which also lists sstb_self_employment_income. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 tasks done
_rebuild_baseline_branch cloned the reform simulation and rebound the clone's holders to baseline variables, but it kept each holder the reform had built in another entity or with other ETERNITY storage. Construction, subsample and _baseline_measurement_branch all drop those. Under a reform that stores child_care_subsidies for all time, a New York family with a $500 input got S0 = $500 from the arm, where baseline policy pays it $13,916.40, so its reform expense came out $14,000.00 where the dataset rule gives $27,416.40. - Call _drop_holders_built_for_another_policy before _rebind_holders. - Add test_the_arm_calculates_what_the_reform_stores_for_all_time, and check in every arm test that each holder suits its arm's variable. - Check holder binding before subsampling as well. Under the redefining reform, the arm copied an empty yearly holder for the baseline's ETERNITY is_household_head. - Document that apply_reform builds no arm while a simulation built with reform= keeps its arm, and word the input's after-subsidy reading as the model's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…idies 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.
Problem
For a Manhattan parent earning $30,000 with a child in full-time care at $14,000 a year,
ny_ccappays $13,916.40. On main at469ad12cb9(2.90.2), turning that subsidy on raiseshousehold_net_incomeby $8,321.86 and lowersspm_unit_net_incomeby $5,594.54.On main, SPM resources count only the Colorado, Washington and West Virginia subsidies, and the SPM deduction uses pre-subsidy expenses. So in the other 48 states a subsidy cuts SNAP and tax credits, and nothing in the SPM offsets the cut.
Fix
Census subtracts what parents paid for child care (SPM technical documentation, PDF page 17), and its list of noncash benefits (section 4.3.1) has no child care benefit. So from 2024 the model counts a subsidy in the SPM once, as a lower expense.
In a household calculation, the user enters the price of care before any subsidy as
spm_unit_pre_subsidy_childcare_expenses, while both dataset builds fill that input with what parents reported spending. Sospm_unit_capped_work_childcare_expenseshas two rules. In them, W stands for work expenses, C for the earnings cap, E for the input, S1 for the subsidy the simulated policy pays and S0 for the subsidy baseline policy pays.min(W + max(E - S1, 0), C)min(W + max(E - (S1 - S0), 0), C)Over a dataset, the model grosses the reported amount up by the baseline subsidy at run time and then nets the simulated subsidy from the result. Under baseline policy the formula reads no subsidy and keeps
min(W + E, C), the amount Census subtracts.simulation.is_over_datasetpicks the rule, andsimulation.baselinesupplies S0, as it does formedicaid_slcsp_state_denominator.childcare_expenses, the model's out-of-pocket variable, for a unit with a child. That variable allocates expenses to children, so for a unit with no child the formula netschild_care_subsidiesfrom the unit's own input.co_ccap_subsidy,wa_child_care_subsidiesandwv_child_care_subsidiesfromspm_unit_benefitsand from the four reform copies of that list. The expense has no formula before 2024, so SPM resources keep those subsidies in those years wherever main counts them, and every year before 2024 matches main. The Tlaib copies hold only Colorado's. A YAML case in each reform's file pins 2023 and 2024.child_care_subsidieslist holds all 51 state programs. The DC, Tennessee and Texas programs pay in 2024, but main's list leaves them out until 2025. Adding them lowerschildcare_expensesfor those three states in 2024. Its readers see the lower amount that year: the SNAP dependent care deduction, HUD adjusted income, dependent-care credits, and the DC, Tennessee and Texas TANF deductions. Over Populace 2024 the three programs pay nothing, so the default file's 2024 results don't move.The Manhattan family's expense falls from $14,000 to $83.60, its SPM net income rises by $8,321.86, as its household net income does, and it leaves SPM poverty.
Over a dataset, a unit reporting $14,000 keeps a $14,000 expense under baseline policy. Repealing New York's subsidy raises that expense to $27,916.40 and lowers SPM net income by the same $8,321.86.
test_repeal_moves_dataset_and_household_income_by_the_same_amountpins this in New York and Texas.Adding subsidies to resources instead would depart from the Census definition and fail in two places. It would add income at the earnings cap, where the expense doesn't fall. It would also overcount wherever a program pays more than the family spends. Vermont pays its state rate regardless of the provider's charge, and in the audit it paid $22,828 against a $1,200 bill.
Why the dataset needs its own rule
Both dataset builds fill the input from the ASEC's reported spending,
SPM_CHILDCAREXPNS: Populace inmicrocosm/build/us_runtime/childcare.pyandpolicyengine-us-dataindatasets/cps/cps.py. The ASEC asks parents how much they spent on child care, and Census subtracts that amount as it stands. This rule reads the amount as what parents paid after any subsidy, so netting a modeled subsidy from it would count the subsidy twice. Over the default file in 2026, netting cut the SPM expense by $1.135B at606699d640, and by $7.280B with schedules set at640753a617. So the builds keep the reported amount, and the model moves it only by the change in subsidy a reform makes.The baseline arm
The dataset rule reads S0 from the reform's baseline arm. Core builds that arm inside its
__init__.Simulation.__init__andMicrosimulation.__init__then moveemployment_income,self_employment_income,sstb_self_employment_income,weekly_hours_workedandlong_term_capital_gainsto their pre-response variables, and fillstate_codefromstate_code_str. Atd4d391788d, the arm kept the raw inputs and read 0 from the pre-response variables. In a household situation it paid the Manhattan family $3,498 in New York TANF and no child care subsidy, where a plain run paid no TANF and a $13,916.40 subsidy. Over a dataset that stores raw hours, aspolicyengine-us-data's CPS build does (cps.py:481), the dataset rule would have carried that gap into the SPM.#9698, which this branch merges from main, branches the arm again after the moves (
_rebuild_baseline_branch). That rebuild bound the copied holders to baseline variables but kept each holder the reform had built in another entity or with otherETERNITYstorage. The SPM mixin's constructor,subsampleand_baseline_measurement_branchall drop such holders. Under a reform that storeschild_care_subsidiesfor all time, the arm read a New York family's $500 input as S0, where baseline policy pays the family $13,916.40. The family's reform expense came out $14,000.00, where the rule gives $27,416.40. This PR makes the rebuild drop those holders too. When a reform stores a dataset input in another entity or with otherETERNITYstorage, the arm now goes without that input and computes the variable under baseline policy, as core's arm does.This PR also adds
tests/core/test_reform_baseline_arm_inputs.py, which pins the rebuilt arm on the dataset path this rule reads, and updates the docstring of_baseline_measurement_branch, which still said the arm keeps the raw inputs.Invariants
I test these with Hypothesis in
policyengine_us/tests/test_spm_child_care_subsidy_invariants.py.For a household calculation:
0 <= expense(0) - expense(S) <= S, and a larger subsidy never raises the expense.spm_unit_benefitsunchanged and raisesspm_unit_net_incomeby the fall in the expense, so the SPM counts it once.*_child_care_subsidiesvariable, and no program one of them adds, appears inspm_unit_benefitsor a reform's copy of it, and all 51 appear in every program block.Over a dataset:
min(W + E, C), whatever subsidy the model pays, in a plain simulation and in a reform's baseline arm.In
tests/microsimulation/test_spm_child_care_reported_expense.py,Microsimulationloads the shipped population, so the dataset flag and the baseline arm come from the path a user's run takes. One test checks property 7 on every record of the default dataset. The other runs a real reform on a 500-household slice that holds units with both a modeled subsidy and reported spending. It checks that the baseline arm matches a plain run and that the reform arm follows the rule.tests/core/test_reform_baseline_arm_inputs.pychecks the arm itself. Under a reform that changes nothing, the reform and its baseline arm compute what a simulation with no reform computes, on four pre-response variables and seven outputs from the subsidy to household net income. It runs a dataset of raw columns with two families in every state, and a situation that names New York only throughstate_code_str. It also checks that the arm owns its policy and holds no value the reform's policy produced, and that every holder in either arm suits that arm's definition of its variable. A third test runs the reform that storeschild_care_subsidiesfor all time: the arm pays what a plain run pays, and the reform's expense follows the dataset rule.test_subsampling_never_changes_which_policy_a_holder_belongs_tointests/core/test_spm_system.pynow checks every holder before it subsamples as well.The five new invariant cases and the slice test all fail against the formula that nets the subsidy from a reported payment (
513aa0bb66). With_rebuild_baseline_branchdisabled, all three arm tests and the New York repeal case fail, and a no-change reform gets the expense wrong for 32 of the arm test's 102 households, by up to $13,916.40. With only the rebuild's drop removed, the all-time test and the subsampling test fail.Intended violation
A subsidy can lower SPM resources at the whole-model level. SNAP, TANF and tax credits read out-of-pocket expenses, so a subsidy can cut them by more than the SPM expense falls. At the earnings cap the expense can't fall at all, and below the cap a benefit cliff can cut more than the expense falls. Household income loses the same benefits but also counts the subsidy, so it can rise while SPM income falls. SPM net income fell in 22 of the 1,395 subsidized cells in the grid below, and
test_subsidy_at_the_cap_can_lower_spm_incomepins one case.Audit of all 51 subsidies
I ran these audits on household calculations at
606699d640against main at469ad12cb9, and this head keeps that commit's household rule. Main has since changed the Vermont, New Hampshire, Nevada and Texas subsidies (#10584, #10575, #10598, #10636), so those states' counts can differ at the merged head.child_care_subsidiesleft out paying state programs in DC, Tennessee and Texas in 2024, and in DC, North Carolina, Oregon and Texas in 2021 to 2023. From 2024 they agree. This PR leaves 2021 to 2023 as they were, because the SPM expense formula starts in 2024.meets_ccdf_activity_testset. Every state pays somewhere on the grid, and the identity in invariant 6 holds in every cell.meets_ccdf_activity_test, TANF enrollment, homelessness and protective care, and the family had none of them set. Stop requiring an activity input from Nevada's working parents #10575 has since stopped requiring the activity input from working parents.household_state_benefitslists 23 of the 51 subsidies, and Count all child care subsidies in household benefits #9503 would count all of them.Population impact
I computed these tables from saved full-file arrays for 2026 (
populace_us_2024.h5@populace-us-2024-spm-20260909): main at469ad12cb9, and this PR's taxes and benefits at606699d640. Over a dataset this head's baseline expense equalsmin(W + E, C), and among variables onlyspm_unit_spm_expensesreads the expense, so the arrays give what this head's rule produces at that base. Main has since changed the Vermont, New Hampshire, Nevada and Texas subsidies, among others, and added cash programs to SPM resources. The branch has merged main twice since those runs, most recently at4bb2b92b39. Full-file runs at this head and its merge base wait on the host's heavy-run gate and will replace these tables.All of the change comes from the three subsidies leaving resources. Main adds modeled Colorado and Washington subsidies ($270.7M and $255.9M) on top of what those families reported paying, and this PR stops that. From 2025 the two program lists match, so
child_care_subsidies,childcare_expensesand SNAP equal main's on every record in 2026, with or without schedules. The deduction differs from main on 0 records, and SPM net income differs on 35. Five records (116,936 people, 64,207 children) enter poverty and none leave. Washington's rate moves from 12.00% to 13.44% on 4 records, and no other state's rate moves by 0.01 point.With schedules set
The default dataset has no schedule columns, so most programs pay nothing. As a sensitivity, I gave every child under 13 in a unit with child care expenses a full-time schedule on both sides: 8 hours a day, 5 days a week, 21 days a month. I ran main at
469ad12cb9and this PR's taxes and benefits at640753a617.child_care_subsidiesFive records (126,465 people) enter poverty and none leave. Washington moves as above, and West Virginia's rate moves from 14.03% to 14.56% on 1 record.
Costs and limits of this rule
childcare_expenses, which nets the modeled subsidy from the input over a dataset, as on main. This rule reads that input as what parents paid after any subsidy, so these programs net it twice wherever a program pays. Schedule data would widen the gap: with schedules set,child_care_subsidiesrises $7.475B, SNAP falls $1.231B and taxes rise $0.462B. Add NSECE childcare attendance candidate and validation tools microcosm#916, a draft, fills those schedule columns, so these programs need the same treatment before any schedule release. That fix gets its own PR.spm_unit_net_incometook 29.6 s in a plain run and 37.4 s in the reform arm of a no-change reform.set_inputon a reform simulation reaches only that arm. A caller who sets schedule columns on a reform simulation has to set them onsimulation.baselinetoo, or the model computes S0 without them.reform=, andapply_reformbuilds none. A simulation built withreform=keeps its arm, under baseline policy, through any reform applied later.Microsimulation(tax_benefit_system=...)withoutreform=, orapply_reformon a simulation built withoutreform=, leaves no arm, so the expense keeps E, and subsidy changes don't move it. Inpolicyengine_us, only tests call the first form.policyengine-us-data's CPS build also storesspm_unit_capped_work_childcare_expensesitself, fromSPM_CAPWKCCXPNS(cps.py:786), and the model accepts it as an input. Over that file neither rule runs in the stored year, as on main.scripts/microsim.pyraises this input to an assumed price over a dataset, but it reads subsidies and not the SPM, so this rule doesn't reach it.Test cost
test_spm_child_care_subsidy_invariants.pystays inREST_REMAINING_HEAVY_TESTS, and it retains one reformed system and builds two more for the real-reform cases. Its tests ran for 206 s in CI at6946cd74a4, against 102 s alone on a loaded Mac, where the process peaked at 4.5 GB resident.test_spm_child_care_reported_expense.pyadds two tests: one reads only the expense and its inputs from aMicrosimulation()over the whole file, and the other runs on a slice. In CI they took 7 s and 31 s.tests/core/test_reform_baseline_arm_inputs.pybuilds three plain simulations and three reformed ones. In CI at6946cd74a4its dataset test took 100 s and its situation test 37 s. Alone on the Mac, the all-time test took 9 s, and the whole file took 59 s and peaked at 3.6 GB.What this PR does not change
copay/rate/first_child,additional_childand the 7%maximum) start in January 2025 with that scale. The model backdates each parameter's first value to 2015, so a 2024 subsidy uses the statewide scale too. Add Texas CCS Board Contract Year 2024 provider payment rates #10598, now on main, adds BCY 2024 provider rates. I kept Texas in the 2024 list, because leaving it out drops the whole subsidy. Apply Texas Workforce Board parent share of cost scales before TX3C #10623, a draft, adds board scales for Alamo, Borderplex, Brazos Valley, Dallas County, Panhandle and South Texas. This PR's 2024 Texas case uses the model's default county, Anderson, in the East Texas board. Apply Texas Workforce Board parent share of cost scales before TX3C #10623 leaves that board on the statewide scale, so the case's expected $5,793.01 holds whichever PR merges first.axiom: n/a: Census publishes the SPM as a statistical measure, and rulespec-us encodes no SPM provision.
🤖 Generated with Claude Code