Skip to content
Draft
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
1 change: 1 addition & 0 deletions changelog.d/liheap-la-credit-mixed-simulations.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed DC and Illinois LIHEAP payments and Louisiana's school readiness credit in simulations with multiple states or tax units.
107 changes: 107 additions & 0 deletions policyengine_us/tests/core/test_liheap_la_credit_mixed_simulations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""Check population independence through Core's masks and entity projectors.

YAML covers payment amounts. Hypothesis varies states, household membership,
tax units, income, bills and child care ratings, then compares each household
with its results in isolation. Fresh simulations share the default US system;
they keep their own inputs and calculation caches. This file runs in the
existing rest-python core group.
"""

import numpy as np
from hypothesis import example, given, settings, strategies as st

from policyengine_us import Simulation
from policyengine_us.variables.household.demographic.geographic.state_code import (
StateCode,
)

YEAR = 2026
STATES = tuple(state.name for state in StateCode)[:51]
OUTPUTS = (
"dc_liheap_payment",
"il_liheap_base_payment",
"il_liheap",
"il_ihwap_categorically_eligible",
"il_ihwap_eligible",
"la_school_readiness_credit",
"la_school_readiness_credit_non_refundable",
"la_school_readiness_credit_refundable",
)


@st.composite
def households(draw):
return (
draw(st.sampled_from(STATES)),
draw(
st.lists(
st.tuples(
st.integers(0, 2), # children per tax unit
st.integers(0, 100_000), # adult earnings
st.integers(0, 5_000), # heating bill
st.integers(0, 500), # child care credit
st.integers(1, 5), # facility rating
),
min_size=1,
max_size=2,
)
),
)


def _calculate(households):
situation = {
group: {}
for group in (
"people",
"marital_units",
"tax_units",
"families",
"spm_units",
"households",
)
}
for i, (state, tax_units) in enumerate(households):
household_members = []
for j, (children, earnings, bill, credit, rating) in enumerate(tax_units):
adult = f"adult_{i}_{j}"
members = [adult] + [f"child_{i}_{j}_{k}" for k in range(children)]
household_members.extend(members)
for member in members:
situation["people"][member] = {
"age": {YEAR: 40 if member == adult else 3},
"employment_income": {YEAR: earnings if member == adult else 0},
"heating_expense_person": {YEAR: bill if member == adult else 0},
"la_quality_rating_of_child_care_facility": {YEAR: rating},
}
situation["marital_units"][member] = {"members": [member]}
situation["tax_units"][f"tax_unit_{i}_{j}"] = {
"members": members,
"la_refundable_cdcc": {YEAR: credit},
"la_non_refundable_cdcc": {YEAR: 0},
}
for group in ("families", "spm_units", "households"):
situation[group][f"{group}_{i}"] = {"members": household_members}
situation["households"][f"households_{i}"]["state_code"] = {YEAR: state}
simulation = Simulation(situation=situation)
return {name: simulation.calculate(name, YEAR).copy() for name in OUTPUTS}


@settings(max_examples=8, deadline=None, derandomize=True, database=None)
@example(population=[(state, [(1, 0, 2_000, 50, 5)]) for state in STATES])
@example(
population=[
("DC", [(0, 0, 2_000, 0, 1)]),
("IL", [(0, 0, 2_000, 0, 1)]),
("LA", [(1, 0, 0, 50, 5), (0, 40_000, 0, 120, 4)]),
("LA", [(2, 30_000, 0, 75, 3)]),
("NY", [(0, 0, 0, 0, 1)]),
]
)
@given(population=st.lists(households(), min_size=1, max_size=5))
def test_households_keep_their_results_in_any_state_mix(population):
combined = _calculate(population)
isolated = [_calculate([household]) for household in population]
for name in OUTPUTS:
expected = np.concatenate([result[name] for result in isolated])
np.testing.assert_array_equal(combined[name], expected, err_msg=name)
Original file line number Diff line number Diff line change
Expand Up @@ -337,3 +337,77 @@
# canonical heating_type (gas bill 1_400): min(1_500, 1_400) = 1_400
dc_liheap_heating_type: OIL
dc_liheap_payment: 1_400

- name: A DC household receives its payment alone.
period: 2026
input:
state_code: DC
age: 40
irs_gross_income: 0
heating_expense_person: 2_000
output:
# Multi-family electricity, level 1, size 1: min(904, 2_000) = 904.
dc_liheap_income_level: 1
dc_liheap_payment: 904

- name: Two DC households each retain their payment.
period: 2026
input:
state_code: [DC, DC]
age: [40, 40]
irs_gross_income: [0, 0]
heating_expense_person: [2_000, 2_000]
output:
# Each household receives min(904, 2_000) = 904, as it does alone.
dc_liheap_payment: [904, 904]

- name: A DC household retains its payment beside households in other states.
period: 2026
input:
state_code: [DC, NY, IL]
age: [40, 40, 40]
irs_gross_income: [0, 0, 0]
heating_expense_person: [2_000, 2_000, 2_000]
output:
# DC receives min(904, 2_000) = 904; the state mask zeroes NY and IL.
dc_liheap_income_level: [1, 0, 0]
dc_liheap_payment: [904, 0, 0]

- name: DC bounds matrix keys and preserves both endpoint rows.
period: 2026
input:
state_code: [DC, DC, DC, DC]
dc_liheap_eligible: [true, true, true, true]
dc_liheap_income_level: [0, 1, 10, 11]
dc_liheap_housing_type: [SINGLE_FAMILY, SINGLE_FAMILY, SINGLE_FAMILY, SINGLE_FAMILY]
heating_expense_person: [2_000, 2_000, 2_000, 2_000]
output:
# Clip levels to [1, 1, 10, 10]; single-family electricity pays
# min(1_265, 2_000) = 1_265 at level 1 and min(200, 2_000) = 200 at 10.
dc_liheap_payment: [1_265, 1_265, 200, 200]

- name: An over-income DC household receives no payment alone.
period: 2026
input:
state_code: DC
age: 40
irs_gross_income: 100_000
heating_expense_person: 2_000
output:
# The eligibility mask zeroes the payment despite the level 10 matrix row.
dc_liheap_income_level: 10
dc_liheap_eligible: false
dc_liheap_payment: 0

- name: An over-income DC household receives no payment beside a New York household.
period: 2026
input:
state_code: [DC, NY]
age: [40, 40]
irs_gross_income: [100_000, 0]
heating_expense_person: [2_000, 2_000]
output:
# Neither household qualifies for DC LIHEAP, so both payments remain zero.
dc_liheap_income_level: [10, 0]
dc_liheap_eligible: [false, false]
dc_liheap_payment: [0, 0]
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
- name: An Illinois LIHEAP recipient qualifies for IHWAP when simulated alone.
period: 2026
input:
state_code: IL
age: 40
irs_gross_income: 0
heating_expense_person: 2_000
ssi: 0
receives_ssi: false
il_tanf: 0
il_aabd: 0
housing_assistance: 0
output:
# All-electric, bracket 1, size 1: min(910, 2_000) = 910.
# With no crisis payment, LIHEAP totals 910 and establishes IHWAP
# categorical eligibility; zero income also passes its income test.
il_liheap: 910
il_ihwap_categorically_eligible: true
il_ihwap_eligible: true

- name: An Illinois LIHEAP recipient retains payment and IHWAP eligibility beside other states.
period: 2026
input:
state_code: [IL, NY, DC]
age: [40, 40, 40]
irs_gross_income: [0, 0, 0]
heating_expense_person: [2_000, 2_000, 2_000]
ssi: [0, 0, 0]
receives_ssi: [false, false, false]
il_tanf: [0, 0, 0]
il_aabd: [0, 0, 0]
housing_assistance: [0, 0, 0]
output:
# Illinois retains 910 + 0 = 910 and both eligibility flags.
# Illinois's state masks zero the payment and flags for NY and DC.
il_liheap: [910, 0, 0]
il_ihwap_categorically_eligible: [true, false, false]
il_ihwap_eligible: [true, false, false]
Original file line number Diff line number Diff line change
Expand Up @@ -488,3 +488,66 @@
# the canonical heating_type (gas bill 2_000): min(840, 2_000) = 840
il_liheap_heating_type: ALL_ELECTRIC
il_liheap_base_payment: 840

- name: An Illinois household receives its base payment alone.
period: 2026
input:
state_code: IL
age: 40
irs_gross_income: 0
heating_expense_person: 2_000
output:
# All-electric, bracket 1, size 1: min(910, 2_000) = 910.
il_liheap_income_bracket: 1
il_liheap_base_payment: 910

- name: An Illinois household retains its base payment beside households in other states.
period: 2026
input:
state_code: [IL, NY, DC]
age: [40, 40, 40]
irs_gross_income: [0, 0, 0]
heating_expense_person: [2_000, 2_000, 2_000]
output:
# Illinois receives min(910, 2_000) = 910; the state mask zeroes NY and DC.
il_liheap_income_bracket: [1, 0, 0]
il_liheap_base_payment: [910, 0, 0]

- name: An over-income Illinois household retains its base amount alone.
period: 2026
input:
state_code: IL
age: 40
irs_gross_income: 100_000
heating_expense_person: 2_000
output:
# The base formula has no income eligibility mask: bracket 4, size 1
# gives min(325, 2_000) = 325, while the total benefit remains zero.
il_liheap_income_bracket: 4
il_liheap_base_payment: 325
il_liheap: 0

- name: An over-income Illinois household retains its base amount beside a New York household.
period: 2026
input:
state_code: [IL, NY]
age: [40, 40]
irs_gross_income: [100_000, 0]
heating_expense_person: [2_000, 2_000]
output:
# Illinois retains min(325, 2_000) = 325; NY receives zero.
# Neither household qualifies for the Illinois total benefit.
il_liheap_income_bracket: [4, 0]
il_liheap_base_payment: [325, 0]
il_liheap: [0, 0]

- name: Illinois bounds matrix keys and preserves both endpoint rows.
period: 2026
input:
state_code: [IL, IL, IL, IL]
il_liheap_income_bracket: [0, 1, 4, 5]
heating_expense_person: [2_000, 2_000, 2_000, 2_000]
output:
# Clip brackets to [1, 1, 4, 4]; all-electric, size 1 pays
# min(910, 2_000) = 910 at bracket 1 and min(325, 2_000) = 325 at 4.
il_liheap_base_payment: [910, 910, 325, 325]
Loading