Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 34 additions & 6 deletions skills/policyengine-healthcare/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ This skill is the domain layer for analysts using the `policyengine` package; fo
programs use **policyengine-us**, for the shared calculation/reform mechanics use the
**policyengine** skill, and for building new health variables use **policyengine-model-development**.

Verified against policyengine 4.21.0 / policyengine-us 1.764.6 (2026-07). Re-verify variable names
and parameter values before reporting.
Verified against policyengine 4.21.0 / policyengine-us 1.764.6 (2026-07); the marked examples
re-verified on policyengine 6.1.2 / policyengine-us 2.2.1 (2026-09). Re-verify variable names and
parameter values before reporting.

## The one gotcha that dominates everything: health benefits are excluded from net income

Expand All @@ -49,7 +50,8 @@ Measure health impact one of these ways instead:
Δtax − Δbenefits, so it captures the health split automatically — do not hand-roll it from
`household_net_income`. See the policyengine skill for the population flow.

Verified end to end (single adult, $35k, Texas — ACA-eligible, not Medicaid-eligible):
Verified end to end (single adult, $35k, Harris County, Texas — ACA-eligible, not
Medicaid-eligible):

<!-- verify -->
```python
Expand All @@ -58,11 +60,11 @@ import policyengine as pe
r = pe.us.calculate_household(
people=[{"age": 45, "employment_income": 35_000}],
tax_unit={"filing_status": "SINGLE"},
household={"state_code": "TX"},
household={"state_code": "TX", "county_fips": "48201"}, # Harris County
year=2026,
extra_variables=["aca_ptc", "household_net_income_including_health_benefits"],
)
assert round(r.tax_unit.aca_ptc, 2) == 6_250.38 # a real PTC...
assert round(r.tax_unit.aca_ptc, 2) == 6_635.45 # a real PTC...
# ...but it is NOT in default net income; the gap is exactly the PTC:
gap = (r.household.household_net_income_including_health_benefits
- r.household.household_net_income)
Expand Down Expand Up @@ -170,7 +172,33 @@ p.gov.aca.state_rating_area_cost # indexed by state and rating area

## Geographic variation: age curves and family tiers

ACA premiums vary by rating area, and the age-rating rule is not uniform. Most states use the
ACA premiums vary by rating area, which the model looks up from the household's county, so pass
`county_fips` for any premium or PTC question. Without it the model uses the state's
alphabetically first county (`first_county_in_state`), silently: the $35k Texas adult above
lands in Anderson County (rating area 21) instead of Harris County (rating area 10), with a
different benchmark premium and PTC. Tracked in PolicyEngine/policyengine-us#9480.

<!-- verify -->
```python
import policyengine as pe

def aca(household):
r = pe.us.calculate_household(
people=[{"age": 45, "employment_income": 35_000}],
tax_unit={"filing_status": "SINGLE"},
household=household,
year=2026,
spm={"geography_kind": "national"}, # lets the county-less call run
extra_variables=["slcsp_rating_area", "slcsp", "aca_ptc"],
)
# slcsp is monthly; on an annual result it is the year's total benchmark premium
return r.household.slcsp_rating_area, round(r.tax_unit.slcsp, 2), round(r.tax_unit.aca_ptc, 2)

assert aca({"state_code": "TX"}) == (21, 8_856.62, 6_250.38) # Anderson County
assert aca({"state_code": "TX", "county_fips": "48201"}) == (10, 9_241.69, 6_635.45) # Harris
```

The age-rating rule is not uniform either. Most states use the
federal age curve, but the model carries **custom age curves for AL, DC, MA, MN, MS, OR, UT** and
**family-tier rating (not age-based) for NY and VT** (verified from
`policyengine_us/parameters/gov/aca/age_curves/`: `al, dc, ma, mn, ms, or, ut` plus `ny, vt`). When
Expand Down
7 changes: 7 additions & 0 deletions skills/policyengine-model-development/references/tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ assert both sides of a boundary in one case:
2024-09: true
```

Always quote `county_fips`: YAML reads an unquoted `06037` as the octal integer 3103, and the
model accepts only a five-digit string. Since policyengine-us 2.0 a case asserting an
SPM-dependent output (`spm_unit_spm_threshold`, `spm_unit_is_in_spm_poverty`, `in_poverty`, or
the SPM resources of a unit allocated housing assistance) also needs a household
`county_fips`; with `state_code` alone the calculation raises `SPMInputError`
(`SPM_GEOGRAPHY_REQUIRED`). Cases for other variables still run on `state_code` alone.

Any month works here. Annual inputs can remain scalars; supply monthly inputs with
period keys when their values vary through the year. This is useful whenever a MONTH-defined variable changes mid-year — a benefit standard that
re-bases in April, a waiver that starts in September, a rate that changes on a state's own schedule.
Expand Down
7 changes: 5 additions & 2 deletions skills/policyengine-tools/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ import modal
from _image_setup import snapshot_models
app = modal.App("my-tool-workers")
image = (modal.Image.debian_slim(python_version="3.11")
.pip_install("policyengine[us]==X.Y.Z", "pydantic") # top-level release (>=5.0.1): pins matched country model + certified data bundle
.pip_install("policyengine[us]==X.Y.Z", "pydantic") # top-level release (>=6.0.0): pins matched country model + certified data bundle
.run_function(snapshot_models)
.add_local_file("backend/simulation.py", remote_path="/root/simulation.py"))

Expand All @@ -178,7 +178,10 @@ policyengine as pe; sim = pe.us.managed_microsimulation(reform=...)` (same weigh
MicroSeries surface: `.calc()` US / `.calculate()` UK; provenance on
`sim.policyengine_bundle`) — never a directly-imported country-package `Microsimulation`,
whose default dataset can lag the certified bundle.
Household-level endpoints may use `from policyengine_us import Simulation`. Pre-cache the
Household-level endpoints may use `from policyengine_us import Simulation`; a US situation
needs a household `county_fips` (five-digit string) or `spm={"geography_kind": "national"}`
for SPM outputs, and the county drives county-dependent rules, so collect it in the form (the
`policyengine` skill, "US geography"). Pre-cache the
certified dataset by constructing the managed microsim inside `snapshot_models()` (UK builds
need `HUGGING_FACE_TOKEN` — private dataset repo). Details: the `policyengine` skill.

Expand Down
103 changes: 85 additions & 18 deletions skills/policyengine-us/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ reform dicts, datasets — from the **policyengine** skill; read that first for
US-specific. For building new US variables or parameters, use **policyengine-model-development**.
For Medicaid / ACA / CHIP / Medicare, use **policyengine-healthcare**.

Verified against policyengine 4.21.0 / policyengine-us 1.764.6 (2026-07). Re-verify law-year
values before reporting — the model updates continuously.
Verified against policyengine 4.21.0 / policyengine-us 1.764.6 (2026-07); the marked examples
re-verified on policyengine 6.1.2 / policyengine-us 2.2.1 (2026-09). Re-verify law-year values
before reporting — the model updates continuously.

## The six entities, and which program lives on which

Expand Down Expand Up @@ -95,7 +96,7 @@ def odc(dependent):
return pe.us.calculate_household(
people=[{"age": 45, "employment_income": 40_000}, kid],
tax_unit={"filing_status": "HEAD_OF_HOUSEHOLD"},
household={"state_code": "CA"},
household={"state_code": "CA", "county_fips": "06037"},
year=2026,
).tax_unit.ctc

Expand All @@ -105,8 +106,11 @@ assert odc(True) == 500 # $500 credit for other dependents (ODC)

## Household calculation

`state_code` (not `state_code_str`) goes in the `household` dict. A low-income married couple with
two children, showing where each program lands:
`state_code` (not `state_code_str`) and `county_fips` (a five-digit string) go in the `household`
dict. Since policyengine 6.0.0 a state alone raises `SPMInputError`, because the default outputs
include SPM poverty and its threshold is set by county; the policyengine skill covers the
national-threshold alternative. A low-income married couple with two children in Albany County,
showing where each program lands:

<!-- verify -->
```python
Expand All @@ -120,22 +124,34 @@ r = pe.us.calculate_household(
{"age": 5},
],
tax_unit={"filing_status": "JOINT"},
household={"state_code": "NY"},
household={"state_code": "NY", "county_fips": "36001"}, # Albany County
year=2026,
extra_variables=["head_start"],
)
assert round(r.tax_unit.ctc) == 4_400 # 2 children x $2,200 (OBBBA, 2026)
assert round(r.tax_unit.eitc) == 7_316 # federal EITC (tax_unit)
assert round(r.spm_unit.snap) == 7_364 # annual = sum of 12 monthly allotments
assert round(r.household.household_net_income) == 62_681
assert round(r.spm_unit.snap) == 7_398 # annual = sum of 12 monthly allotments
assert round(r.household.household_net_income) == 46_246
assert round(r.person[3].head_start) == 16_491 # the 5-year-old; not in net income
```

Here `income_tax` is about −$10,691 (refundable EITC + CTC exceed liability), so
`household_net_income` exceeds gross earnings. `state_income_tax` can likewise be negative when
refundable state credits exceed state liability.

Variables outside the default output columns (poverty flags, `is_child`, state credits, most
intermediate variables) must be requested with `extra_variables=[...]`, or attribute access raises
`AttributeError` listing what *is* available:
**Head Start is left out of net income by default.** The 5-year-old's `head_start` value (the
program's per-enrollee cost) enters `household_benefits` only when
`gov.simulation.include_head_start_benefits_in_net_income` is true; it defaults to false
(policyengine-us 1.819.0 onward), the same in-kind treatment as health benefits (see
policyengine-healthcare). Before that release this household's net income was about $16,000
higher.

The default output columns are short: on `spm_unit`, `snap`, `tanf`, `spm_unit_net_income` and
the SPM poverty flags (`spm_unit_is_in_spm_poverty`, `spm_unit_is_in_deep_spm_poverty`); on
`person`, `age`, `is_child`, `is_adult`, `employment_income`, `ssi` and a few more. Anything else
(`in_poverty`, `person_in_poverty`, state credits, most intermediate variables) must be requested
with `extra_variables=[...]`, or attribute access raises `AttributeError` listing what *is*
available:

<!-- verify -->
```python
Expand All @@ -144,11 +160,11 @@ import policyengine as pe
r = pe.us.calculate_household(
people=[{"age": 35, "employment_income": 25_000}, {"age": 8}],
tax_unit={"filing_status": "HEAD_OF_HOUSEHOLD"},
household={"state_code": "NY"},
household={"state_code": "NY", "county_fips": "36001"},
year=2026,
extra_variables=["in_poverty", "person_in_poverty", "is_child"],
extra_variables=["in_poverty", "person_in_poverty"],
)
assert r.person[1].is_child == 1
assert r.person[1].is_child == 1 # a default column
r.spm_unit.in_poverty # SPM-unit poverty flag
r.person[0].person_in_poverty # person-level projection
```
Expand Down Expand Up @@ -218,12 +234,12 @@ import policyengine as pe
r = pe.us.calculate_household(
people=[{"age": 30, "employment_income": 20_000}, {"age": 4}],
tax_unit={"filing_status": "HEAD_OF_HOUSEHOLD"},
household={"state_code": "CA"},
household={"state_code": "CA", "county_fips": "06037"},
year=2026,
extra_variables=["ca_eitc"],
)
assert round(r.tax_unit.eitc) == 4_427 # federal EITC
assert round(r.tax_unit.ca_eitc, 2) == 410.16 # CalEITC (state)
assert round(r.tax_unit.ca_eitc, 2) == 401.89 # CalEITC (state)
assert r.tax_unit.state_income_tax < 0 # refundable CA credits exceed liability
```

Expand All @@ -235,8 +251,56 @@ state's variables with `[v for v in CountryTaxBenefitSystem().variables if v.sta
SNAP is the US benefit whose timing most often surprises analysts. Everything below is verified
against the model source.

**The county sets New York's and Alaska's utility allowances.** SNAP utility allowances follow
`snap_utility_region`, which reads the county in Alaska and New York. The same Albany County
family from "Household calculation", moved to New York County (Manhattan), gets more SNAP, pays
New York City tax, and crosses the SPM poverty line (Manhattan's SPM threshold is higher):

<!-- verify -->
```python
import policyengine as pe

def ny(county_fips):
r = pe.us.calculate_household(
people=[
{"age": 35, "employment_income": 25_000},
{"age": 33, "employment_income": 0},
{"age": 8},
{"age": 5},
],
tax_unit={"filing_status": "JOINT"},
household={"state_code": "NY", "county_fips": county_fips},
year=2026,
)
return round(r.spm_unit.snap), bool(r.spm_unit.spm_unit_is_in_spm_poverty)

assert ny("36001") == (7_398, False) # Albany County: rest-of-state utility allowance
assert ny("36061") == (8_061, True) # New York County: New York City utility allowance
```

**Adults subject to the ABAWD rules need their hours.** `weekly_hours_worked_before_lsr`
defaults to 0 (policyengine-us 2.x), so a childless adult who is subject to the ABAWD work
requirement (`is_subject_to_snap_abawd`) fails it (`meets_snap_abawd_work_requirements`) unless
you pass their hours:

<!-- verify -->
```python
import policyengine as pe

def snap(**hours):
return pe.us.calculate_household(
people=[{"age": 30, "employment_income": 8_000, **hours}],
tax_unit={"filing_status": "SINGLE"},
household={"state_code": "CA", "county_fips": "06037"},
year=2026,
).spm_unit.snap

assert round(snap()) == 298 # hours default to 0
assert round(snap(weekly_hours_worked_before_lsr=25), 2) == 3_607.57
```

**SNAP is monthly.** `snap` and its inputs have `definition_period = MONTH`. When
`calculate_household` reports an annual figure (like the $7,364 above), it is the **sum of twelve
`calculate_household` reports an annual figure (like the $7,398 above), it is the **sum of twelve
monthly allotments** — not one annual calculation. That is why SNAP produces partial-year cliffs
that annual-only reasoning misses.

Expand Down Expand Up @@ -272,7 +336,10 @@ The 30% expected-contribution rate is itself a parameter — `gov.usda.snap.expe

To debug one month, drop to the country package and calculate at a monthly period:
`from policyengine_us import Simulation; Simulation(situation=...).calculate("snap_normal_allotment", "2026-11")`, with `.trace = True` for the dependency tree. That direct-import surface is for tracing,
not for analysis results you report (see the policyengine skill).
not for analysis results you report (see the policyengine skill). It applies the same SPM rule:
to trace an SPM output (`in_poverty`, `spm_unit_spm_threshold`), put a year-keyed five-digit
`county_fips` in the situation's household (`{"2026": "06037"}`) or construct
`Simulation(situation=..., spm={"geography_kind": "national"})`.

## Datasets and population analysis

Expand Down
2 changes: 1 addition & 1 deletion skills/policyengine-writing/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -650,7 +650,7 @@ Install PolicyEngine from PyPI:
uv pip install "policyengine[us]"
```

This installs version 5.0.1 or later, which pins the US model and the
This installs version 6.0.0 or later, which pins the US model and the
certified Microcosm data bundle.
```

Expand Down
Loading
Loading