Repository navigation
Measure SPM poverty on legacy certified US sources (backward compatibility) - #599
Conversation
…independence role The certified ACS-local source stops every US SPM calculation with SPM_COMPOSITION_REQUIRED. Two temporary, hash-gated compatibilities fix this for year files cut from that exact source: - #597: ACS group-quarters SPM units (TYPEHUGQ 2/3 with matching RELSHIPP 37/38) are outside the SPM measurement, following Census's ACS household-population method. They never reach the canonical SPM provider, and their SPM thresholds, resources and poverty indicators are NaN. Poverty indicators become floats on this source. - #598: the source's ASEC/PUF people take is_spm_independent_minor_role from the certified national release's independence list, joined exactly on person and SPM unit. This resolves 28 independent-teen SPM units. ACS people keep the model's own fallback value. Ordinary tax, benefit and income results are unchanged, and nobody is dropped. The year file and output records bind both hashes; stale year files are regenerated and stale outputs are not reused. Both compatibilities are marked for replacement in the code and in docs/engineering/runbooks/acs-local-spm-compatibility.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
23f4e39 to
3031bd8
Compare
Restructure the ACS-local SPM fixes into the legacy_spm package, which derives the SPM declarations that legacy certified sources lack and hands them to the model as the inputs newer sources carry: - A typed registry keys legacy sources by certified SHA-256. - The scope is written as spm_unit_spm_universe_status (INCLUDED/OUTSIDE, the Microcosm #977 contract), with is_spm_independent_minor_role. - The model adapter runs in native mode when policyengine-us defines the scope input, and otherwise overrides the SPM chain without copying any formula: the housing cap runs through the installed formula. It refuses a chain it was not verified against. - Records carry a versioned profile. Year-file and output checks are total and exact, so unreadable, malformed or other-profile records are regenerated, not raised. - Declarations also apply to runs straight over a raw legacy file and to managed_microsimulation. - Poverty.rate is None when nobody in a group is measured. Add Hypothesis properties, a model-contract test that rebuilds the provider's dependents from the installed model, and integration tests for every load path. Reword docs, runbook and changelog as backward compatibility. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
policyengine.py 6.3.4 adds backward compatibility for legacy certified SPM sources such as the ACS-local Build O release: ACS group quarters are outside the SPM measurement, and ASEC/PUF records take the certified SPM independence role (PolicyEngine/policyengine.py#599). Without it, every ACS-local state and district calculation stops with SPM_COMPOSITION_REQUIRED. - Pin policyengine[models]==6.3.4 in the executor. The lockfile is regenerated once 6.3.4 is published; until then the lock check and image build fail by design. - Add PovertyRateValue to the shared contract. A poverty group with nobody measured now reports null instead of a fabricated 0, and the executor keeps policyengine.py's "nobody measured" rows (NaN rate or zero measured population) as null. The public OpenAPI golden is updated. - Extend the Utah image smoke check to assert the legacy SPM declarations record, group-quarters units outside the measurement, finite measured outcomes and valid poverty rates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR Review: Measure SPM poverty on legacy certified US sources (backward compatibility)Early feedback on the draft. The design is sound. Only registered sources get the declarations: the registry is keyed by certified SHA-256 and Build O is its one entry. The ACS household-population rule matches Census's method. The override wraps the installed SPM chain rather than forking it, and most failure modes are loud. I found no wrong-result or crash bugs against policyengine-us 2.2.1. The items below are gaps to close before this leaves draft: one formula really is copied (the docs say none is), one lookup works for a dataset's manifest name but not its URI, a few guard branches have no tests, and there is changelog and docs housekeeping. Critical (0)None found. Should address (5)1. The decile override copies the model formula, the docs say it doesn't, and nothing catches drift
2.
3. Tests do not cover the guards that keep failures loud These checks are the PR's guarantee against silent fallback, and none of them has a test:
4.
5. Both changelog fragments will render as double bullets
Suggestions (6)
Verified correct
CI statusPending. "Verify release inputs and bundle packages" and "Test documentation builds" pass. Changelog check, Lint, Mypy, Install + smoke-import (3.11 to 3.14) and Test (3.11 to 3.14) have not finished. The CI test job installs Branch status2 commits ahead and 0 behind Verdict: REQUEST_CHANGES |
DTrim99
left a comment
There was a problem hiding this comment.
Early feedback on the draft (details in the comment above). The design is sound: the hash-gated registry, the ACS group-quarters rule that matches Census's ACS SPM, and the override wrapping the installed formulas. I found no wrong-result or crash bugs against policyengine-us 2.2.1. Five should items: (1) the decile override re-implements spm_unit_income_decile despite the 'never copies a model formula' claim, so pin it in a contract test; (2) Build O is recognised only by manifest name, so the region hf:// URI for the same file is refused and ensure_datasets falls through to create_datasets and raises; (3) the loud-failure guards (SPM_UNIVERSE_REQUIRED, the year-file frame checks) are untested; (4) Poverty.rate changing from 0.0 to None affects every source, sits in a .fixed fragment, and breaks examples/us_budgetary_impact.py formatting; (5) both changelog fragments start with '- ' and render as '- - '.
Address the five should-fix items from review: 1. spm_unit_income_decile is the one installed formula the override re-implements. Name that exception in the module banner and runbook, and pin the installed method with a contract test. 2. Recognise a managed dataset by the URI its manifest name resolves to, the form the region registry uses, so load_datasets and ensure_datasets accept Build O year files referenced either way. 3. Test the guards that keep failures loud: SPM_UNIVERSE_REQUIRED for a year with no declaration, and every check_year_file_frames branch. 4. Move the cross-source Poverty.rate None change into its own changed fragment, document it in docs/outputs.md, and handle None in examples/us_budgetary_impact.py. 5. Drop the leading "- " from the changelog fragments, which towncrier rendered as double bullets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pytest used the 200,000-character deep-JSON string as its test ID, which exceeds Windows' environment-variable limit for PYTEST_CURRENT_TEST and prints in full in verbose CI logs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
policyengine.py 6.3.4 adds backward compatibility for legacy certified SPM sources such as the ACS-local Build O release: ACS group quarters are outside the SPM measurement, and ASEC/PUF records take the certified SPM independence role (PolicyEngine/policyengine.py#599). Without it, every ACS-local state and district calculation stops with SPM_COMPOSITION_REQUIRED. - Pin policyengine[models]==6.3.4 in the executor. The lockfile is regenerated once 6.3.4 is published; until then the lock check and image build fail by design. - Add PovertyRateValue to the shared contract. A poverty group with nobody measured now reports null instead of a fabricated 0, and the executor keeps policyengine.py's "nobody measured" rows (NaN rate or zero measured population) as null. The public OpenAPI golden is updated. - Extend the Utah image smoke check to assert the legacy SPM declarations record, group-quarters units outside the measurement, finite measured outcomes and valid poverty rates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes #597
Fixes #598
Problem
Every US SPM calculation on the certified ACS-local source stops with "SPM unit has no classified adult". This is the failure behind:
state/ut;A read-only scan of the full source found two causes:
Design:
policyengine.tax_benefit_models.us.legacy_spmregistryuniverseacs_household_population, following Census's ACS household-population method. ACS records withTYPEHUGQ2 or 3 and the matchingRELSHIPP(37 or 38) areOUTSIDE. ACS housing units andasec/puf_tax_detailrecords areINCLUDED. Age is never read, and unknown or inconsistent evidence raises.independencedeclarationsspm_unit_spm_universe_status(INCLUDED/OUTSIDE, the Microcosm #977 contract) andis_spm_independent_minor_role.recordLegacySPMRecordholding theLegacySPMProfile(registry entry plusADAPTER_REVISION). Checks are total (no stored value makes them raise) and exact (the whole profile must match), following #590's year-file stamp.runsmodel_adapterspm_unit_spm_universe_status, the declaration is just an input. override mode (policyengine-us 2.2.1): a variable-only reform wraps the installed SPM chain. Outside units never reach the canonical provider, and every SPM outcome for them is NaN. The override wraps the installed formulas with one exception.spm_unit_income_decileranks units, so it is recalculated over measured units with the installed method, and a contract test pins that method. The housing cap runs through the installed formula, because the allocation it reads is NaN for outside units. The reform checks the chain's shape and refuses to run on a chain it wasn't verified against.Load paths covered:
create_datasets/ensure_datasetsyear files,Simulation.run()on a raw legacy file, andmanaged_microsimulation.Output contract:
Poverty.rateis nowNonewhen nobody in the group is measured. Previously it was 0.0. This applies to every source, so it has its ownchangedchangelog fragment and is documented indocs/outputs.md.examples/us_budgetary_impact.pyhandles it.Povertyrefuses to sum NaN indicators onto a group entity.release_bundleand run records carrylegacy_spm_declarations, including themodel_mode.Not changed: country, core, calculator and dataset pins, and environment variables. The independence list comes from the same public Hugging Face repository as the sources, with no credentials needed. Ordinary tax, benefit and income calculations are unchanged, and nobody is dropped.
Validation
Tests:
None;managed_microsimulation;SPM_UNIVERSE_REQUIREDfor a year with no declaration;ruff formatandruff checkare clean on new code; the remaining lint errors are already onmain.Full certified source, read-only: running the derivation on the real file gives:
Real Utah partition, with its hash checked and served under the certified identity for validation only, for 2025–2027:
Follow-ups
Unmanaged copies of a legacy source get declarations in
Simulation.run(), which hashes the file, but not increate_datasets. Matching that would mean judging those year files by hashing the source, which Stamp US year files with the rename register they were cut under #590's staleness decision deliberately avoids. Source identity in year-file stamps (Tie US year files to the source dataset and package versions they were cut from #593) is the place to close this.Fold the record into the year-file and output stamps once policyengine.py Tie US year files to the source dataset and package versions they were cut from #593 and Stamp saved US outputs with the rename register they were calculated under #594 land.
Carry
Nonepoverty rates through the simulation API (_number(), the gateway schema) and API v1. Today they collapse to 0.policyengine-sim-api #726 adopts this release, and policyengine-api #3868 re-pins.
🤖 Generated with Claude Code