Repository navigation
Conversation
calculate_household put every person in one marital unit, family, SPM unit and tax unit. policyengine-us defines a marital unit as one unmarried person or one married couple, and its SSI and Medicaid rules read a two-person marital unit as a couple and sum some incomes across it. So a single parent with one child got the couple's Medicaid income limit, and a teenager's earnings counted as their married parents' SSI income. marital_unit, family, spm_unit and tax_unit now also accept a list of groups, each a mapping with members (indexes into people) plus the group's inputs. Every person must belong to exactly one group of the entity; a list that leaves someone out, names someone twice or names an index outside people raises before any calculation. An entity passed as a list comes back as a list of results in the same order, with one axis series per group under axes. A mapping still builds one group holding everyone, so existing calls return the same results. Fixes #738 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reject NumPy booleans and other non-index types in members, and give an empty members list its own error. Name a one-group list's position in validation errors and point the misplaced-variable hint at the listed entity's groups. Cover family and SPM unit lists, three marital units, perpendicular axes and an axis on a group variable. Add a differential test against the builder on origin/main, and correct the docs and changelog. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tolist() turned datetime64 and timedelta64 arrays into integers before _person_index could refuse them, so keep NumPy's scalars until then. SSI adds up income across the unit only when the couple computation applies, which excludes exactly one member in a medical treatment facility. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4 of 8 tasks
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.
Fixes #738
Summary
pe.us.calculate_householdput every person in one marital unit, family, SPM unit and tax unit. policyengine-us defines a marital unit as one unmarried person or one married couple, and two of its rules depend on that. Medicaid's optional pathway for seniors and people with disabilities applies the couple income limit to a marital unit of exactly two people, so a single parent with one child got the couple's limit. SSI treats two aged, blind or disabled heads or spouses in one marital unit as a couple and then adds up income across the whole unit, so a teenager's earnings counted toward their aged parents' SSI (#738 has the reproductions).marital_unit,family,spm_unitandtax_unitnow also accept a list of groups. Each group is a mapping withmembers, a list of indexes intopeople, plus that group's own inputs:EntityResults in the same order; under axes, each group's variables are axis series, as each person's are.EntityResult, so existing calls return the same results.householdstays a single mapping:calculate_householdcomputes one household.people, passesmembersin the mapping form, or passes a list forhouseholdraisesValueErrorbefore any calculation, naming the entity and group.memberstakes a list, tuple, range or one-dimensional NumPy array of integer indexes. Booleans (NumPy's included), floats, strings, bytes,bytearray,memoryviewand mappings raise.changelog.d/738.changed.md).docs/households.mddocuments the list form with the SSI and adult-child tax-unit examples, and its "Accessing the result" section now describes the result as adictwith attribute access (it said Pydantic model). It also says what each group needs (policyengine-us makes the oldest person aged 18 or over in each tax unit its head, and SPM measurement needs an adult in each SPM unit) and how to see each person's group: pass{entity}_idas a group input.Invariants
Stated in
tests/test_us_household_groups.pyand checked there:calculate_householdaccepts a list of groups exactly when it assigns every person to exactly one group, and the situation it builds has memberships equal to that partition (Hypothesis, 200 random partitions). Dropping a person raises "belong to no", and repeating one raises "appears" (200 more).policyengine_us.Simulationbuilt by hand with the same groups:[0::2]and[1::2]slices.SPM_COMPOSITION_REQUIRED, and a tax unit of children has no head.Review round 1 (5 November 2026)
An independent review (Opus, read-only) asked for these changes, and this push makes them:
marital_unit_id, which stays 0 for every group unless the caller passes it. It now compares SSI's joint-claim flag and marital earned income, group sizes and outputs on every entity, and a new test passes ids as group inputs and finds them on each group and member.np.True_to failoperator.index; on NumPy 2.1.3 it returns 1, somembers=[np.True_, 0]passed as[1, 0]. NumPy booleans now raise, a one-dimensional NumPy array of indexes works, andbytearrayandmemoryviewraise. An emptymembersgets its own message.HouseholdResult's docstring covers lists, the changelog notes the empty-list change, and the docs no longer readincome_taxoff a person.Review rounds 2 and 3 (5 November 2026)
Round 2 (GPT-6.1 Sol, read-only) requested two changes, and 393a569 makes both:
tolist()turneddatetime64andtimedelta64arrays into integers before the index check ran, sonp.array([0, 1], dtype="datetime64[ns]")resolved to members[0, 1]. The resolver now keeps NumPy's scalars until the check, and the tests cover datetime, timedelta and float arrays.ssi_couple_computation_applies.Round 3 (GPT-6.1 Sol, the delta from 7c995cf to 393a569) approved with no findings. It ran the validators on all 33 resolver cases and NumPy dtypes, and ran the 2.2.1 SSI formulas across 64 facility and payment combinations.
Checks
tests/test_us_household_groups.py,test_household_impact.py,test_household_calculator_snapshot.pyandtest_spm_household.py: 141 passed.tests/test_us_household_groups.pyat 393a569: 55 passed.test_uk_cgt_default.py,test_uk_cgt_responses.py,test_uk_dated_reforms.py,test_uk_budget_reform_contract.py,test_dict_reforms_on_simulation.py,test_spm_selection.py,test_us_reform_application.py): 243 passed.ruff format --check .andruff check .pass with ruff 0.17.3, which CI installs. The venv's older ruff 0.12.11 reports UP038 in 66 places, 64 of them already on main; this PR keeps the repository's tuple style inisinstance.mypy src/policyenginereports nothing inus/household.py(CI runs mypy as informational).No country-package pins change.
axiom: n/a: wrapper API for household group structure; no policy encoding change
🤖 Generated with Claude Code