From 4b860071deb140153386fa94e42e01b11e8101f6 Mon Sep 17 00:00:00 2001 From: Ziming Date: Thu, 8 Oct 2026 20:48:02 -0400 Subject: [PATCH] Share one HHS SMI helper for LIHEAP income limits Add smi_size_factor() and liheap_smi_limit() to hhs_smi.py and have smi() use the size factor. Mississippi, Maryland and New Jersey LIHEAP read the 60% SMI limit from the shared helper, and Colorado CCAP reads smi() at its October date instead of repeating the size adjustment. Co-Authored-By: Claude Opus 5.5 --- changelog.d/liheap-smi-helper.changed.md | 1 + policyengine_us/variables/gov/hhs/hhs_smi.py | 26 ++++++++++++++++--- .../gov/states/co/ccap/co_ccap_smi.py | 15 ++--------- .../meap/eligibility/md_meap_income_limit.py | 8 ++---- .../ms/mdhs/liheap/ms_liheap_income_limit.py | 17 +++--------- .../eligibility/nj_liheap_income_limit.py | 12 +++------ 6 files changed, 36 insertions(+), 43 deletions(-) create mode 100644 changelog.d/liheap-smi-helper.changed.md diff --git a/changelog.d/liheap-smi-helper.changed.md b/changelog.d/liheap-smi-helper.changed.md new file mode 100644 index 00000000000..da0483e1d5f --- /dev/null +++ b/changelog.d/liheap-smi-helper.changed.md @@ -0,0 +1 @@ +Share one HHS state median income helper for the LIHEAP income limits in Mississippi, Maryland and New Jersey, and reuse the SMI household-size factor in Colorado CCAP. diff --git a/policyengine_us/variables/gov/hhs/hhs_smi.py b/policyengine_us/variables/gov/hhs/hhs_smi.py index d161fc27634..a4cea5f043a 100644 --- a/policyengine_us/variables/gov/hhs/hhs_smi.py +++ b/policyengine_us/variables/gov/hhs/hhs_smi.py @@ -1,17 +1,37 @@ from policyengine_us.model_api import * -def smi(unit_size, state, period, parameters): +def smi_size_factor(unit_size, period, parameters): + """Return the 45 CFR 96.85(b) share of the four-person SMI for a size.""" p = parameters(period).gov.hhs.smi size_threshold = p.additional_person_threshold capped_size = clip(unit_size - 1, 0, size_threshold - 1) extra_persons = max_(unit_size - size_threshold, 0) - size_adjustment = ( + return ( p.household_size_adjustment.first_person + p.household_size_adjustment.second_to_sixth_person * capped_size + p.household_size_adjustment.additional_person * extra_persons ) - return p.amount[state] * size_adjustment + + +def smi(unit_size, state, period, parameters): + p = parameters(period).gov.hhs.smi + return p.amount[state] * smi_size_factor(unit_size, period, parameters) + + +def liheap_smi_limit(unit_size, state, rate, period, parameters): + """Return the LIHEAP limit at a rate of the size-adjusted SMI. + + HHS multiplies the rate times the four-person SMI by the size factor + (LIHEAP IM 2025-02, Attachment 4). Flooring the four-person amount, then + the size-adjusted amount, reproduces the published tables; HHS does not + state a rounding order. Pass an earlier date as the period to use a prior + year's SMI. + """ + four_person_smi = parameters(period).gov.hhs.smi.amount[state] + four_person_limit = np.floor(four_person_smi * rate) + size_factor = smi_size_factor(unit_size, period, parameters) + return np.floor(four_person_limit * size_factor) class hhs_smi(Variable): diff --git a/policyengine_us/variables/gov/states/co/ccap/co_ccap_smi.py b/policyengine_us/variables/gov/states/co/ccap/co_ccap_smi.py index 549adaf66a3..7f919461bd6 100644 --- a/policyengine_us/variables/gov/states/co/ccap/co_ccap_smi.py +++ b/policyengine_us/variables/gov/states/co/ccap/co_ccap_smi.py @@ -1,4 +1,5 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.hhs.hhs_smi import smi class co_ccap_smi(Variable): @@ -18,16 +19,4 @@ def formula(spm_unit, period, parameters): instant_str = f"{year}-10-01" else: instant_str = f"{year - 1}-10-01" - four_person_smi = parameters(instant_str).gov.hhs.smi.amount[state_code] - adjustment_mapping = parameters( - instant_str - ).gov.hhs.smi.household_size_adjustment - first_person_rate = adjustment_mapping.first_person - second_to_sixth_additional_rate = adjustment_mapping.second_to_sixth_person - seven_or_more_additional_rate = adjustment_mapping.additional_person - size_adjustment = ( - first_person_rate - + second_to_sixth_additional_rate * (min_(size, 6) - 1) - + seven_or_more_additional_rate * max_(size - 6, 0) - ) - return four_person_smi * size_adjustment / MONTHS_IN_YEAR + return smi(size, state_code, instant_str, parameters) / MONTHS_IN_YEAR diff --git a/policyengine_us/variables/gov/states/md/dhs/meap/eligibility/md_meap_income_limit.py b/policyengine_us/variables/gov/states/md/dhs/meap/eligibility/md_meap_income_limit.py index 8a87469af84..fe926ea1e8e 100644 --- a/policyengine_us/variables/gov/states/md/dhs/meap/eligibility/md_meap_income_limit.py +++ b/policyengine_us/variables/gov/states/md/dhs/meap/eligibility/md_meap_income_limit.py @@ -1,5 +1,5 @@ from policyengine_us.model_api import * -from policyengine_us.variables.gov.hhs.hhs_smi import smi +from policyengine_us.variables.gov.hhs.hhs_smi import liheap_smi_limit class md_meap_income_limit(Variable): @@ -28,10 +28,6 @@ def formula(spm_unit, period, parameters): p = parameters(period).gov.states.md.dhs.meap.eligibility size = max_(spm_unit("md_meap_household_size", period), 1) state = spm_unit.household("state_code_str", period) - base = parameters(period).gov.hhs.smi.amount[state] - factor = smi(size, state, period, parameters) / base - # ACF LIHEAP IM 2025-02 Attachment 4 (page 5) floors 60% of the - # four-person SMI, then floors each size's share of it. - smi_limit = np.floor(factor * np.floor(base * p.smi_rate)) + smi_limit = liheap_smi_limit(size, state, p.smi_rate, period, parameters) poverty_limit = spm_unit("md_meap_fpg", period) * p.fpg_rate return where(size >= p.smi_min_size, smi_limit, poverty_limit) diff --git a/policyengine_us/variables/gov/states/ms/mdhs/liheap/ms_liheap_income_limit.py b/policyengine_us/variables/gov/states/ms/mdhs/liheap/ms_liheap_income_limit.py index 8e13d27724f..0a1f06659fb 100644 --- a/policyengine_us/variables/gov/states/ms/mdhs/liheap/ms_liheap_income_limit.py +++ b/policyengine_us/variables/gov/states/ms/mdhs/liheap/ms_liheap_income_limit.py @@ -1,4 +1,5 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.hhs.hhs_smi import liheap_smi_limit class ms_liheap_income_limit(Variable): @@ -18,16 +19,6 @@ def formula(spm_unit, period, parameters): p = parameters(period).gov.states.ms.mdhs.liheap size = spm_unit("ms_liheap_household_size", period) state = spm_unit.household("state_code_str", period) - federal = parameters(period).gov.hhs.smi - # The published FY2026 limits truncate the four-person 60% SMI amount - # first, then the household-adjusted amount. This reproduces all 20 rows; - # it is a reconciled numerical pattern, not an explicit rounding rule. - four_person_limit = np.floor(federal.amount[state] * p.income_limit) - adjustment = federal.household_size_adjustment - threshold = federal.additional_person_threshold - size_share = ( - adjustment.first_person - + adjustment.second_to_sixth_person * clip(size - 1, 0, threshold - 1) - + adjustment.additional_person * max_(size - threshold, 0) - ) - return where(size > 0, np.floor(four_person_limit * size_share), 0) + # The HHS floor order reproduces all 20 published FY2026 rows. + limit = liheap_smi_limit(size, state, p.income_limit, period, parameters) + return where(size > 0, limit, 0) diff --git a/policyengine_us/variables/gov/states/nj/dca/liheap/eligibility/nj_liheap_income_limit.py b/policyengine_us/variables/gov/states/nj/dca/liheap/eligibility/nj_liheap_income_limit.py index 21bfbd4c8a5..989f070c99c 100644 --- a/policyengine_us/variables/gov/states/nj/dca/liheap/eligibility/nj_liheap_income_limit.py +++ b/policyengine_us/variables/gov/states/nj/dca/liheap/eligibility/nj_liheap_income_limit.py @@ -1,5 +1,5 @@ from policyengine_us.model_api import * -from policyengine_us.variables.gov.hhs.hhs_smi import smi +from policyengine_us.variables.gov.hhs.hhs_smi import liheap_smi_limit class nj_liheap_income_limit(Variable): @@ -32,15 +32,11 @@ def formula(spm_unit, period, parameters): p = parameters(period).gov.states.nj.dca.liheap.eligibility size = max_(spm_unit("nj_liheap_household_size", period), 1) state = spm_unit.household("state_code_str", period) - smi_parameters = parameters(period).gov.hhs.smi - base = smi_parameters.amount[state] def monthly_limit(table_size): - factor = smi(table_size, state, period, parameters) / base - # ACF LIHEAP IM 2025-02 Attachment 4 (page 2) floors 60% of the - # four-person SMI, then floors each size's share of it; the fact - # sheet's monthly column rounds that half up (not half to even). - annual = np.floor(factor * np.floor(base * p.smi_rate)) + annual = liheap_smi_limit(table_size, state, p.smi_rate, period, parameters) + # The fact sheet's monthly column rounds the HHS annual limit half + # up (not half to even). return np.floor(annual / MONTHS_IN_YEAR + 0.5) monthly = monthly_limit(min_(size, p.max_table_size))