Use canonical SPM measurements and immutable population default - #9428
Merged
Conversation
Keep spm-calculator==1.0.0 from this branch over the protective spm-calculator==0.3.1 pin that PR #9427 landed on main; take main's automatic version bump to 1.825.1 and its lock, changelog and model changes unchanged. The registry lock is regenerated in the next commit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
spm-calculator 1.0.0 is now on PyPI, so the committed country lock no longer has to stay blocked. Regenerating with the guard's own uv invocation moves the root package to 1.825.1 and the calculator from 0.3.1 to 1.0.0, changing nothing else in the resolved graph. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Core's TaxBenefitSystem.clone() rebuilds the parameter tree node by node and empties its at-instant caches, and this country deep-copies every variable object on top. Doing that per simulation put a full policy rebuild in front of every household API request and started each one with cold parameter caches. An ordinary simulation applies no user reform and supplies no system, so nothing distinguishes its policy from the shared instance's. Share the parameter tree and the variable objects, and keep only the receipts and the variable registry private: every core operation a reform performs on a variable rebinds variables[name] to a new object rather than mutating the registered one, so the private dict is enough to keep the structural reform re-applied at this simulation's start instant out of the shared instance. 20 sequential single-household Simulation constructions plus household_net_income, one process, warm HF cache: 109.8s before (70.4s of it in construction), 5.9s after (0.6s in construction). scripts/ benchmark_household_simulation.py is the measurement. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…build The calculator treats only None and the empty string as an absent county, so a county FIPS input that is an integer, a missing value or a code that lost its leading zero was reported as an unavailable county assignment - pointing at the artifact rather than at the input. County FIPS is a string variable, so every such value reaches the forecast as a string; require a five-digit code and name the two fixes a caller has, sending county_fips as a five-digit string or selecting geography_kind="national". A dataset URI whose build id is not published reached the caller as a Hugging Face transport error that never mentioned which dataset the model was asked for. Name the URI and keep the Hub error as the cause. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The legacy policyengine-us-data CPS files cannot satisfy the contract this PR introduces, so the tests that ran the model over them could not pass: - cps_2023.h5 stores the formula-owned spm_unit_spm_threshold column, so the loader rejects it before any geography selection matters. It also stores county FIPS as the CPS within-state integer code. - enhanced_cps_2024.h5 loads and computes tax variables, and its county column does hold five-digit FIPS codes, but 18 of its 43,134 SPM units are a lone 15-to-17-year-old with no source-backed independence role, so those units classify no SPM measurement adult. Assert both of those failures instead, and move the society-wide coverage they used to carry - net income across the extended years, in-range income deciles, nonzero earnings - onto the default build, which is the population the model ships. The default-dataset tests stay real: the build id they resolve is published, so they download it rather than skipping. The future-dated reform regression needs two simulations over one population, which neither the legacy files nor two subsamples of the whole default build can provide. Slice whole households out of the shipped file instead, and add a non-vacuity guard so the pre-start equality cannot pass on a population the reform never reaches. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #9428 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 2 1 -1
Lines 20 18 -2
Branches 0 1 +1
=========================================
- Hits 20 18 -2
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:
|
CI on the reviewed head found five more baseline and contrib files whose cases compute a resource, a benefit aggregate or a marginal tax rate and therefore reach the canonical housing cap: CBO after-tax income, marginal tax rate including health benefits, the Trump dividend benefit aggregates, and CA CPUC countable income and its integration cases. Apply the same repair as the eight files already in this PR and supply a zero housing subsidy, which also settles the 2021 marginal-tax-rate case: with no housing aid, the measurement year is never looked up. Each file passes with itemization branching on and off. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An ordinary simulation is new rather than cloned, so it never inherits a receipt; the flag had no caller. Branch cloning keeps its own shallow copy, which must share the variables dict rather than take a private one. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Brings main's Washington working families credit work and its 1.825.2 version bump, so the committed registry lock root can be refreshed to match the version CI resolves on the merge commit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The pull-request checks run on the merge commit, so the committed lock root has to match main's pyproject version, not the version the branch carried before the merge. Only the root version changes; the guard's own refresh path verified the dependency graph is untouched. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The first pass only covered the path with no reform and no supplied system, and
that left the expensive clone on the two paths the suite and the household API
actually use most: a caller that builds one system and runs many households
through it (Simulation(tax_benefit_system=SYSTEM, ...)), and the baseline branch
core builds for a policy reform. Both hold policy identical to the shared
instance's, so both share it. A supplied system with a reform still gets the
full clone, because core applies the reform set to whatever system it is handed
and the caller's own system must not be the one it reforms.
Same machine, same 174 tests in tests/core/{payroll_contributions,
local_employee_taxes,employer_state_unemployment_tax_jurisdictions,
medicaid_slcsp_cost}, itemization branching on: 26.12s at main, 545.58s before
this commit, 26.56s after. The 20-iteration household benchmark is 4.74s.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The congress shard hung in batch 6 on this branch until CI killed it after 39 minutes without output. Per-subdir mode gives each proposal folder one subprocess regardless of the module's own memory model, and congress/tlaib carries reform-combo weight 8.25: it peaked at 15.0 GB on the 16 GB runner at main (run 34637166889) and reaches 18.1 GB on this branch locally, so on the runner it swaps rather than fails. Pack any folder whose distinct combos exceed MAX_BATCH_COMBO_WEIGHT by combo weight instead; tlaib becomes two subprocesses of weight 4.0 and 4.25, and no other congress folder changes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…es not support Main's tlaib batch peaks at 18.2 GB locally, the same as this branch, so the earlier wording that the branch adds per-combo memory is unsupported. What is supported: the runner has no headroom for this batch at main, and the branch run produced no output for 39 minutes before CI killed it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e to cap The capped housing subsidy asked the calculator for the canonical housing portion for every unit, so every consumer of the cap - household net income, benefits, marginal tax rates, and state programs whose countable income sums the cap, such as CA CPUC CARE and FERA - demanded SPM geography and composition from households that never use the measurement. Partners do not request SPM outputs, and two partner contract fixtures for LA households failed with SPM_GEOGRAPHY_REQUIRED for that reason alone. A unit with no housing assistance has nothing to cap: min(0, cap) is zero for any cap. Evaluate the housing portion for assisted units only, through the same provider path (masked_policyengine_amount), so the county and composition requirements, the typed errors and the receipts attach to exactly the units whose result depends on the measurement. Assisted units without a county still fail closed; an explicit national selection still computes them. The contract tests that pinned the old behavior now pin this one, and the thirteen fixture files that had been given a zero capped-subsidy input to sidestep the requirement return to their main-branch content: all 93 of their cases pass unmodified, as do the two partner fixtures. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…the wording The Fable review of the delta asked for three changes. The code-health test repeated a memory claim the main-branch baseline does not support; it now mirrors the batcher's wording. The test named for a mixed population built a single unit; two tests now build two households, one assisted with a county and one with neither, and pin that the assisted unit is capped, the other is zero, exactly one geography receipt is recorded, and an assisted unit without a county still fails closed beside an unassisted neighbour. PROGRESS.md was the fix lane's working record and described a state this branch has moved past, so it leaves the PR. The batcher docstring, its CLI help, the Makefile and docs/spm.md now describe per-subdir splitting and the assisted-only cap. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ct no tests Quick Feedback failed on this branch with exit code 5 although every selected test passed: two changed files under tests/, the populace_fixture helper and a fixtures module named like a test, collect nothing, and pytest reports that with exit code 5, which the runner counted as a failure. A selected path with nothing to collect is a support module, not a failing test; the runner now notes it and moves on, and real failures beside it still fail the run. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
MaxGhenis
marked this pull request as ready for review
September 11, 2026 23:16
This was referenced Sep 11, 2026
MaxGhenis
added a commit
that referenced
this pull request
Sep 12, 2026
…ization (#9444) The first automatic version bump after #9428 failed closed in the release lock guard: "Versioning changed the reviewed dependency graph". The runner installs the latest uv (0.12.13, unpinned) while the committed lock was written by 0.11.7. A fresh lock is left alone by either version, but the bump forces a re-resolution, and 0.12.13 then rewrites 37 resolution-marker lines in its own normalization; the guard compares the whole graph and refuses. Reproduced locally with uv 0.12.13 at version 2.0.0; under 0.11.7 the same refresh changes only the root version. Commit the lock as 0.12.13 writes it after a re-resolution (bump, lock, restore, lock: 37 marker lines change, no package or version changes), and pin uv 0.12.13 in every setup-uv step of the push, pull request and weekly lock workflows so the toolchain that checks the lock is the one that wrote it. With this lock the 2.0.0 refresh differs from the committed graph in the root version alone, by the guard's own comparison. uv 0.11.7 still passes `uv lock --check` on it, so developers need no upgrade. The release-lock document records how to move the pin. Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
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.
The country model now uses the canonical calculator artifact for SPM measurement, with observed county assignment by default and an explicit national or fixed-area selection. It preserves country tax/benefit formulas, applies the housing cap to the canonical amount before final storage conversion, and binds cloned/reform/baseline holders to their own policy variables.
Microsimulation()defaults to:That immutable tag is published. A real download with no offline override resolves it to a file whose SHA256 is
6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84, matching the published H5 hash. Readback confirms 57,240 households,household.county_fipsstored as five-digit strings, andperson.is_spm_independent_minor_rolepresent. The no-argumentMicrosimulation()tests therefore run for real; none of them is guarded or skipped.County measurement requires observed
county_fips; state-only requests can select national measurement explicitly withspm={"geography_kind": "national"}. A request with no usable county raises a country-levelSPM_GEOGRAPHY_REQUIREDnaming the two fixes a caller has — sendcounty_fipsas a five-digit string, or selectgeography_kind="national"— and that now covers integers, pandas missing values and codes that lost a leading zero, which the calculator alone would have reported as an unavailable county assignment. An unrecognised five-digit code still raisesSPM_GEOGRAPHY_UNAVAILABLE. Unsupported measurement years raiseSPM_YEAR_UNAVAILABLE; the artifact covers 2022–2035. Tax-only requests remain lazy. There is no consumer extrapolation or implicit geographic fallback.An unpublished or renamed dataset build id now surfaces as a country-level
FileNotFoundErrornaming the URI, with the Hugging Face error as its cause, instead of a transport error that never says which dataset the model was asked for.Population data must supply the source-backed
is_spm_independent_minor_roleprimitive and observed county inputs. Formula-owned stored SPM outputs are rejected; source observations belong in report-only columns. The legacypolicyengine-us-dataCPS files predate that contract and cannot produce resource or poverty outputs:cps_2023.h5stores the formula-ownedspm_unit_spm_thresholdcolumn, so the loader rejects it at construction. It also stores county FIPS as the CPS within-state integer code.enhanced_cps_2024.h5loads and computes tax variables, and its county column does hold five-digit FIPS codes, but 18 of its 43,134 SPM units are a lone 15-to-17-year-old with no source-backed independence role, so those units classify no SPM measurement adult.Both failures are now asserted as contract tests. The society-wide coverage those files used to carry — net income across the extended years, in-range income deciles, nonzero earnings — moved onto the shipped default build. The future-dated reform regression (#9075) runs two simulations over one 500-household slice of the shipped file, with a non-vacuity guard proving the reform reaches that population in its first year.
A simulation no longer clones policy state when its policy is the shipped policy. Core's
TaxBenefitSystem.clone()rebuilds the whole parameter tree node by node and empties its at-instant caches, and this country deep-copies every one of its 6,157 variable objects on top. Three paths were paying that per simulation, and none of them has any policy of its own to isolate: no reform and no supplied system; a supplied system with no reform, which is how a caller that builds one system and runs many households through it works, including the household API and much of this suite; and the baseline branch core builds for a policy reform. Each now shares the shipped parameter tree and variable objects and keeps only its receipts and its own variable registry private, which is safe because every core operation a reform performs on a variable rebindsvariables[name]to a new object rather than mutating the registered one. A supplied system with a reform still gets the full clone, because core applies the reform set to whatever system it is handed and the caller's own system must not be the one it reforms.test_ordinary_simulation_shares_default_policy_stateasserts the shared tree is reused and that nothing reaches the shared instance.Measured on one machine, one process, warm caches. 20 sequential single-household
Simulationconstructions plushousehold_net_income(scripts/benchmark_household_simulation.py):maind8fb269Net income is bit-identical across all three revisions for that household, so the comparison is like for like.
The same clone also sat behind the reviewed head's Rest job, which hit its 60-minute timeout at 52% of the Python suite where
main's Rest job takes 21m22s end to end. Two same-machine measurements of that suite:maind8fb269tests/corefiles, itemization branching onmake test-other-python, itemization branching offReceipts for this head:
pyproject.tomlversion is 1.825.1 and the committeduv.lockroot matches it;python .github/release_lock.py --committedexits 0.spm-calculatorresolves to 1.0.0 fromhttps://pypi.org/simple, sdist SHA256a99aac8c2c0bf81a9455105bbf872366f1ea9066a99cd793714881cd3db846dcand wheel SHA256e354937a5e1a4045d4966ed594a528d8b02866fabaac9bb5672017004b627305. The exactspm-calculator==1.0.0pin remains.policyengine-coreresolves to 3.30.2 in both the lock and this environment.spm-rolling-2026-09-09, content SHA2563d86d5c4c0423480e6b69b75d222ffa4a7a2639e4094df5ba2504af01be17173, default scenarioce_trend, 14 years covering 2022–2035.Test coverage on this head:
tests/core/test_spm_system.py(16 functions, 30 cases),tests/unit/test_spm_integration_contract.py(9, 24), andtests/policy/baseline/household/income/spm_unit/test_spm_unit_spm_threshold.py(11, 25). Eleven of those cases are new on this head, covering shared policy state, the URI-naming download error, integer county columns in a dataset, and non-string county inputs in a situation.Prepared from authoritative upstream main
b8ca61a23e1c7ca9f66181dee36d5d8bee89c916, preserving its Alabama 2025 deduction correction and automated version metadata. A full-index three-way patch transferred the reviewed canonical donor on42103db1b7d5be74bd28ebfc8e93adb2c7859079, including its fixture repairs.Related scientific integration history: #9081 (inspected; older draft 0.4.0 proposal). Also inspected #9421, the earlier 0.5.0 Git-pin draft. This is a fresh canonical integration PR; neither prior PR is overwritten or closed.
Automatic release safeguards check the committed PyPI lock before the version bump and before publication. After the existing bump and Towncrier run, a guarded refresh permits only the root version to change; resolver failure or dependency drift restores the reviewed lock bytes. Every dependency artifact must have a canonical PyPI file URL and SHA256 hash. The guard tests, the committed-lock check and the changelog check pass in CI on this branch.
Open, needing a decision before this can go green:
tests/policy/baseline/partners/analytics_coverage/edge_cases/state/ca/care.yamland.../fera.yamlfail withSPM_GEOGRAPHY_REQUIRED. CA CPUC countable income includesspm_unit_capped_housing_subsidy(PG&E Form 01-9077 counts housing subsidies), so CARE and FERA now need an SPM area. Both fixtures do name their county, ascounty_str: LOS_ANGELES_COUNTY_CA, but the model derives geography one-directionally (county_fips->county->county_str), so an explicitly named county never becomes a FIPS code. Three candidate fixes, none applied: add acounty_fipsinput to the two fixtures (a partner contract edit, gated); backfillcounty_fipsfrom an explicitly supplied county the way_backfill_state_code_from_strhandles state (correct, but 328 YAML test files supplycounty_str, 134 of them partner fixtures, so it would changecountyfrom the first-county-in-state fallback to the named county across the partner suite); or dropspm_unit_capped_housing_subsidyfrom the CPUC income list in favour ofhousing_assistance(changes partner-visible CARE/FERA amounts for assisted households). This is the same coordinated-rollout item as state-only callers losing resource and poverty outputs, and it belongs with the API and app PRs.No merge, release, publication, heavy population rerun or partner snapshot rewrite is included in this PR.
Root additions after the fix lane (b4453be → 21d0b0d)
contrib/congress/tlaib) hung without output until CI killed it. Per-subdir batching gave the whole proposal folder one subprocess regardless of the batcher's own memory model; tlaib carries reform-combo weight 8.25 and peaked at 15.0 GB on the 16 GB runner at main (run 34637166889), and locally the batch peaks at 18.2 GB on main and 18.1 GB on this branch, so the runner had no headroom and swapped instead of failing.test_batched.pynow packs any per-subdir folder whose distinct combos exceedMAX_BATCH_COMBO_WEIGHTby combo weight, pinned bytests/code_health/test_batched_per_subdir_budget.py. On this branch the shard now passes in 17 minutes with tlaib as two subprocesses peaking at 8,705 MB and 10,615 MB (run 34648331803); no other congress folder changes.spm_unit_capped_housing_subsidynow evaluates the housing portion for assisted units only throughpolicyengine_us.spm.masked_policyengine_amount(same provider path, so composition and county requirements, typed errors and receipts attach to exactly the units whose result depends on the measurement); unassisted units are 0 with no lookup. Assisted units without a county still fail closed withSPM_GEOGRAPHY_REQUIRED; an explicit national selection computes them. The contract tests pin the new behavior, the two partner fixtures pass unmodified, and the thirteen fixture files that had been given aspm_unit_capped_housing_subsidy: 0input return to their main-branch content (93 cases pass).🤖 Generated with Claude Code