From 18377c9db9ea9772a047663b474cc8e091301437 Mon Sep 17 00:00:00 2001 From: Ziming Date: Sun, 8 Nov 2026 22:01:40 -0500 Subject: [PATCH 01/11] Add a national LIHEAP total and LIHEAP cooling registry notes Sum each state's regular LIHEAP payment in a new liheap variable, add an Illinois regular heating payment without crisis assistance, add a Michigan Home Heating Credit registry entry, and record each state's cooling treatment in its registry note. Co-Authored-By: Claude Opus 5.5 --- changelog.d/liheap-aggregate.added.md | 1 + .../gov/hhs/liheap/regular_payments.yaml | 64 ++++++++++ policyengine_us/programs.yaml | 74 ++++++----- .../baseline/gov/hhs/liheap/liheap.yaml | 120 ++++++++++++++++++ .../liheap/il_liheap_regular_heating.yaml | 65 ++++++++++ .../variables/gov/hhs/liheap/liheap.py | 11 ++ .../dceo/liheap/il_liheap_regular_heating.py | 13 ++ .../ma/doer/liheap/payment/ma_liheap.py | 1 + 8 files changed, 317 insertions(+), 32 deletions(-) create mode 100644 changelog.d/liheap-aggregate.added.md create mode 100644 policyengine_us/parameters/gov/hhs/liheap/regular_payments.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/hhs/liheap/liheap.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/il/dceo/liheap/il_liheap_regular_heating.yaml create mode 100644 policyengine_us/variables/gov/hhs/liheap/liheap.py create mode 100644 policyengine_us/variables/gov/states/il/dceo/liheap/il_liheap_regular_heating.py diff --git a/changelog.d/liheap-aggregate.added.md b/changelog.d/liheap-aggregate.added.md new file mode 100644 index 00000000000..010b258594b --- /dev/null +++ b/changelog.d/liheap-aggregate.added.md @@ -0,0 +1 @@ +Add a national LIHEAP total that sums each state's regular payment, an Illinois regular heating payment without crisis assistance, a Michigan Home Heating Credit registry entry, and cooling notes for each state's LIHEAP registry entry. diff --git a/policyengine_us/parameters/gov/hhs/liheap/regular_payments.yaml b/policyengine_us/parameters/gov/hhs/liheap/regular_payments.yaml new file mode 100644 index 00000000000..e811bea6fbd --- /dev/null +++ b/policyengine_us/parameters/gov/hhs/liheap/regular_payments.yaml @@ -0,0 +1,64 @@ +description: States provide these regular payments under the Low Income Home Energy Assistance Program. +values: + 2023-01-01: + - ak_hap # Alaska Heating Assistance Program + - al_liheap # Alabama LIHEAP + - ar_liheap # Arkansas LIHEAP + - az_liheap # Arizona LIHEAP + # California's three components are exclusive by eligibility, so their sum is its single regular payment. + - ca_liheap_utility_heating # California HEAP utility heating + - ca_liheap_covered_liquid_heating # California WPO propane and fuel oil heating + - ca_liheap_wood_heating # California WPO wood heating + - co_leap # Colorado LEAP + - ct_ceap # Connecticut CEAP + - dc_liheap_payment # DC LIHEAP + - de_liheap # Delaware LIHEAP + - fl_liheap # Florida LIHEAP + - ga_liheap # Georgia LIHEAP + - hi_liheap_subsidized_amount # Hawaii LIHEAP subsidized dwelling benefit + - ia_liheap # Iowa LIHEAP + - id_liheap # Idaho LIHEAP + - il_liheap_regular_heating # Illinois LIHEAP, without crisis assistance + - in_eap # Indiana EAP + - ks_liheap # Kansas LIEAP + - ky_liheap # Kentucky LIHEAP + - la_liheap # Louisiana LIHEAP + - ma_liheap # Massachusetts LIHEAP + - md_meap # Maryland MEAP + - me_heap # Maine HEAP + - mn_eap # Minnesota EAP + - mo_liheap # Missouri LIHEAP + - ms_liheap # Mississippi LIHEAP + - mt_liheap # Montana LIHEAP + - nc_lieap # North Carolina LIEAP + - nd_liheap # North Dakota LIHEAP + - ne_liheap # Nebraska LIHEAP + - nh_fap # New Hampshire FAP + - nj_liheap # New Jersey LIHEAP + - nm_liheap # New Mexico LIHEAP + - nv_liheap # Nevada LIHEAP + - ny_heap # New York HEAP + - oh_liheap # Ohio HEAP + - ok_liheap # Oklahoma LIHEAP + - or_liheap # Oregon LIHEAP + - pa_liheap # Pennsylvania LIHEAP + - ri_liheap # Rhode Island LIHEAP + - sc_liheap # South Carolina LIHEAP + - sd_liheap # South Dakota LIEAP + - tn_liheap # Tennessee LIHEAP + - tx_ceap # Texas CEAP + - ut_heat # Utah HEAT + - va_eap # Virginia EAP + - vt_liheap # Vermont Seasonal Fuel Assistance + - wa_liheap # Washington LIHEAP + - wi_wheap # Wisconsin WHEAP + - wv_lieap # West Virginia LIEAP + - wy_lieap # Wyoming LIEAP + +metadata: + unit: list + period: year + label: LIHEAP regular state payments + reference: + - title: 42 U.S.C. 8624(b)(1)(A) + href: https://www.law.cornell.edu/uscode/text/42/8624#b_1_A diff --git a/policyengine_us/programs.yaml b/policyengine_us/programs.yaml index a7cef450f8b..26816f1b9b9 100644 --- a/policyengine_us/programs.yaml +++ b/policyengine_us/programs.yaml @@ -987,7 +987,8 @@ programs: category: Benefits agency: HHS status: partial - coverage: AK, AR, AL, AZ, CA, CO, CT, DC, DE, GA, HI, Riverside County, MA, FL, IL, IA, ID, IN, KS, KY, LA, MD, ME, MN, MO, MS, MT, NC, ND, NE, NH, NJ, NM, NV, NY, OH, OK, OR, PA, SC, SD, RI, TN, TX, UT, VA, VT, WA, WI, WV, WY + coverage: AK, AR, AL, AZ, CA, CO, CT, DC, DE, GA, HI, Riverside County, MA, FL, IL, IA, ID, IN, KS, KY, LA, MD, ME, MI, MN, MO, MS, MT, NC, ND, NE, NH, NJ, NM, NV, NY, OH, OK, OR, PA, SC, SD, RI, TN, TX, UT, VA, VT, WA, WI, WV, WY + variable: liheap state_implementations: - state: AL status: partial @@ -1004,7 +1005,7 @@ programs: variable: ak_hap parameter_prefix: gov.states.ak.dpa.hap verified_years: "2027" - notes: FY2027 regular heating under the federal LIHEAP component (not the state-funded AAHP), with shared-dwelling and utility-allowance reductions applied to heating points as 7 AAC 44.080 directs. Crisis, cooling, weatherization and equipment assistance are out of scope. + notes: FY2027 regular heating under the federal LIHEAP component (not the state-funded AAHP), with shared-dwelling and utility-allowance reductions applied to heating points as 7 AAC 44.080 directs. Alaska offers no cooling assistance; crisis, weatherization and equipment assistance are out of scope. - state: AR status: partial name: Arkansas LIHEAP @@ -1035,7 +1036,7 @@ programs: variable: co_leap parameter_prefix: gov.states.co.cdhs.leap verified_years: "2026" - notes: FY2026 regular heating assistance, including heat-in-rent costs, income contribution rates and the subsidized-housing deduction. Crisis, cooling, weatherization and equipment assistance are out of scope. + notes: FY2026 regular heating assistance, including heat-in-rent costs, income contribution rates and the subsidized-housing deduction. Colorado offers no cooling assistance; crisis, weatherization and equipment assistance are out of scope. - state: CT status: partial name: Connecticut CEAP @@ -1043,7 +1044,7 @@ programs: variable: ct_ceap parameter_prefix: gov.states.ct.dss.ceap verified_years: "2026-2027" - notes: FY2026-FY2027 basic and rental heating assistance. The SNAP nominal benefit, crisis, weatherization and equipment assistance are out of scope. + notes: FY2026-FY2027 basic and rental heating assistance. Connecticut offers no cooling assistance; the SNAP nominal benefit, crisis, weatherization and equipment assistance are out of scope. - state: GA status: partial name: Georgia LIHEAP @@ -1065,7 +1066,7 @@ programs: full_name: Ohio Home Energy Assistance Program variable: oh_liheap parameter_prefix: gov.states.oh.odjfs.liheap - notes: Regular heating only, without the PIPP reduction; crisis, cooling and weatherization assistance are out of scope. Payment coefficients after 2023 are estimates from workbook covers and published benefit ranges, so no full benefit year is verified. + notes: Regular heating only, without the PIPP reduction; payment coefficients after 2023 are estimates from workbook covers and published benefit ranges, so no full benefit year is verified. Ohio has no regular cooling payment, and cooling help comes only through the Summer Crisis Program and weatherization; crisis and weatherization assistance are out of scope. - state: OK status: partial name: Oklahoma LIHEAP @@ -1081,7 +1082,7 @@ programs: variable: pa_liheap parameter_prefix: gov.states.pa.dhs.liheap verified_years: "2025-2027" - notes: FY2025-FY2027 regular heating only, from the FY2025 and FY2026 benefit matrices and the current benefit lookup. Crisis, cooling and weatherization assistance are out of scope. + notes: FY2025-FY2027 regular heating only, from the FY2025 and FY2026 benefit matrices and the current benefit lookup. Pennsylvania has no regular cooling payment, and cooling help comes only through DCED cooling equipment; crisis, equipment and weatherization assistance are out of scope. - state: RI status: partial name: Rhode Island LIHEAP @@ -1089,7 +1090,7 @@ programs: variable: ri_liheap parameter_prefix: gov.states.ri.dhs.liheap verified_years: "2026" - notes: FY2026 regular heating, including heat-in-rent grants and the $350 utility enhancement, assuming a qualifying utility account. Earlier years and FY2027 payments reuse FY2026 amounts as unverified estimates; crisis, cooling and weatherization are out of scope. + notes: FY2026 regular heating, including heat-in-rent grants and the $350 utility enhancement, assuming a qualifying utility account. Earlier years and FY2027 payments reuse FY2026 amounts as unverified estimates; Rhode Island offers no cooling assistance in FY2026 or FY2027, and crisis and weatherization are out of scope. - state: DC status: complete name: DC LIHEAP @@ -1110,6 +1111,7 @@ programs: full_name: Massachusetts Low Income Home Energy Assistance Program variable: ma_liheap parameter_prefix: gov.states.ma.doer.liheap + notes: Massachusetts offers no cooling assistance. - state: FL status: partial name: Florida LIHEAP @@ -1123,6 +1125,7 @@ programs: full_name: Illinois Low Income Home Energy Assistance Program variable: il_liheap parameter_prefix: gov.states.il.dceo.liheap + notes: Regular heating plus the winter crisis amount; the national liheap total counts only the regular heating payment. Illinois has no regular cooling payment, and cooling help comes only through crisis equipment repair and weatherization, which are out of scope. - state: IA status: partial name: Iowa LIHEAP @@ -1130,7 +1133,7 @@ programs: variable: ia_liheap parameter_prefix: gov.states.ia.hhs.liheap verified_years: "2026" - notes: FY2026 Regular Assistance heating benefit from the points matrix, with a flat amount for households that buy propane or fuel oil. Crisis assistance, cooling and weatherization are out of scope. + notes: FY2026 Regular Assistance heating benefit from the points matrix, with a flat amount for households that buy propane or fuel oil. Iowa has no regular cooling payment, and cooling help comes only through crisis Emergency Cooling; crisis assistance and weatherization are out of scope. - state: IN status: partial name: Indiana EAP @@ -1138,7 +1141,7 @@ programs: variable: in_eap parameter_prefix: gov.states.in.ihcda.eap verified_years: "2026" - notes: PY2026 regular heating and the winter electric allowance only. Crisis, cooling and weatherization assistance are out of scope. + notes: PY2026 regular heating and the winter electric allowance only. Indiana offers no cooling assistance; crisis and weatherization assistance are out of scope. - state: KS status: partial name: Kansas LIEAP @@ -1146,7 +1149,7 @@ programs: variable: ks_liheap parameter_prefix: gov.states.ks.dcf.liheap verified_years: "2025-2026" - notes: FY2025-FY2026 regular heating only. The FY2026 $130 minimum comes from the plan's estimated benefit levels; crisis, cooling and weatherization assistance are out of scope. + notes: FY2025-FY2026 regular heating only, with the FY2026 $130 minimum from the plan's estimated benefit levels. Kansas has no regular cooling payment, and cooling help comes only through weatherization; crisis and weatherization assistance are out of scope. - state: KY status: partial name: Kentucky LIHEAP @@ -1170,7 +1173,7 @@ programs: variable: md_meap parameter_prefix: gov.states.md.dhs.meap verified_years: "2025-2027" - notes: FY25-FY27 regular heating only, with the fuel tables, the Garrett County multiplier and the subsidized and nominal levels. The SNAP nominal payment, cooling, EUSP, arrearage, crisis and equipment benefits are out of scope. + notes: FY25-FY27 regular heating only, with the fuel tables, the Garrett County multiplier and the subsidized and nominal levels. Maryland has no regular cooling payment, and cooling help comes only through crisis and weatherization equipment work; the SNAP nominal payment, EUSP, arrearage, crisis and equipment benefits are out of scope. - state: ME status: partial name: Maine HEAP @@ -1178,7 +1181,14 @@ programs: variable: me_heap parameter_prefix: gov.states.me.msha.heap verified_years: "2026-2027" - notes: FY2026-FY2027 Fuel Assistance under both points matrices, with an estimated $22 per point because MaineHousing has not announced one. Crisis, the TANF fuel supplement, weatherization and CHIP are out of scope. + notes: FY2026-FY2027 Fuel Assistance under both points matrices, with an estimated $22 per point because MaineHousing has not announced one. Maine offers no cooling assistance; crisis, the TANF fuel supplement, weatherization and CHIP are out of scope. + - state: MI + status: partial + name: Michigan Home Heating Credit + full_name: Michigan Home Heating Credit + variable: mi_home_heating_credit + parameter_prefix: gov.states.mi.tax.income.credits.home_heating + notes: Michigan delivers regular heating help through the refundable Home Heating Credit, which is not in the national liheap total. Cooling help comes only through crisis and weatherization, which are out of scope. - state: MN status: partial name: Minnesota EAP @@ -1186,7 +1196,7 @@ programs: variable: mn_eap parameter_prefix: gov.states.mn.commerce.eap verified_years: "2026-2027" - notes: FFY2026-FFY2027 Primary Heat, Minnesota's single regular benefit, based on total household energy costs including electricity. Crisis, Energy Related Repair and weatherization are out of scope. + notes: FFY2026-FFY2027 Primary Heat, Minnesota's single regular benefit, based on total household energy costs including electricity, so it also covers cooling bills. Crisis, Energy Related Repair and weatherization are out of scope. - state: MO status: partial name: Missouri LIHEAP @@ -1194,7 +1204,7 @@ programs: variable: mo_liheap parameter_prefix: gov.states.mo.dss.liheap verified_years: "2026-2027" - notes: FY2026-FY2027 regular Energy Assistance only, including the heat-in-rent payment. Crisis (ECIP), cooling and weatherization assistance are out of scope. + notes: FY2026-FY2027 regular Energy Assistance, including the heat-in-rent payment; paid to an electric account, it also covers cooling bills from October to May. Crisis (ECIP), including Summer ECIP, and weatherization assistance are out of scope. - state: MS status: partial name: Mississippi LIHEAP @@ -1202,7 +1212,7 @@ programs: variable: ms_liheap parameter_prefix: gov.states.ms.mdhs.liheap verified_years: "2026" - notes: FY2026 regular assistance for heating fuel and electric bills, using the published matrix as printed. Crisis assistance and energy equipment are out of scope. + notes: FY2026 regular assistance for heating fuel and electric bills in any season, so it also covers cooling bills, using the published matrix as printed. Crisis assistance and energy equipment are out of scope. - state: MT status: partial name: Montana LIHEAP @@ -1210,7 +1220,7 @@ programs: variable: mt_liheap parameter_prefix: gov.states.mt.dphhs.liheap verified_years: "2026-2027" - notes: FFY2026-FFY2027 regular heating benefit from the base benefit matrix and district multipliers, including the modified benefit for subsidized heat-in-rent households. Emergency (CRF), proration and weatherization are out of scope. + notes: FFY2026-FFY2027 regular heating benefit from the base benefit matrix and district multipliers, including the modified benefit for subsidized heat-in-rent households. Montana has no regular cooling payment, and cooling help comes only through Emergency Assistance (CRF); CRF, proration and weatherization are out of scope. - state: NC status: partial name: North Carolina LIEAP @@ -1218,7 +1228,7 @@ programs: variable: nc_lieap parameter_prefix: gov.states.nc.ncdhhs.lieap verified_years: "2026-2027" - notes: FY2026-FY2027 regular heating only, with the dated resource waiver. Crisis, cooling and weatherization assistance are out of scope. + notes: FY2026-FY2027 regular heating only, with the dated resource waiver. North Carolina has no regular cooling payment, and cooling help comes only through the Crisis Intervention Program; crisis and weatherization assistance are out of scope. - state: ND status: partial name: North Dakota LIHEAP @@ -1226,7 +1236,7 @@ programs: variable: nd_liheap parameter_prefix: gov.states.nd.dhs.liheap verified_years: "2026" - notes: FY2026 regular heating, including heat-in-rent payments. Crisis, cooling, weatherization and equipment assistance are out of scope. + notes: FY2026 regular heating, including heat-in-rent payments. North Dakota has no regular cooling payment, and its cooling component only buys or repairs air conditioners and fans; crisis, weatherization and equipment assistance are out of scope. - state: NE status: partial name: Nebraska LIHEAP @@ -1242,7 +1252,7 @@ programs: variable: nh_fap parameter_prefix: gov.states.nh.doe.fap verified_years: "2027" - notes: PY2027 regular heating, estimating heating cost with bedrooms (at least one room) as the room count; unsubsidized heat in rent receives the ordinary schedule, which prints no heat-in-rent reduction. Crisis, cooling, weatherization and equipment assistance are out of scope. + notes: PY2027 regular heating, estimating heating cost with bedrooms (at least one room) as the room count; unsubsidized heat in rent receives the ordinary schedule, which prints no heat-in-rent reduction. New Hampshire has no regular cooling payment, and cooling help comes only through weatherization repairs; crisis, weatherization and equipment assistance are out of scope. - state: NJ status: partial name: New Jersey LIHEAP @@ -1250,7 +1260,7 @@ programs: variable: nj_liheap parameter_prefix: gov.states.nj.dca.liheap verified_years: "2026-2027" - notes: FY2026-FY2027 regular heating only, covering eligibility, countable income and the benefit grid, including the one renters cell that changes in FY2027; the program is not modeled before FY2022. Crisis, cooling and equipment assistance are out of scope. + notes: FY2026-FY2027 regular heating only, covering eligibility, countable income and the benefit grid, including the one renters cell that changes in FY2027; the program is not modeled before FY2022. The medically necessary cooling benefit is not modeled because it needs a medical certification that no input carries; crisis and equipment assistance are out of scope. - state: NM status: partial name: New Mexico LIHEAP @@ -1258,7 +1268,7 @@ programs: variable: nm_liheap parameter_prefix: gov.states.nm.hca.liheap verified_years: "2026-2027" - notes: FY2026-FY2027 annual benefit from the point system on the household's heating fuel or electric bill, with three income points up to 100% of the poverty guideline. Crisis assistance and weatherization are out of scope. + notes: FY2026-FY2027 annual benefit from the point system on the household's heating fuel or electric bill, with three income points up to 100% of the poverty guideline; the one benefit also covers cooling bills. Crisis assistance and weatherization are out of scope. - state: NV status: partial name: Nevada LIHEAP @@ -1274,7 +1284,7 @@ programs: variable: ny_heap parameter_prefix: gov.states.ny.otda.heap verified_years: "2024-2026" - notes: FY2024-FY2026 regular heating only, with the fuel base amounts, the Tier I and vulnerable supplements and the heat-included levels. Crisis, cooling, HERR and clean-and-tune assistance are out of scope. + notes: FY2024-FY2026 regular heating only, with the fuel base amounts, the Tier I and vulnerable supplements and the heat-included levels. New York has no regular cooling payment, and its Cooling Assistance Component only installs an air conditioner or fan; crisis, cooling equipment, HERR and clean-and-tune assistance are out of scope. - state: OR status: partial name: Oregon LIHEAP @@ -1297,7 +1307,7 @@ programs: full_name: South Dakota Low Income Energy Assistance Program variable: sd_liheap parameter_prefix: gov.states.sd.dss.liheap - notes: FY2024-FY2026 regular heating only; mixed-status income is counted in full and FY2026 Mid thresholds are inferred, so no year is verified. Crisis, cooling and weatherization are out of scope. + notes: FY2024-FY2026 regular heating only; mixed-status income is counted in full and FY2026 Mid thresholds are inferred, so no year is verified. South Dakota offers no cooling assistance; crisis and weatherization are out of scope. - state: TN status: partial name: Tennessee LIHEAP @@ -1305,14 +1315,14 @@ programs: variable: tn_liheap parameter_prefix: gov.states.tn.thda.liheap verified_years: "2026-2027" - notes: FY2026-FY2027 regular benefit from the THDA benefit matrices; FY2027 uses the posted LI-12 matrix while its plan and manual are drafts. Crisis and weatherization assistance are out of scope. + notes: FY2026-FY2027 regular benefit from the THDA benefit matrices, which also covers cooling bills; FY2027 uses the posted LI-12 matrix while its plan and manual are drafts. Crisis and weatherization assistance are out of scope. - state: TX status: partial name: Texas CEAP full_name: Texas Comprehensive Energy Assistance Program variable: tx_ceap parameter_prefix: gov.states.tx.tdhca.ceap - notes: Models the 150% FPG income limit, the alternative 60% SMI limit from the FY2024 state plan (applied through 2024), benefit amounts, and categorical eligibility through TANF enrollment, SNAP eligibility, and SSI receipt. Not modeled - net rather than gross gambling winnings, the Medicare premium deduction from counted Social Security, and the exclusion of means-tested veterans' pensions from counted income (10 TAC 6.4(c)) + notes: Models the Utility Assistance benefit, which also covers cooling bills under one yearly cap, with the 150% FPG income limit, the alternative 60% SMI limit from the FY2024 state plan (applied through 2024), benefit amounts, and categorical eligibility through TANF enrollment, SNAP eligibility, and SSI receipt. Not modeled - net rather than gross gambling winnings, the Medicare premium deduction from counted Social Security, and the exclusion of means-tested veterans' pensions from counted income (10 TAC 6.4(c)) - state: UT status: partial name: Utah HEAT @@ -1320,7 +1330,7 @@ programs: variable: ut_heat parameter_prefix: gov.states.ut.dwf.heat verified_years: "2026" - notes: Regular heating under the rules effective March 1, 2026, including the target-group and propane, kerosene or fuel oil supplements. Crisis, cooling, weatherization and equipment assistance are out of scope. + notes: The one HEAT benefit a program year under the rules effective March 1, 2026, including the target-group and propane, kerosene or fuel oil supplements; it also covers cooling bills. Crisis, weatherization and equipment assistance are out of scope. - state: VA status: partial name: Virginia EAP @@ -1336,7 +1346,7 @@ programs: variable: vt_liheap parameter_prefix: gov.states.vt.dcf.liheap verified_years: "2026" - notes: FY2026 regular heating with an estimated 51% payment rate, which DCF does not publish; later years carry it forward, so they omit the larger 2026-27 oil and kerosene benefit. Late-application proration of the regular benefit is not modeled; crisis, Heat and Eat and weatherization are out of scope. + notes: FY2026 regular heating with an estimated 51% payment rate, which DCF does not publish; later years carry it forward, so they omit the larger 2026-27 oil and kerosene benefit. Vermont offers no cooling assistance, late-application proration of the regular benefit is not modeled, and crisis, Heat and Eat and weatherization are out of scope. - state: WA status: partial name: Washington LIHEAP @@ -1344,7 +1354,7 @@ programs: variable: wa_liheap parameter_prefix: gov.states.wa.commerce.liheap verified_years: "2026" - notes: FY2026 regular heating from reported annual heating costs, with the $250 minimum when heat is included in rent. Crisis, cooling, weatherization and equipment assistance are out of scope. + notes: FY2026 regular heating from reported annual heating costs, with the $250 minimum when heat is included in rent. Washington has no regular cooling payment, and cooling help comes only through crisis, in-kind equipment and weatherization, which are out of scope. - state: WI status: partial name: Wisconsin WHEAP @@ -1352,7 +1362,7 @@ programs: variable: wi_wheap parameter_prefix: gov.states.wi.doa.wheap verified_years: "2026" - notes: FFY2026 regular heating benefit from the HE+ formula with the published heat proxy table. The non-heating electric benefit, crisis and weatherization are out of scope. + notes: FFY2026 regular heating benefit from the HE+ formula with the published heat proxy table. Wisconsin has no regular cooling payment, and cooling help comes only through summer crisis and weatherization; the non-heating electric benefit, crisis and weatherization are out of scope. - state: WV status: partial name: West Virginia LIEAP @@ -1360,7 +1370,7 @@ programs: variable: wv_lieap parameter_prefix: gov.states.wv.dhhr.lieap verified_years: "2026-2027" - notes: FY2026-FY2027 regular heating payments for households of one to twenty qualified members. Crisis, cooling, weatherization, equipment and the utility discount are out of scope. + notes: FY2026-FY2027 regular heating payments for households of one to twenty qualified members. West Virginia has no regular cooling payment, and cooling help comes only through Emergency Cooling crisis payments and unit repair or replacement; crisis, equipment, weatherization and the utility discount are out of scope. - state: WY status: partial name: Wyoming LIEAP @@ -1368,14 +1378,14 @@ programs: variable: wy_lieap parameter_prefix: gov.states.wy.dfs.lieap verified_years: "2026" - notes: FY2026 regular heating from the published fuel cost standards. Crisis, cooling, weatherization and equipment assistance are out of scope. + notes: FY2026 regular heating from the published fuel cost standards. Wyoming has no regular cooling payment, and cooling help comes only through crisis cooling-system repair and weatherization; crisis, weatherization and equipment assistance are out of scope. - state: ID status: partial name: Idaho LIHEAP full_name: Idaho Low Income Home Energy Assistance Program variable: id_liheap parameter_prefix: gov.states.id.dhw.liheap - notes: FY2026 regular heating; burden bands use the PY2007 heating-cost table, whose FY2026 currency is unconfirmed, so no year is verified. Crisis, cooling, weatherization and equipment assistance are out of scope. + notes: FY2026 regular heating; burden bands use the PY2007 heating-cost table, whose FY2026 currency is unconfirmed, so no year is verified. Idaho has no regular cooling payment, and cooling help comes only through crisis and weatherization; crisis, weatherization and equipment assistance are out of scope. # --- SSA programs --- - id: ssi diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/liheap/liheap.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/liheap/liheap.yaml new file mode 100644 index 00000000000..136b8caa264 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/hhs/liheap/liheap.yaml @@ -0,0 +1,120 @@ +# The national total adds each state's regular LIHEAP payment listed in +# gov.hhs.liheap.regular_payments. 2026 poverty guidelines (contiguous US): +# 15,960 for one person plus 5,680 for each additional person. + +- name: Case 1, a Texas CEAP utility payment flows into the national total. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 34 + # 12 an hour for 30 hours a week: 12 * 30 * 52 = 18,720. + employment_income_before_lsr: 18_720 + spm_units: + spm_unit: + members: [person1] + heating_type: ELECTRICITY + has_cooling_expense: true + # 165 a month for heating and air conditioning. + pre_subsidy_electricity_expense: 1_980 + households: + household: + members: [person1] + state_code: TX + output: + # 18,720 / 15,960 = 117% of the guideline, within 150%: eligible. + # Above 75%, so the 76-150% cap of 1,200 applies (10 TAC 6.309(e)(3)). + # min(1,200, electricity 1,980 + gas 0) = 1,200. + tx_ceap: 1_200 + liheap: 1_200 + +- name: Case 2, a Michigan household gets nothing because the Home Heating Credit is not in the total. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 41 + employment_income_before_lsr: 24_000 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + has_cooling_expense: true + # 85 a month for gas heat. + gas_expense: 1_020 + households: + household: + members: [person1] + state_code: MI + output: + # Michigan's regular heating help is the refundable Home Heating Credit, + # which the list leaves out, and Michigan has no regular cooling payment. + liheap: 0 + +- name: Case 3, Illinois counts the regular heating payment but not the crisis amount. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 29 + # 33 hours a week at the 15 Illinois minimum wage: 15 * 33 * 52 = 25,740. + employment_income_before_lsr: 25_740 + person2: + age: 6 + spm_units: + spm_unit: + members: [person1, person2] + heating_type: ELECTRICITY + has_cooling_expense: true + # 150 a month in an all-electric apartment. + pre_subsidy_electricity_expense: 1_800 + il_liheap_eligible_for_crisis_assistance: true + households: + household: + members: [person1, person2] + state_code: IL + output: + # Guideline for two: 15,960 + 5,680 = 21,640. + # 25,740 / 21,640 = 119%: income bracket 3 (101-150%). + # FY2026 all-electric amount, bracket 3, two people: 425. + # min(425, electricity 1,800) = 425. + il_liheap_base_payment: 425 + il_liheap_crisis_assistance_amount: 1_500 + # 425 + 1,500 = 1,925. + il_liheap: 1_925 + il_liheap_regular_heating: 425 + liheap: 425 + +- name: Case 4, a California wood heating payment counts once, with no utility payment added. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + # 1,500 a month. + social_security_retirement: 18_000 + spm_units: + spm_unit: + members: [person1] + heating_type: WOOD + wood_expense: 850 + ca_liheap_approved_wood_delivery_cost: 850 + # 100 a month for electricity, which does not heat the home. + pre_subsidy_electricity_expense: 1_200 + households: + household: + members: [person1] + state_code: CA + county: FRESNO_COUNTY_CA + output: + # The approved wood quote of 850 is below the 1,000 ordinary maximum. + ca_liheap_wood_heating: 850 + # A household with a wood or liquid-fuel payment gets no utility payment, + # so the electric bill adds nothing. + ca_liheap_utility_heating: 0 + ca_liheap_covered_liquid_heating: 0 + liheap: 850 diff --git a/policyengine_us/tests/policy/baseline/gov/states/il/dceo/liheap/il_liheap_regular_heating.yaml b/policyengine_us/tests/policy/baseline/gov/states/il/dceo/liheap/il_liheap_regular_heating.yaml new file mode 100644 index 00000000000..879ce06541d --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/il/dceo/liheap/il_liheap_regular_heating.yaml @@ -0,0 +1,65 @@ +# 2026 poverty guidelines (contiguous US): 15,960 for one person plus 5,680 +# for each additional person. Amounts are the FY2026 benefit matrix values. +# Wages are 40 hours a week at the 15 Illinois minimum wage: 15 * 40 * 52 = 31,200. + +- name: Case 1, the regular heating payment leaves out the crisis amount. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 33 + employment_income_before_lsr: 31_200 + person2: + age: 9 + person3: + age: 4 + spm_units: + spm_unit: + members: [person1, person2, person3] + heating_type: NATURAL_GAS + # 90 a month for gas heat. + gas_expense: 1_080 + il_liheap_eligible_for_crisis_assistance: true + households: + household: + members: [person1, person2, person3] + state_code: IL + output: + # Guideline for three: 15,960 + 2 * 5,680 = 27,320. + # 31,200 / 27,320 = 114%: income bracket 3 (101-150%). + # Natural gas amount, bracket 3, three people: 546. + # min(546, gas 1,080) = 546. + il_liheap_base_payment: 546 + # 546 + crisis 1,500 = 2,046. + il_liheap: 2_046 + il_liheap_regular_heating: 546 + +- name: Case 2, a heat-in-rent renter whose rent is below the threshold gets no regular heating payment. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 26 + employment_income_before_lsr: 31_200 + # 700 a month with heat included. + pre_subsidy_rent: 8_400 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + heat_expense_included_in_rent: true + households: + household: + members: [person1] + state_code: IL + output: + # 31,200 is within 200% of the one-person guideline (31,920). + il_liheap_income_eligible: true + # Rent of at most 8,400 does not exceed 30% of income (9,360). + il_liheap_eligible: false + # 31,200 / 15,960 = 195%: bracket 4; the cash amount for one person is 310, + # but the unit is ineligible, so the regular heating payment is zero. + il_liheap_base_payment: 310 + il_liheap_regular_heating: 0 diff --git a/policyengine_us/variables/gov/hhs/liheap/liheap.py b/policyengine_us/variables/gov/hhs/liheap/liheap.py new file mode 100644 index 00000000000..558cc05ff3c --- /dev/null +++ b/policyengine_us/variables/gov/hhs/liheap/liheap.py @@ -0,0 +1,11 @@ +from policyengine_us.model_api import * + + +class liheap(Variable): + value_type = float + entity = SPMUnit + definition_period = YEAR + label = "LIHEAP regular payments" + unit = USD + reference = "https://www.law.cornell.edu/uscode/text/42/8624#b_1_A" + adds = "gov.hhs.liheap.regular_payments" diff --git a/policyengine_us/variables/gov/states/il/dceo/liheap/il_liheap_regular_heating.py b/policyengine_us/variables/gov/states/il/dceo/liheap/il_liheap_regular_heating.py new file mode 100644 index 00000000000..ae502298336 --- /dev/null +++ b/policyengine_us/variables/gov/states/il/dceo/liheap/il_liheap_regular_heating.py @@ -0,0 +1,13 @@ +from policyengine_us.model_api import * + + +class il_liheap_regular_heating(Variable): + value_type = float + entity = SPMUnit + label = "Illinois LIHEAP regular heating payment" + unit = USD + definition_period = YEAR + defined_for = "il_liheap_eligible" + reference = "https://liheapch.acf.gov/docs/2026/benefits-matricies/IL_BenefitMatrix_Heat-Cool_2026.pdf#page=2" + + adds = ["il_liheap_base_payment"] diff --git a/policyengine_us/variables/gov/states/ma/doer/liheap/payment/ma_liheap.py b/policyengine_us/variables/gov/states/ma/doer/liheap/payment/ma_liheap.py index 301425e8bc3..cf00bb07d03 100644 --- a/policyengine_us/variables/gov/states/ma/doer/liheap/payment/ma_liheap.py +++ b/policyengine_us/variables/gov/states/ma/doer/liheap/payment/ma_liheap.py @@ -5,6 +5,7 @@ class ma_liheap(Variable): value_type = float entity = SPMUnit label = "Massachusetts LIHEAP payment" + unit = USD definition_period = YEAR defined_for = "ma_liheap_eligible" reference = "https://web.archive.org/web/20250720165524/https://www.mass.gov/doc/fy-2025-heap-income-eligibility-benefit-chart-may-8-2025/download" From 56db3706a2628ed83109bcefe5f5accb6c61c4e1 Mon Sep 17 00:00:00 2001 From: Ziming Date: Sun, 8 Nov 2026 22:06:00 -0500 Subject: [PATCH 02/11] Add Alabama LIHEAP regular cooling assistance Co-Authored-By: Claude Opus 5.5 --- changelog.d/al-liheap-cooling.added.md | 1 + .../al/adeca/liheap/cooling/fpg_year_lag.yaml | 17 ++ .../al/adeca/liheap/eligibility/fpg_rate.yaml | 2 +- .../payment/additional_person_amount.yaml | 2 +- .../al/adeca/liheap/payment/base_amount.yaml | 4 +- .../adeca/liheap/payment/iija_supplement.yaml | 15 +- .../liheap/payment/income_band_reduction.yaml | 4 +- .../maximum_payment_household_size.yaml | 4 +- policyengine_us/programs.yaml | 10 +- .../liheap/cooling/al_liheap_cooling.yaml | 190 ++++++++++++++++++ .../cooling/al_liheap_cooling_eligible.yaml | 73 +++++++ .../al_liheap_cooling_income_limit.yaml | 16 ++ .../gov/states/al/adeca/liheap/al_liheap.py | 30 +-- .../adeca/liheap/cooling/al_liheap_cooling.py | 45 +++++ .../cooling/al_liheap_cooling_eligible.py | 30 +++ .../cooling/al_liheap_cooling_income_limit.py | 24 +++ .../eligibility/al_liheap_income_limit.py | 31 +-- .../liheap/payment/al_liheap_base_payment.py | 28 ++- .../liheap/payment/al_liheap_income_band.py | 27 ++- 19 files changed, 490 insertions(+), 63 deletions(-) create mode 100644 changelog.d/al-liheap-cooling.added.md create mode 100644 policyengine_us/parameters/gov/states/al/adeca/liheap/cooling/fpg_year_lag.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_eligible.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_income_limit.yaml create mode 100644 policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling.py create mode 100644 policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_eligible.py create mode 100644 policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_income_limit.py diff --git a/changelog.d/al-liheap-cooling.added.md b/changelog.d/al-liheap-cooling.added.md new file mode 100644 index 00000000000..ba256d83edd --- /dev/null +++ b/changelog.d/al-liheap-cooling.added.md @@ -0,0 +1 @@ +Add Alabama LIHEAP regular cooling assistance. diff --git a/policyengine_us/parameters/gov/states/al/adeca/liheap/cooling/fpg_year_lag.yaml b/policyengine_us/parameters/gov/states/al/adeca/liheap/cooling/fpg_year_lag.yaml new file mode 100644 index 00000000000..25d2e4fba41 --- /dev/null +++ b/policyengine_us/parameters/gov/states/al/adeca/liheap/cooling/fpg_year_lag.yaml @@ -0,0 +1,17 @@ +description: Alabama uses federal poverty guidelines from this many years before the benefit year for regular cooling assistance under the Low Income Home Energy Assistance Program. +# The May-September cooling season runs on the chart ADECA updates to the +# calendar year's guidelines (May 15, 2026 for summer 2026). The model applies +# that chart to the whole season, including May 1-14 applications. +values: + 2025-10-01: 0 +metadata: + unit: year + period: year + label: Alabama LIHEAP cooling poverty guideline year lag + reference: + - title: "Alabama FY2026 LIHEAP State Plan, sections 1.1 and 3.5 (May-September cooling season; adopt the 2026 guidelines upon release) #page 4,12" + href: https://liheapch.acf.gov/docs/2026/state-plans/AL_Plan_2026.pdf#page=4 + - title: Alabama draft FY2027 LIHEAP State Plan, Attachment A (PY 2026 Payment Assistance Chart effective May 15, 2026) + href: https://adeca.alabama.gov/wp-content/uploads/FY27-LIHEAP-State-Plan.pdf#page=53 + - title: Alabama FY2027 LIHEAP State Plan, section 3.5 (adopt the 2027 guidelines upon release) + href: https://liheapch.acf.gov/docs/2027/state-plans/AL_Plan_2027.pdf#page=12 diff --git a/policyengine_us/parameters/gov/states/al/adeca/liheap/eligibility/fpg_rate.yaml b/policyengine_us/parameters/gov/states/al/adeca/liheap/eligibility/fpg_rate.yaml index 2f2d2065135..3ece83e0352 100644 --- a/policyengine_us/parameters/gov/states/al/adeca/liheap/eligibility/fpg_rate.yaml +++ b/policyengine_us/parameters/gov/states/al/adeca/liheap/eligibility/fpg_rate.yaml @@ -6,5 +6,5 @@ metadata: period: year label: Alabama LIHEAP federal poverty guideline rate reference: - - title: Alabama FY2026 LIHEAP State Plan, section 2.1 + - title: "Alabama FY2026 LIHEAP State Plan, sections 2.1 and 3.1 #page 8,11" href: https://liheapch.acf.gov/docs/2026/state-plans/AL_Plan_2026.pdf#page=8 diff --git a/policyengine_us/parameters/gov/states/al/adeca/liheap/payment/additional_person_amount.yaml b/policyengine_us/parameters/gov/states/al/adeca/liheap/payment/additional_person_amount.yaml index fb7ddf01173..6e9fd5edfe0 100644 --- a/policyengine_us/parameters/gov/states/al/adeca/liheap/payment/additional_person_amount.yaml +++ b/policyengine_us/parameters/gov/states/al/adeca/liheap/payment/additional_person_amount.yaml @@ -4,7 +4,7 @@ values: metadata: unit: currency-USD period: year - label: Alabama LIHEAP additional person heating payment + label: Alabama LIHEAP additional person payment reference: - title: Alabama FY2026 LIHEAP Policy Manual, Appendix D (current benefit chart) href: https://adeca.alabama.gov/wp-content/uploads/FY-2026-LIHEAP-Manual-1.pdf#page=57 diff --git a/policyengine_us/parameters/gov/states/al/adeca/liheap/payment/base_amount.yaml b/policyengine_us/parameters/gov/states/al/adeca/liheap/payment/base_amount.yaml index b71aa51c967..40eab3f14c1 100644 --- a/policyengine_us/parameters/gov/states/al/adeca/liheap/payment/base_amount.yaml +++ b/policyengine_us/parameters/gov/states/al/adeca/liheap/payment/base_amount.yaml @@ -1,4 +1,4 @@ -description: Alabama provides this base payment for a one-person household in the lowest income band, by heating fuel, under the Low Income Home Energy Assistance Program. +description: Alabama provides this base payment for a one-person household in the lowest income band, by fuel type, under the Low Income Home Energy Assistance Program. propane: 2025-10-01: 500 natural_gas: @@ -10,7 +10,7 @@ wood_coal_kerosene: metadata: unit: currency-USD period: year - label: Alabama LIHEAP base heating payment by fuel + label: Alabama LIHEAP base payment by fuel propagate_metadata_to_children: true reference: - title: Alabama FY2026 LIHEAP Policy Manual, Appendix D (current benefit chart) diff --git a/policyengine_us/parameters/gov/states/al/adeca/liheap/payment/iija_supplement.yaml b/policyengine_us/parameters/gov/states/al/adeca/liheap/payment/iija_supplement.yaml index e64582a64a3..cf20564b110 100644 --- a/policyengine_us/parameters/gov/states/al/adeca/liheap/payment/iija_supplement.yaml +++ b/policyengine_us/parameters/gov/states/al/adeca/liheap/payment/iija_supplement.yaml @@ -1,24 +1,25 @@ -description: Alabama provides this amount as an Infrastructure Investment and Jobs Act supplement with each regular heating assistance award under the Low Income Home Energy Assistance Program. +description: Alabama provides this amount as an Infrastructure Investment and Jobs Act supplement with each regular heating or cooling assistance award under the Low Income Home Energy Assistance Program. # Award 2601ALLIEI funds the FY2026 supplement: $1,248,405 (November 26, 2025) plus # $111,300 (August 13, 2026), or $1,359,705, roughly 13,600 supplements across heating # and cooling. The plan pays it "if funding is available"; the model assumes the award # does not run out, so population totals overstate funded supplements. values: 2025-10-01: 100 - # The draft FY2027 plan (section 2.5) omits the supplement; IIJA's last tranche was - # FY2026, although award 2601ALLIEI runs to September 30, 2027. Recheck against the - # final FY2027 plan, which was not published as of October 19, 2026. + # The draft and accepted FY2027 plans (sections 2.5 and 3.5) omit the supplement; + # IIJA's last tranche was FY2026, although award 2601ALLIEI runs to September 30, 2027. 2026-10-01: 0 metadata: unit: currency-USD period: year - label: Alabama LIHEAP IIJA heating supplement + label: Alabama LIHEAP IIJA supplement reference: - - title: Alabama FY2026 LIHEAP State Plan, section 2.5 + - title: "Alabama FY2026 LIHEAP State Plan, sections 2.5 and 3.5 #page 10,12" href: https://liheapch.acf.gov/docs/2026/state-plans/AL_Plan_2026.pdf#page=10 - title: HHS TAGGS award 2601ALLIEI, Alabama LIHEAP Infrastructure Supplement (FY2026 funding) href: https://taggs.hhs.gov/Detail/AwardDetail?arg_AwardNum=2601ALLIEI&arg_ProgOfficeCode=31 - - title: "Alabama draft FY2027 LIHEAP State Plan, section 2.5 (no IIJA supplement) #page 9-10" + - title: "Alabama draft FY2027 LIHEAP State Plan, sections 2.5 and 3.5 (no IIJA supplement) #page 9-10,12" href: https://adeca.alabama.gov/wp-content/uploads/FY27-LIHEAP-State-Plan.pdf#page=9 + - title: "Alabama FY2027 LIHEAP State Plan, sections 2.5 and 3.5 (no IIJA supplement) #page 9-10,12" + href: https://liheapch.acf.gov/docs/2027/state-plans/AL_Plan_2027.pdf#page=9 - title: Infrastructure Investment and Jobs Act, Pub. L. 117-58, Division J, Title VII, 135 Stat. 1412 href: https://www.govinfo.gov/content/pkg/PLAW-117publ58/pdf/PLAW-117publ58.pdf#page=984 diff --git a/policyengine_us/parameters/gov/states/al/adeca/liheap/payment/income_band_reduction.yaml b/policyengine_us/parameters/gov/states/al/adeca/liheap/payment/income_band_reduction.yaml index bf665f985c8..bb1f6d5552a 100644 --- a/policyengine_us/parameters/gov/states/al/adeca/liheap/payment/income_band_reduction.yaml +++ b/policyengine_us/parameters/gov/states/al/adeca/liheap/payment/income_band_reduction.yaml @@ -1,4 +1,4 @@ -description: Alabama deducts this amount from the lowest-band heating payment for each income band under the Low Income Home Energy Assistance Program. +description: Alabama deducts this amount from the lowest-band payment for each income band under the Low Income Home Energy Assistance Program. # These differences reproduce every fuel and household-size cell in Appendix D. 1: 2025-10-01: 0 @@ -9,7 +9,7 @@ description: Alabama deducts this amount from the lowest-band heating payment fo metadata: unit: currency-USD period: year - label: Alabama LIHEAP heating payment reduction by income band + label: Alabama LIHEAP payment reduction by income band breakdown: - range(1, 4) propagate_metadata_to_children: true diff --git a/policyengine_us/parameters/gov/states/al/adeca/liheap/payment/maximum_payment_household_size.yaml b/policyengine_us/parameters/gov/states/al/adeca/liheap/payment/maximum_payment_household_size.yaml index b02c1ed57da..3ff1c484a4c 100644 --- a/policyengine_us/parameters/gov/states/al/adeca/liheap/payment/maximum_payment_household_size.yaml +++ b/policyengine_us/parameters/gov/states/al/adeca/liheap/payment/maximum_payment_household_size.yaml @@ -1,4 +1,4 @@ -description: Alabama uses no more than this many household members to determine the regular heating payment under the Low Income Home Energy Assistance Program. +description: Alabama uses no more than this many household members to determine the regular heating or cooling payment under the Low Income Home Energy Assistance Program. values: 2025-10-01: 4 metadata: @@ -6,5 +6,5 @@ metadata: period: year label: Alabama LIHEAP maximum payment household size reference: - - title: Alabama FY2026 LIHEAP State Plan, section 2.5 (households over four receive the four-person payment) + - title: "Alabama FY2026 LIHEAP State Plan, sections 2.5 and 3.5 (households over four receive the four-person payment) #page 9,12" href: https://liheapch.acf.gov/docs/2026/state-plans/AL_Plan_2026.pdf#page=9 diff --git a/policyengine_us/programs.yaml b/policyengine_us/programs.yaml index a7cef450f8b..c76a3942a4e 100644 --- a/policyengine_us/programs.yaml +++ b/policyengine_us/programs.yaml @@ -996,7 +996,15 @@ programs: variable: al_liheap parameter_prefix: gov.states.al.adeca.liheap verified_years: "2026" - notes: FY2026 regular heating with the HUD allowance deduction and the funded IIJA supplement. Crisis, cooling, weatherization and equipment assistance are out of scope. + notes: FY2026 regular heating with the HUD allowance deduction and the funded IIJA supplement. Regular Cooling is a separate entry; crisis, weatherization and equipment assistance are out of scope. + - state: AL + status: partial + name: Alabama LIHEAP cooling + full_name: Alabama Low Income Home Energy Assistance Program Regular Cooling + variable: al_liheap_cooling + parameter_prefix: gov.states.al.adeca.liheap + verified_years: "2026" + notes: FY2026 Regular Cooling from the Electric column of the May 15, 2026 chart, less the HUD allowance, plus the funded IIJA supplement. Crisis cooling, fans, air conditioners and the discretionary $50 high-need addition are out of scope. - state: AK status: partial name: Alaska HAP diff --git a/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling.yaml b/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling.yaml new file mode 100644 index 00000000000..e8981f00b9e --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling.yaml @@ -0,0 +1,190 @@ +# Source: ADECA FY2026 LIHEAP Policy Manual (pages 9, 21-23), FY2026 plan sections 3.5-3.6 +# (pages 12-13), and the PY 2026 Payment Assistance Chart effective May 15, 2026 (draft FY2027 +# plan, page 53). Regular Cooling pays the chart's Electric column, minus one month of HUD +# utility allowance, plus the $100 FY2026 IIJA supplement when the regular payment is positive. +# 2026 monthly band tops (2026 guidelines): size 1 $665 / $1,331 / $1,995; size 4 $1,375 / +# $2,751 / $4,125; size 5 $1,612 / $3,225 / $4,835. +# Electric column: size 1 $440 / $410 / $320; size 4 and up $470 / $440 / $350. + +# Part-time minimum wage: 20 hours x $7.25 x 52 weeks = $7,540 a year, $628.33 a month, +# band 1. Cooling: Electric $440 + $100 = $540. The gas heating column would pay $480. +- name: Case 1, a gas-heated worker receives the Electric column cooling payment. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: AL + age: 45 + employment_income_before_lsr: 7_540 + heating_type: NATURAL_GAS + gas_expense: 900 + has_cooling_expense: true + hud_utility_allowance: 0 + output: + al_liheap_base_payment: 480 + al_liheap_cooling: 540 + +# Retiree with $1,100 a month of Social Security ($13,200 a year), band 2 ($666 to $1,331). +# SSI is zero: $1,100 - $20 = $1,080 exceeds the $994 federal benefit rate. +# Cooling: $440 - $30 = $410 + $100 = $510. +- name: Case 2, a one-person band 2 household receives $410 plus the supplement. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: AL + age: 70 + social_security_retirement: 13_200 + heating_type: ELECTRICITY + has_cooling_expense: true + hud_utility_allowance: 0 + output: + al_liheap_cooling: 510 + +# One earner at about $17.31 an hour full time: $36,000 a year, $3,000 a month, size 4 band 3 +# ($2,752 to $4,125). Cooling: $440 - $120 + 3 x $10 = $350 + $100 = $450. +- name: Case 3, a four-person band 3 household receives $350 plus the supplement. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + employment_income_before_lsr: 36_000 + person2: + age: 33 + person3: + age: 8 + person4: + age: 5 + spm_units: + spm_unit: + members: [person1, person2, person3, person4] + heating_type: ELECTRICITY + has_cooling_expense: true + households: + household: + members: [person1, person2, person3, person4] + state_code: AL + hud_utility_allowance: 0 + output: + al_liheap_cooling: 450 + +# Five people with $30,000 of wages: $2,500 a month. Size 5 band tops are +# floor(4,835 / 3 + 0.5) = $1,612 and 2 x 1,612 + 1 = $3,225, so band 2. The payment uses +# the four-person column: $440 - $30 + 3 x $10 = $440 + $100 = $540. +- name: Case 4, a five-person household receives the four-person payment. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 38 + employment_income_before_lsr: 30_000 + person2: + age: 36 + person3: + age: 12 + person4: + age: 9 + person5: + age: 4 + spm_units: + spm_unit: + members: [person1, person2, person3, person4, person5] + heating_type: ELECTRICITY + has_cooling_expense: true + households: + household: + members: [person1, person2, person3, person4, person5] + state_code: AL + hud_utility_allowance: 0 + output: + al_liheap_cooling: 540 + +# Case 1's worker with HUD utility allowances of $50 and $440 a month ($600 and $5,280 a year). +# $440 - $50 = $390 + $100 = $490. $440 - $440 = $0, so no supplement is added. +- name: Case 5, the monthly HUD utility allowance reduces the cooling payment. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [AL, AL] + age: [45, 45] + employment_income_before_lsr: [7_540, 7_540] + heating_type: [ELECTRICITY, ELECTRICITY] + has_cooling_expense: [true, true] + hud_utility_allowance: [600, 5_280] + output: + al_liheap_cooling: [490, 0] + +# $15 an hour full time: $31,200 a year, $2,600 a month, above the $1,995 one-person limit. +- name: Case 6, income above the cooling limit pays nothing. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: AL + age: 30 + employment_income_before_lsr: 31_200 + heating_type: ELECTRICITY + has_cooling_expense: true + hud_utility_allowance: 0 + output: + al_liheap_cooling: 0 + +- name: Case 7, a household without a cooling cost receives no cooling payment. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: AL + age: 45 + employment_income_before_lsr: 7_540 + heating_type: ELECTRICITY + has_cooling_expense: false + hud_utility_allowance: 0 + output: + al_liheap_cooling: 0 + +# FY2027 carries the chart amounts forward and ends the IIJA supplement on October 1, 2026. +# $628.33 a month stays in band 1 under any 2027 guideline at least as high as 2026's $15,960 +# (band 1 top of $665 or more). Cooling: $440. +- name: Case 8, the summer 2027 payment has no IIJA supplement. + period: 2027 + absolute_error_margin: 0.01 + input: + state_code: AL + age: 45 + employment_income_before_lsr: 7_540 + heating_type: ELECTRICITY + has_cooling_expense: true + hud_utility_allowance: 0 + output: + al_liheap_cooling: 440 + +# Each income source is floored at zero, so a $20,000 business loss leaves countable income +# at $0, band 1: $440 + $100 = $540, not more. +- name: Case 9, a business loss does not raise the payment above the band 1 amount. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: AL + age: 45 + self_employment_income: -20_000 + heating_type: ELECTRICITY + has_cooling_expense: true + hud_utility_allowance: 0 + output: + al_liheap_cooling: 540 + +# Case 1's worker with the heating fuel left unspecified. The standing LIHEAP rule pays +# nothing when heating_type is UNSPECIFIED. +- name: Case 10, an unspecified heating fuel pays no cooling. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: AL + age: 45 + employment_income_before_lsr: 7_540 + heating_type: UNSPECIFIED + has_cooling_expense: true + hud_utility_allowance: 0 + output: + al_liheap_cooling_eligible: false + al_liheap_cooling: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_eligible.yaml new file mode 100644 index 00000000000..e889551bc46 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_eligible.yaml @@ -0,0 +1,73 @@ +# Source: ADECA FY2026 LIHEAP Policy Manual (pages 9, 16-17, 21), FY2026 plan sections 3.1-3.2, +# and the PY 2026 Payment Assistance Chart effective May 15, 2026 (2026 guidelines). + +# One person: the cooling limit is 15,960 x 1.5 / 12 = $1,995 a month, or $23,940 a year. +# $23,940 of wages is exactly $1,995 a month, so the limit is inclusive. +- name: Case 1, income exactly at the cooling limit is eligible. + period: 2026 + input: + state_code: AL + age: 40 + employment_income_before_lsr: 23_940 + heating_type: ELECTRICITY + has_cooling_expense: true + output: + al_liheap_cooling_income_limit: 23_940 + al_liheap_cooling_eligible: true + +# $23,940.12 a year is $1,995.01 a month, one cent over the limit. +- name: Case 2, one cent a month above the cooling limit is ineligible. + period: 2026 + input: + state_code: AL + age: 40 + employment_income_before_lsr: 23_940.12 + heating_type: ELECTRICITY + has_cooling_expense: true + output: + al_liheap_cooling_eligible: false + +# $23,700 of wages is $1,975 a month: above the heating limit of 15,650 x 1.5 / 12 = $1,956 +# (2025 guidelines) but within the cooling limit of $1,995 (2026 guidelines). +- name: Case 3, cooling uses the current year guidelines while heating uses the prior year. + period: 2026 + input: + state_code: AL + age: 40 + employment_income_before_lsr: 23_700 + heating_type: NATURAL_GAS + gas_expense: 900 + has_cooling_expense: true + output: + al_liheap_income_limit: 23_472 + al_liheap_cooling_income_limit: 23_940 + al_liheap_eligible: false + al_liheap_cooling_eligible: true + +# Part-time minimum wage: 20 hours x $7.25 x 52 weeks = $7,540 a year ($628.33 a month). +# Regular Cooling has no heating-fuel requirement, so a home without heating and a gas-heated +# home are both eligible. +- name: Case 4, a home without heating and a gas-heated home are both cooling eligible. + period: 2026 + input: + state_code: [AL, AL] + age: [45, 45] + employment_income_before_lsr: [7_540, 7_540] + heating_type: [NONE, NATURAL_GAS] + has_cooling_expense: [true, true] + output: + al_liheap_cooling_eligible: [true, true] + +# The same worker heats with gas and pays a $900 gas bill but has no cooling cost. +- name: Case 5, a heating-eligible household without a cooling cost is not cooling eligible. + period: 2026 + input: + state_code: AL + age: 45 + employment_income_before_lsr: 7_540 + heating_type: NATURAL_GAS + gas_expense: 900 + has_cooling_expense: false + output: + al_liheap_eligible: true + al_liheap_cooling_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_income_limit.yaml b/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_income_limit.yaml new file mode 100644 index 00000000000..0bfdd4325f7 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_income_limit.yaml @@ -0,0 +1,16 @@ +# Source: ADECA PY 2026 Payment Assistance Chart effective May 15, 2026 (draft FY2027 plan, page 53), +# built on the 2026 guidelines of $15,960 plus $5,680 per additional person. + +# 150% a month: size 1 = 15,960 x 1.5 / 12 = 1,995; size 2 = 21,640 x 1.5 / 12 = 2,705; +# size 3 = 3,415; size 4 = 4,125; size 5 = 4,835; size 15 = 95,480 x 1.5 / 12 = 11,935. +# Each printed monthly limit times 12. +- name: Case 1, summer 2026 cooling limits follow the May 15, 2026 chart. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [AL, AL, AL, AL, AL, AL] + heating_type: [ELECTRICITY, ELECTRICITY, ELECTRICITY, ELECTRICITY, ELECTRICITY, ELECTRICITY] + has_cooling_expense: [true, true, true, true, true, true] + al_liheap_household_size: [1, 2, 3, 4, 5, 15] + output: + al_liheap_cooling_income_limit: [23_940, 32_460, 40_980, 49_500, 58_020, 143_220] diff --git a/policyengine_us/variables/gov/states/al/adeca/liheap/al_liheap.py b/policyengine_us/variables/gov/states/al/adeca/liheap/al_liheap.py index e6bc85a0fd1..fd36c336d6b 100644 --- a/policyengine_us/variables/gov/states/al/adeca/liheap/al_liheap.py +++ b/policyengine_us/variables/gov/states/al/adeca/liheap/al_liheap.py @@ -1,6 +1,22 @@ from policyengine_us.model_api import * +def al_liheap_award(spm_unit, period, parameters, chart_amount): + p = parameters(period).gov.states.al.adeca.liheap.payment + # The monthly HUD allowance reduces the award, not countable income. + # AL local allowance schedules are not encoded in the shared input; + # an observed annual allowance can be supplied directly. + allowance = spm_unit.household("hud_utility_allowance", period) / MONTHS_IN_YEAR + regular_payment = max_(chart_amount - allowance, 0) + # The plan pays the IIJA supplement "if funding is available"; the model + # assumes the FY2026 award does not run out (see the supplement parameter). + # Interpret the plan's "receives a Heating benefit" (or Cooling benefit) + # as requiring a positive regular payment after the HUD adjustment. The + # plan does not expressly address a regular payment reduced to zero. + supplement = where(regular_payment > 0, p.iija_supplement, 0) + return regular_payment + supplement + + class al_liheap(Variable): value_type = float entity = SPMUnit @@ -15,22 +31,10 @@ class al_liheap(Variable): ) def formula(spm_unit, period, parameters): - p = parameters(period).gov.states.al.adeca.liheap.payment eligible = spm_unit("al_liheap_eligible", period) base = spm_unit("al_liheap_base_payment", period) - # The monthly HUD allowance reduces the award, not countable income. - # AL local allowance schedules are not encoded in the shared input; - # an observed annual allowance can be supplied directly. - allowance = spm_unit.household("hud_utility_allowance", period) / MONTHS_IN_YEAR - regular_payment = max_(base - allowance, 0) - # The plan pays the IIJA supplement "if funding is available"; the model - # assumes the FY2026 award does not run out (see the supplement parameter). - # Interpret the plan's "receives a Heating benefit" as requiring a - # positive regular payment after the HUD adjustment. The plan does - # not expressly address a regular payment reduced to zero. - supplement = where(regular_payment > 0, p.iija_supplement, 0) # The separate $50 high-need addition requires an agency decision # unavailable in existing inputs, so demographic traits alone do not # add it. Fixed regular payments have no cap at the reported heating # expense. - return eligible * (regular_payment + supplement) + return eligible * al_liheap_award(spm_unit, period, parameters, base) diff --git a/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling.py b/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling.py new file mode 100644 index 00000000000..cd3d84942d8 --- /dev/null +++ b/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling.py @@ -0,0 +1,45 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.al.adeca.liheap.al_liheap import ( + al_liheap_award, +) +from policyengine_us.variables.gov.states.al.adeca.liheap.payment.al_liheap_base_payment import ( + al_liheap_chart_amount, +) +from policyengine_us.variables.gov.states.al.adeca.liheap.payment.al_liheap_income_band import ( + al_liheap_income_band_for_limit, +) + + +class al_liheap_cooling(Variable): + value_type = float + entity = SPMUnit + definition_period = YEAR + unit = USD + label = "Alabama LIHEAP regular cooling assistance" + documentation = ( + "Verified for FY2026 (the May-September 2026 cooling season). Earlier " + "years use model parameter backfilling and are unverified historical " + "estimates. Later years carry the FY2026 chart amounts forward." + ) + defined_for = "al_liheap_cooling_eligible" + reference = ( + # PDF pages 9, 21-23 + "https://adeca.alabama.gov/wp-content/uploads/FY-2026-LIHEAP-Manual-1.pdf#page=9", + # PDF pages 12-13 + "https://liheapch.acf.gov/docs/2026/state-plans/AL_Plan_2026.pdf#page=12", + "https://adeca.alabama.gov/wp-content/uploads/FY27-LIHEAP-State-Plan.pdf#page=53", + ) + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.al.adeca.liheap.payment + limit = spm_unit("al_liheap_cooling_income_limit", period) + band = al_liheap_income_band_for_limit(spm_unit, period, limit) + # Regular Cooling pays the chart's Electric column, whose $320 to + # $470 range plus the $50 high-need addition is plan section 3.6's + # $320 to $520 cooling benefit range. One payment per cooling season. + chart_amount = al_liheap_chart_amount( + spm_unit, period, parameters, p.base_amount.electricity, band + ) + # The discretionary $50 high-need addition (manual 6.1.6) requires an + # agency decision unavailable in existing inputs, as for heating. + return al_liheap_award(spm_unit, period, parameters, chart_amount) diff --git a/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_eligible.py b/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_eligible.py new file mode 100644 index 00000000000..f16de32f4f8 --- /dev/null +++ b/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_eligible.py @@ -0,0 +1,30 @@ +from policyengine_us.model_api import * + + +class al_liheap_cooling_eligible(Variable): + value_type = bool + entity = SPMUnit + definition_period = YEAR + label = "Alabama LIHEAP regular cooling assistance eligibility" + defined_for = StateCode.AL + reference = ( + # PDF pages 9, 16-17, 21 + "https://adeca.alabama.gov/wp-content/uploads/FY-2026-LIHEAP-Manual-1.pdf#page=9", + "https://liheapch.acf.gov/docs/2026/state-plans/AL_Plan_2026.pdf#page=11", + ) + + def formula(spm_unit, period, parameters): + size = spm_unit("al_liheap_household_size", period) + income = spm_unit("al_liheap_countable_income", period) + limit = spm_unit("al_liheap_cooling_income_limit", period) + # The cooling cost input stands for the recent cooling bill (manual + # 5.3) and, for subsidized tenants, the out-of-pocket energy cost that + # manual 6.1.1 requires. Cooling costs paid only through nonsubsidized + # rent have no existing input and are not paid. + has_cooling_expense = spm_unit("has_cooling_expense", period) + # The heating fuel must be reported: UNSPECIFIED pays nothing, while + # NONE and every known fuel qualify, since cooling pays the Electric + # column whatever the heating fuel. + heating_type = spm_unit("heating_type", period) + fuel_reported = heating_type != heating_type.possible_values.UNSPECIFIED + return (size > 0) & (income <= limit) & has_cooling_expense & fuel_reported diff --git a/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_income_limit.py b/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_income_limit.py new file mode 100644 index 00000000000..33ea8978b01 --- /dev/null +++ b/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_income_limit.py @@ -0,0 +1,24 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.al.adeca.liheap.eligibility.al_liheap_income_limit import ( + al_liheap_income_limit_for_lag, +) + + +class al_liheap_cooling_income_limit(Variable): + value_type = float + entity = SPMUnit + definition_period = YEAR + unit = USD + label = "Alabama LIHEAP regular cooling annualized income limit" + defined_for = StateCode.AL + reference = ( + "https://adeca.alabama.gov/wp-content/uploads/FY27-LIHEAP-State-Plan.pdf#page=53", + # PDF pages 11-12 + "https://liheapch.acf.gov/docs/2026/state-plans/AL_Plan_2026.pdf#page=11", + ) + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.al.adeca.liheap.cooling + return al_liheap_income_limit_for_lag( + spm_unit, period, parameters, p.fpg_year_lag + ) diff --git a/policyengine_us/variables/gov/states/al/adeca/liheap/eligibility/al_liheap_income_limit.py b/policyengine_us/variables/gov/states/al/adeca/liheap/eligibility/al_liheap_income_limit.py index 344ec073c87..c6e63cb4eff 100644 --- a/policyengine_us/variables/gov/states/al/adeca/liheap/eligibility/al_liheap_income_limit.py +++ b/policyengine_us/variables/gov/states/al/adeca/liheap/eligibility/al_liheap_income_limit.py @@ -2,6 +2,22 @@ from policyengine_us.variables.gov.hhs.tax_unit_fpg import fpg +def al_liheap_income_limit_for_lag(spm_unit, period, parameters, year_lag): + p = parameters(period).gov.states.al.adeca.liheap.eligibility + size = spm_unit("al_liheap_household_size", period) + state_group = spm_unit.household("state_group_str", period) + guideline = fpg( + max_(size, 1), + state_group, + period, + parameters, + year_lag=year_lag, + ) + monthly_limit = guideline * p.fpg_rate / MONTHS_IN_YEAR + # Nearest-dollar, half-up rounding reproduces all 15 printed limits. + return np.floor(monthly_limit + 0.5) * MONTHS_IN_YEAR + + class al_liheap_income_limit(Variable): value_type = float entity = SPMUnit @@ -16,16 +32,7 @@ class al_liheap_income_limit(Variable): def formula(spm_unit, period, parameters): p = parameters(period).gov.states.al.adeca.liheap.eligibility - size = spm_unit("al_liheap_household_size", period) - state_group = spm_unit.household("state_group_str", period) - guideline = fpg( - max_(size, 1), - state_group, - period, - parameters, - year_lag=p.fpg_year_lag, - ) - monthly_limit = guideline * p.fpg_rate / MONTHS_IN_YEAR - # Nearest-dollar, half-up rounding reproduces all 15 printed limits. # The FY2026 heating season ends before the May 2026 chart update. - return np.floor(monthly_limit + 0.5) * MONTHS_IN_YEAR + return al_liheap_income_limit_for_lag( + spm_unit, period, parameters, p.fpg_year_lag + ) diff --git a/policyengine_us/variables/gov/states/al/adeca/liheap/payment/al_liheap_base_payment.py b/policyengine_us/variables/gov/states/al/adeca/liheap/payment/al_liheap_base_payment.py index 00b210f3705..2a171bb35b4 100644 --- a/policyengine_us/variables/gov/states/al/adeca/liheap/payment/al_liheap_base_payment.py +++ b/policyengine_us/variables/gov/states/al/adeca/liheap/payment/al_liheap_base_payment.py @@ -1,6 +1,21 @@ from policyengine_us.model_api import * +def al_liheap_chart_amount(spm_unit, period, parameters, base, band): + p = parameters(period).gov.states.al.adeca.liheap.payment + size = spm_unit("al_liheap_household_size", period) + additional_people = clip(size - 1, 0, p.maximum_payment_household_size - 1) + # Cross-state rows have band zero before defined_for masks the result; + # raise it to band 1 so the eager lookup stays within the breakdown. + lookup_band = max_(band, 1) + amount = ( + base + - p.income_band_reduction[lookup_band] + + additional_people * p.additional_person_amount + ) + return where(base > 0, amount, 0) + + class al_liheap_base_payment(Variable): value_type = float entity = SPMUnit @@ -31,16 +46,7 @@ def formula(spm_unit, period, parameters): ], default=0, ) - size = spm_unit("al_liheap_household_size", period) - additional_people = clip(size - 1, 0, p.maximum_payment_household_size - 1) - # Cross-state rows have band zero before defined_for masks the result; - # raise it to band 1 so the eager lookup stays within the breakdown. - band = max_(spm_unit("al_liheap_income_band", period), 1) - amount = ( - base - - p.income_band_reduction[band] - + additional_people * p.additional_person_amount - ) + band = spm_unit("al_liheap_income_band", period) # This compact representation reproduces Appendix D's entire matrix. # The manual's worked example on page 22 uses an older payment amount. # No published category was identified for fuel oil, solar, other, or @@ -48,4 +54,4 @@ def formula(spm_unit, period, parameters): # zero leaves those estimates incomplete; it does not establish legal # ineligibility, including heat in rent. Issue 9754 tracks the # unspecified-fuel default. - return where(base > 0, amount, 0) + return al_liheap_chart_amount(spm_unit, period, parameters, base, band) diff --git a/policyengine_us/variables/gov/states/al/adeca/liheap/payment/al_liheap_income_band.py b/policyengine_us/variables/gov/states/al/adeca/liheap/payment/al_liheap_income_band.py index 184c0a34af6..60ffd3ec7fc 100644 --- a/policyengine_us/variables/gov/states/al/adeca/liheap/payment/al_liheap_income_band.py +++ b/policyengine_us/variables/gov/states/al/adeca/liheap/payment/al_liheap_income_band.py @@ -1,6 +1,20 @@ from policyengine_us.model_api import * +def al_liheap_income_band_for_limit(spm_unit, period, annual_limit): + monthly_limit = annual_limit / MONTHS_IN_YEAR + monthly_income = spm_unit("al_liheap_countable_income", period) / MONTHS_IN_YEAR + first_upper = np.floor(monthly_limit / 3 + 0.5) + second_upper = 2 * first_upper + 1 + # Published upper bounds are inclusive. Fractional dollars immediately + # above an upper bound enter the next band without a one-dollar gap. + return select( + [monthly_income <= first_upper, monthly_income <= second_upper], + [1, 2], + default=3, + ) + + class al_liheap_income_band(Variable): value_type = int entity = SPMUnit @@ -10,14 +24,5 @@ class al_liheap_income_band(Variable): reference = "https://liheapch.acf.gov/docs/2026/state-plans/AL_Plan_2026.pdf#page=9" def formula(spm_unit, period, parameters): - monthly_limit = spm_unit("al_liheap_income_limit", period) / MONTHS_IN_YEAR - monthly_income = spm_unit("al_liheap_countable_income", period) / MONTHS_IN_YEAR - first_upper = np.floor(monthly_limit / 3 + 0.5) - second_upper = 2 * first_upper + 1 - # Published upper bounds are inclusive. Fractional dollars immediately - # above an upper bound enter the next band without a one-dollar gap. - return select( - [monthly_income <= first_upper, monthly_income <= second_upper], - [1, 2], - default=3, - ) + limit = spm_unit("al_liheap_income_limit", period) + return al_liheap_income_band_for_limit(spm_unit, period, limit) From 5c95fd9830d51c273a9e430f73192e326a25620d Mon Sep 17 00:00:00 2001 From: Ziming Date: Sun, 8 Nov 2026 22:06:01 -0500 Subject: [PATCH 03/11] Count Alabama LIHEAP cooling in the national LIHEAP total Co-Authored-By: Claude Opus 5.5 --- policyengine_us/parameters/gov/hhs/liheap/regular_payments.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/policyengine_us/parameters/gov/hhs/liheap/regular_payments.yaml b/policyengine_us/parameters/gov/hhs/liheap/regular_payments.yaml index e811bea6fbd..9db68e546b1 100644 --- a/policyengine_us/parameters/gov/hhs/liheap/regular_payments.yaml +++ b/policyengine_us/parameters/gov/hhs/liheap/regular_payments.yaml @@ -3,6 +3,7 @@ values: 2023-01-01: - ak_hap # Alaska Heating Assistance Program - al_liheap # Alabama LIHEAP + - al_liheap_cooling # Alabama LIHEAP cooling - ar_liheap # Arkansas LIHEAP - az_liheap # Arizona LIHEAP # California's three components are exclusive by eligibility, so their sum is its single regular payment. From 77e0f848bb2733470224377d6c037d82cf8aaf73 Mon Sep 17 00:00:00 2001 From: Ziming Date: Mon, 9 Nov 2026 20:34:36 -0500 Subject: [PATCH 04/11] Guard the LIHEAP payment list and reword the Rhode Island cooling note Add regular_payments.yaml to the aggregate-list integrity test, which checks that each member exists, is state-gated and is not dropped between year blocks. Say that Rhode Island plans no regular cooling payment, since its FY2027 plan still allows a leftover-funds cooling grant. Co-Authored-By: Claude Opus 5.5 --- policyengine_us/programs.yaml | 2 +- policyengine_us/tests/test_aggregate_list_integrity.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/policyengine_us/programs.yaml b/policyengine_us/programs.yaml index 26816f1b9b9..3147c473bc1 100644 --- a/policyengine_us/programs.yaml +++ b/policyengine_us/programs.yaml @@ -1090,7 +1090,7 @@ programs: variable: ri_liheap parameter_prefix: gov.states.ri.dhs.liheap verified_years: "2026" - notes: FY2026 regular heating, including heat-in-rent grants and the $350 utility enhancement, assuming a qualifying utility account. Earlier years and FY2027 payments reuse FY2026 amounts as unverified estimates; Rhode Island offers no cooling assistance in FY2026 or FY2027, and crisis and weatherization are out of scope. + notes: FY2026 regular heating, including heat-in-rent grants and the $350 utility enhancement, assuming a qualifying utility account. Earlier years and FY2027 payments reuse FY2026 amounts as unverified estimates; Rhode Island plans no regular cooling payment in FY2026 or FY2027, and crisis and weatherization are out of scope. - state: DC status: complete name: DC LIHEAP diff --git a/policyengine_us/tests/test_aggregate_list_integrity.py b/policyengine_us/tests/test_aggregate_list_integrity.py index e76e164a2f0..917a93e7f60 100644 --- a/policyengine_us/tests/test_aggregate_list_integrity.py +++ b/policyengine_us/tests/test_aggregate_list_integrity.py @@ -36,6 +36,7 @@ "gov/states/household/state_eitcs.yaml", "gov/states/household/state_cdccs.yaml", "gov/hhs/ccdf/child_care_subsidy_programs.yaml", + "gov/hhs/liheap/regular_payments.yaml", "gov/household/household_state_benefits.yaml", ] From 59119070b5a2ec7f15c2bcf73fac354e88b9bf5b Mon Sep 17 00:00:00 2001 From: Ziming Date: Mon, 9 Nov 2026 23:06:32 -0500 Subject: [PATCH 05/11] Verify Alabama LIHEAP cooling for FY2025 and pay in-rent renters Adds the FY2025 plan, the February 2025 benefit chart and the FY2027 plan as references, a FY2025 case, and an in-rent renter case. Manual section 6.1.3 pays unsubsidized renters with utilities in rent. Marks 2025-2026 verified and checks the national liheap total. Co-Authored-By: Claude Opus 5.5 --- .../al/adeca/liheap/cooling/fpg_year_lag.yaml | 4 +-- policyengine_us/programs.yaml | 4 +-- .../liheap/cooling/al_liheap_cooling.yaml | 25 ++++++++++++++++++- .../cooling/al_liheap_cooling_eligible.yaml | 23 +++++++++++++++++ .../adeca/liheap/cooling/al_liheap_cooling.py | 15 ++++++++--- .../cooling/al_liheap_cooling_eligible.py | 19 ++++++++++---- .../cooling/al_liheap_cooling_income_limit.py | 4 +++ 7 files changed, 81 insertions(+), 13 deletions(-) diff --git a/policyengine_us/parameters/gov/states/al/adeca/liheap/cooling/fpg_year_lag.yaml b/policyengine_us/parameters/gov/states/al/adeca/liheap/cooling/fpg_year_lag.yaml index 25d2e4fba41..bcdb376276b 100644 --- a/policyengine_us/parameters/gov/states/al/adeca/liheap/cooling/fpg_year_lag.yaml +++ b/policyengine_us/parameters/gov/states/al/adeca/liheap/cooling/fpg_year_lag.yaml @@ -11,7 +11,7 @@ metadata: reference: - title: "Alabama FY2026 LIHEAP State Plan, sections 1.1 and 3.5 (May-September cooling season; adopt the 2026 guidelines upon release) #page 4,12" href: https://liheapch.acf.gov/docs/2026/state-plans/AL_Plan_2026.pdf#page=4 - - title: Alabama draft FY2027 LIHEAP State Plan, Attachment A (PY 2026 Payment Assistance Chart effective May 15, 2026) + - title: Alabama draft FY2027 LIHEAP State Plan, Attachment A (PY 2026 Payment Assistance Chart effective May 15, 2026; removed from the ADECA site by November 2026) href: https://adeca.alabama.gov/wp-content/uploads/FY27-LIHEAP-State-Plan.pdf#page=53 - - title: Alabama FY2027 LIHEAP State Plan, section 3.5 (adopt the 2027 guidelines upon release) + - title: Alabama FY2027 LIHEAP State Plan, section 3.5 (the 2026 guidelines set the $1,995 one-person limit and $665, $1,331 and $1,995 band tops; adopt the 2027 guidelines upon release) href: https://liheapch.acf.gov/docs/2027/state-plans/AL_Plan_2027.pdf#page=12 diff --git a/policyengine_us/programs.yaml b/policyengine_us/programs.yaml index ecef005005e..299b96b4f1a 100644 --- a/policyengine_us/programs.yaml +++ b/policyengine_us/programs.yaml @@ -1004,8 +1004,8 @@ programs: full_name: Alabama Low Income Home Energy Assistance Program Regular Cooling variable: al_liheap_cooling parameter_prefix: gov.states.al.adeca.liheap - verified_years: "2026" - notes: FY2026 Regular Cooling from the Electric column of the May 15, 2026 chart, less the HUD allowance, plus the funded IIJA supplement. Crisis cooling, fans, air conditioners and the discretionary $50 high-need addition are out of scope. + verified_years: "2025-2026" + notes: FY2025-FY2026 Regular Cooling from the Electric column of the February 17, 2025 and May 15, 2026 charts, less the HUD allowance, plus the funded IIJA supplement, including nonsubsidized renters with energy costs in rent. Crisis cooling, fans, air conditioners and the discretionary $50 high-need addition are out of scope. - state: AK status: partial name: Alaska HAP diff --git a/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling.yaml b/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling.yaml index e8981f00b9e..1a7654d37ac 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling.yaml @@ -7,7 +7,8 @@ # Electric column: size 1 $440 / $410 / $320; size 4 and up $470 / $440 / $350. # Part-time minimum wage: 20 hours x $7.25 x 52 weeks = $7,540 a year, $628.33 a month, -# band 1. Cooling: Electric $440 + $100 = $540. The gas heating column would pay $480. +# band 1. Cooling: Electric $440 + $100 = $540. The gas heating column pays $480, and +# heating adds its own $100 supplement: $580. The national LIHEAP total is $580 + $540. - name: Case 1, a gas-heated worker receives the Electric column cooling payment. period: 2026 absolute_error_margin: 0.01 @@ -22,6 +23,8 @@ output: al_liheap_base_payment: 480 al_liheap_cooling: 540 + al_liheap: 580 + liheap: 1_120 # Retiree with $1,100 a month of Social Security ($13,200 a year), band 2 ($666 to $1,331). # SSI is zero: $1,100 - $20 = $1,080 exceeds the $994 federal benefit rate. @@ -188,3 +191,23 @@ output: al_liheap_cooling_eligible: false al_liheap_cooling: 0 + +# FY2025 plan: cooling runs May 1 to September 30, 2025, pays $320 to $520, and adds the +# $100 IIJA supplement. The chart effective February 17, 2025 uses the 2025 guidelines: +# size 1 limit 15,650 x 1.5 / 12 = 1,956.25, printed $1,956 ($23,472 a year), band 2 +# $653 to $1,305, with the same Electric column. Case 2's retiree, $1,100 a month, is in +# band 2 (SSI is zero: $1,100 - $20 = $1,080 exceeds the $967 federal benefit rate). +# Cooling: $440 - $30 = $410 + $100 = $510. +- name: Case 11, the summer 2025 payment uses the February 17, 2025 chart. + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: AL + age: 70 + social_security_retirement: 13_200 + heating_type: ELECTRICITY + has_cooling_expense: true + hud_utility_allowance: 0 + output: + al_liheap_cooling_income_limit: 23_472 + al_liheap_cooling: 510 diff --git a/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_eligible.yaml index e889551bc46..4adf975a1c7 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_eligible.yaml @@ -71,3 +71,26 @@ output: al_liheap_eligible: true al_liheap_cooling_eligible: false + +# Manual 5.3 and 6.1.3 make renters in nonsubsidized housing whose energy costs are +# included in the rent eligible for Regular Assistance. Both renters pay the FY2026 HUD +# Fair Market Rent for a one-bedroom unit in Jefferson County, $1,155 a month (x12 = +# 13,860), which includes utilities, and report no separate cooling cost. The part-time +# worker ($628.33 a month, band 1) receives $440 + $100 = $540 when unsubsidized; the +# subsidized renter is not covered by the rent route. +- name: Case 6, a nonsubsidized renter with energy costs in the rent is cooling eligible. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [AL, AL] + age: [45, 45] + employment_income_before_lsr: [7_540, 7_540] + heating_type: [NATURAL_GAS, NATURAL_GAS] + has_cooling_expense: [false, false] + tenant_pays_utilities: [false, false] + rent: [13_860, 13_860] + receives_housing_assistance: [false, true] + hud_utility_allowance: [0, 0] + output: + al_liheap_cooling_eligible: [true, false] + al_liheap_cooling: [540, 0] diff --git a/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling.py b/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling.py index cd3d84942d8..66eb449f8ec 100644 --- a/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling.py +++ b/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling.py @@ -17,9 +17,10 @@ class al_liheap_cooling(Variable): unit = USD label = "Alabama LIHEAP regular cooling assistance" documentation = ( - "Verified for FY2026 (the May-September 2026 cooling season). Earlier " - "years use model parameter backfilling and are unverified historical " - "estimates. Later years carry the FY2026 chart amounts forward." + "Verified for FY2025 and FY2026 (the May-September 2025 and 2026 " + "cooling seasons). Earlier years use model parameter backfilling and " + "are unverified historical estimates. Later years carry the FY2026 " + "chart amounts forward." ) defined_for = "al_liheap_cooling_eligible" reference = ( @@ -27,7 +28,15 @@ class al_liheap_cooling(Variable): "https://adeca.alabama.gov/wp-content/uploads/FY-2026-LIHEAP-Manual-1.pdf#page=9", # PDF pages 12-13 "https://liheapch.acf.gov/docs/2026/state-plans/AL_Plan_2026.pdf#page=12", + # PDF pages 4, 12-13: the FY2025 cooling season, IIJA supplement and + # $320 to $520 range. + "https://liheapch.acf.gov/docs/2025/state-plans/AL_Plan_2025.pdf#page=4", + # The chart effective February 17, 2025, on the 2025 guidelines. + "https://liheapch.acf.gov/docs/2026/benefits-matricies/AL_BenefitMatrix_Cooling_2026.pdf", + # The chart effective May 15, 2026, attached to the draft FY2027 plan. "https://adeca.alabama.gov/wp-content/uploads/FY27-LIHEAP-State-Plan.pdf#page=53", + # Section 3.5: the 2026 guideline limit and band arithmetic. + "https://liheapch.acf.gov/docs/2027/state-plans/AL_Plan_2027.pdf#page=12", ) def formula(spm_unit, period, parameters): diff --git a/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_eligible.py b/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_eligible.py index f16de32f4f8..84e3a716cc3 100644 --- a/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_eligible.py +++ b/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_eligible.py @@ -8,7 +8,7 @@ class al_liheap_cooling_eligible(Variable): label = "Alabama LIHEAP regular cooling assistance eligibility" defined_for = StateCode.AL reference = ( - # PDF pages 9, 16-17, 21 + # PDF pages 9, 16-17, 21-22 "https://adeca.alabama.gov/wp-content/uploads/FY-2026-LIHEAP-Manual-1.pdf#page=9", "https://liheapch.acf.gov/docs/2026/state-plans/AL_Plan_2026.pdf#page=11", ) @@ -19,12 +19,21 @@ def formula(spm_unit, period, parameters): limit = spm_unit("al_liheap_cooling_income_limit", period) # The cooling cost input stands for the recent cooling bill (manual # 5.3) and, for subsidized tenants, the out-of-pocket energy cost that - # manual 6.1.1 requires. Cooling costs paid only through nonsubsidized - # rent have no existing input and are not paid. - has_cooling_expense = spm_unit("has_cooling_expense", period) + # manual 6.1.1 requires. Renters in nonsubsidized housing whose energy + # costs are included in the rent are also eligible (manual 5.3 and + # 6.1.3). + subsidized = spm_unit("receives_housing_assistance", period) | ( + spm_unit.household("is_in_public_housing", period) + ) + utilities_in_rent = ~spm_unit.household("tenant_pays_utilities", period) & ( + add(spm_unit, period, ["rent"]) > 0 + ) + responsible = spm_unit("has_cooling_expense", period) | ( + utilities_in_rent & ~subsidized + ) # The heating fuel must be reported: UNSPECIFIED pays nothing, while # NONE and every known fuel qualify, since cooling pays the Electric # column whatever the heating fuel. heating_type = spm_unit("heating_type", period) fuel_reported = heating_type != heating_type.possible_values.UNSPECIFIED - return (size > 0) & (income <= limit) & has_cooling_expense & fuel_reported + return (size > 0) & (income <= limit) & responsible & fuel_reported diff --git a/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_income_limit.py b/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_income_limit.py index 33ea8978b01..4139c13015b 100644 --- a/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_income_limit.py +++ b/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_income_limit.py @@ -12,7 +12,11 @@ class al_liheap_cooling_income_limit(Variable): label = "Alabama LIHEAP regular cooling annualized income limit" defined_for = StateCode.AL reference = ( + # The chart effective May 15, 2026, attached to the draft FY2027 plan. "https://adeca.alabama.gov/wp-content/uploads/FY27-LIHEAP-State-Plan.pdf#page=53", + # Section 3.5: $15,960 x 1.5 = $23,940 a year, $1,995 a month for one + # person, on the 2026 guidelines. + "https://liheapch.acf.gov/docs/2027/state-plans/AL_Plan_2027.pdf#page=12", # PDF pages 11-12 "https://liheapch.acf.gov/docs/2026/state-plans/AL_Plan_2026.pdf#page=11", ) From e5a2a6c994c1b4e14abbfa7dfc9a56a1d934e910 Mon Sep 17 00:00:00 2001 From: Ziming Date: Wed, 11 Nov 2026 20:59:38 -0500 Subject: [PATCH 06/11] Make state LIHEAP formulas run in a microsimulation The national `liheap` total evaluates every state's formula over a mixed-state population, which no household test does. Two patterns on main raised errors there: - 26 formula files passed options=[ADD] to an entity call. Core's Microsimulation.calculate_add rejects the keyword an entity call passes. The option is redundant: a plain read already sums a monthly variable over the year through the same Core function. - The DC and Illinois payment formulas look up a matrix by an income level that is 0 for households outside the state. Add the floor Maryland and Kansas already use. Also: - Reword the North Dakota registry note: the state reserves an optional direct cooling payment, so its cooling component is not only equipment. - Add a `liheap` case with Texas, DC and Illinois households in one simulation. - Add a code-health check against options=[ADD] and options=[DIVIDE]. - Check that every state payment in the LIHEAP registry entry is in the list `liheap` sums. Co-Authored-By: Claude Fable 5.2 --- changelog.d/liheap-aggregate.fixed.md | 1 + policyengine_us/programs.yaml | 2 +- ...o_add_or_divide_options_in_entity_calls.py | 40 +++++++++ .../baseline/gov/hhs/liheap/liheap.yaml | 87 +++++++++++++++++++ .../tests/test_aggregate_list_integrity.py | 37 ++++++++ .../az_liheap_countable_unearned_income.py | 2 +- .../leap/co_leap_countable_income_person.py | 2 +- .../ct_ceap_categorically_eligible.py | 8 +- .../dc/doee/liheap/dc_liheap_payment.py | 3 +- .../fl_liheap_categorically_eligible.py | 2 +- .../income/fl_liheap_countable_income.py | 4 +- .../hi/dhs/liheap/hi_liheap_eligible.py | 3 +- .../states/hi/dhs/liheap/hi_liheap_points.py | 2 +- .../ia_liheap_countable_unearned_income.py | 2 +- .../income/ia_liheap_fixed_income_only.py | 2 +- .../eligibility/id_liheap_income_eligible.py | 5 +- .../liheap/payment/il_liheap_base_payment.py | 3 +- .../income/la_liheap_countable_income.py | 2 +- .../eap/income/mn_eap_countable_income.py | 2 +- .../mt_liheap_categorical_eligibility.py | 2 +- .../income/mt_liheap_countable_income.py | 2 +- .../payment/mt_liheap_benefit_income.py | 2 +- .../income/nm_liheap_countable_income.py | 2 +- .../income/nv_liheap_countable_income.py | 2 +- .../income/oh_liheap_countable_income.py | 2 +- .../income/ok_liheap_countable_income.py | 2 +- .../dhs/liheap/income/ok_liheap_net_income.py | 2 +- .../liheap/payment/pa_liheap_supplement.py | 2 +- .../income/sd_liheap_countable_income.py | 2 +- .../income/tn_liheap_countable_income.py | 2 +- .../va/dss/eap/va_eap_countable_income.py | 2 +- .../wi_wheap_categorically_eligible.py | 2 +- .../wheap/income/wi_wheap_countable_income.py | 2 +- 33 files changed, 200 insertions(+), 37 deletions(-) create mode 100644 changelog.d/liheap-aggregate.fixed.md create mode 100644 policyengine_us/tests/code_health/test_no_add_or_divide_options_in_entity_calls.py diff --git a/changelog.d/liheap-aggregate.fixed.md b/changelog.d/liheap-aggregate.fixed.md new file mode 100644 index 00000000000..0748ac80681 --- /dev/null +++ b/changelog.d/liheap-aggregate.fixed.md @@ -0,0 +1 @@ +Fix state LIHEAP formulas that raised an error when calculated in a microsimulation. diff --git a/policyengine_us/programs.yaml b/policyengine_us/programs.yaml index 05351342814..c01776a76c1 100644 --- a/policyengine_us/programs.yaml +++ b/policyengine_us/programs.yaml @@ -1284,7 +1284,7 @@ programs: variable: nd_liheap parameter_prefix: gov.states.nd.dhs.liheap verified_years: "2026" - notes: FY2026 regular heating, including heat-in-rent payments. North Dakota has no regular cooling payment, and its cooling component only buys or repairs air conditioners and fans; crisis, weatherization and equipment assistance are out of scope. + notes: FY2026 regular heating, including heat-in-rent payments. North Dakota's cooling component buys or repairs air conditioners and fans, and the optional direct cooling payment it reserves has no set amount, so no cooling payment is modeled; crisis, weatherization and equipment assistance are out of scope. - state: NE status: partial name: Nebraska LIHEAP diff --git a/policyengine_us/tests/code_health/test_no_add_or_divide_options_in_entity_calls.py b/policyengine_us/tests/code_health/test_no_add_or_divide_options_in_entity_calls.py new file mode 100644 index 00000000000..c0966626caf --- /dev/null +++ b/policyengine_us/tests/code_health/test_no_add_or_divide_options_in_entity_calls.py @@ -0,0 +1,40 @@ +import ast + +from policyengine_us.model_api import REPO + + +VARIABLES_ROOT = REPO / "variables" +PERIOD_OPTIONS = {"ADD", "DIVIDE"} + + +def passes_period_option(node: ast.AST) -> bool: + if not isinstance(node, ast.Call): + return False + return any( + keyword.arg == "options" + and any( + isinstance(child, ast.Name) and child.id in PERIOD_OPTIONS + for child in ast.walk(keyword.value) + ) + for keyword in node.keywords + ) + + +def test_entity_calls_do_not_pass_add_or_divide_options(): + violations = [] + + for file_name in sorted(VARIABLES_ROOT.glob("**/*.py")): + tree = ast.parse(file_name.read_text(), filename=str(file_name)) + for node in ast.walk(tree): + if passes_period_option(node): + violations.append(f"{file_name.relative_to(REPO)}:{node.lineno}") + + assert not violations, ( + "Read the variable with the bare period, or use add(entity, period, " + "sources): Core already sums a monthly variable over a year and " + "divides a yearly one over a month. options=[ADD] and " + "options=[DIVIDE] raise a TypeError in a Microsimulation, whose " + "calculate_add and calculate_divide do not accept the decode_enums " + "keyword that an entity call passes, so household tests pass while " + "the formula fails over microdata. Violations:\n" + "\n".join(violations) + ) diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/liheap/liheap.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/liheap/liheap.yaml index 136b8caa264..3d44d473075 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/liheap/liheap.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/liheap/liheap.yaml @@ -118,3 +118,90 @@ ca_liheap_utility_heating: 0 ca_liheap_covered_liquid_heating: 0 liheap: 850 + +- name: Case 5, households in different states are each paid by their own state's program. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 34 + # 12 an hour for 30 hours a week: 12 * 30 * 52 = 18,720. + employment_income_before_lsr: 18_720 + person2: + age: 52 + # 10 hours a week at DC's 17.95 minimum wage: 17.95 * 10 * 52 = 9,334. + employment_income_before_lsr: 9_334 + person3: + age: 29 + # 33 hours a week at the 15 Illinois minimum wage: 15 * 33 * 52 = 25,740. + employment_income_before_lsr: 25_740 + person4: + age: 6 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + tax_unit3: + members: [person3, person4] + families: + family1: + members: [person1] + family2: + members: [person2] + family3: + members: [person3, person4] + marital_units: + marital_unit1: + members: [person1] + marital_unit2: + members: [person2] + marital_unit3: + members: [person3] + marital_unit4: + members: [person4] + spm_units: + spm_unit1: + members: [person1] + heating_type: ELECTRICITY + has_cooling_expense: true + # 165 a month for heating and air conditioning. + pre_subsidy_electricity_expense: 1_980 + spm_unit2: + members: [person2] + heating_type: ELECTRICITY + # 110 a month in an all-electric apartment. + pre_subsidy_electricity_expense: 1_320 + spm_unit3: + members: [person3, person4] + heating_type: ELECTRICITY + has_cooling_expense: true + # 150 a month in an all-electric apartment. + pre_subsidy_electricity_expense: 1_800 + households: + household1: + members: [person1] + state_code: TX + household2: + members: [person2] + state_code: DC + household3: + members: [person3, person4] + state_code: IL + output: + # Each state's formula runs over all three households. A household has + # no income level or bracket outside its own state. + # Texas, as in Case 1: min(1,200, electricity 1,980) = 1,200. + tx_ceap: [1_200, 0, 0] + # DC: 9,334 falls in the second 5,000 income step (level 2). FY2026 + # multi-family electricity amount, level 2, one person: 529. + # min(529, electricity 1,320) = 529. + dc_liheap_income_level: [0, 2, 0] + dc_liheap_payment: [0, 529, 0] + # Illinois, as in Case 3: bracket 3, all-electric, two people: 425. + # min(425, electricity 1,800) = 425. + il_liheap_income_bracket: [0, 0, 3] + il_liheap_regular_heating: [0, 0, 425] + # No household is paid by another state's program. + liheap: [1_200, 529, 425] diff --git a/policyengine_us/tests/test_aggregate_list_integrity.py b/policyengine_us/tests/test_aggregate_list_integrity.py index 707e2fe59d3..1f5ac0922ff 100644 --- a/policyengine_us/tests/test_aggregate_list_integrity.py +++ b/policyengine_us/tests/test_aggregate_list_integrity.py @@ -175,6 +175,43 @@ def test_no_silent_removals_between_year_blocks(list_path): ) +LIHEAP_PAYMENTS = "gov/hhs/liheap/regular_payments.yaml" + +# Registry payment variables the national LIHEAP total leaves out on purpose. +LIHEAP_PAYMENTS_NOT_SUMMED = { + # il_liheap_regular_heating is summed instead: il_liheap also counts + # crisis assistance. + "il_liheap", + # The Home Heating Credit already counts as a refundable state credit. + "mi_home_heating_credit", +} + + +def test_liheap_total_sums_every_registered_state_payment(variables): + """A state payment in the registry must reach the national total. + + The LIHEAP registry entry names one variable per state implementation. + Each one that is a dollar amount belongs in the list `liheap` sums, so a + new state payment, or a list line deleted by mistake, cannot drop a state + from the total unnoticed. Eligibility flags carry no amount. + """ + programs = yaml.safe_load((PACKAGE / "programs.yaml").read_text())["programs"] + liheap = next(program for program in programs if program["id"] == "liheap") + payments = { + implementation["variable"] + for implementation in liheap["state_implementations"] + if variables[implementation["variable"]].value_type is float + } + missing = sorted( + payments - LIHEAP_PAYMENTS_NOT_SUMMED - members_of(LIHEAP_PAYMENTS) + ) + assert not missing, ( + f"{LIHEAP_PAYMENTS} lacks these state payments from the LIHEAP " + f"registry entry: {missing}. Add each to the list, or record why it " + "is left out in LIHEAP_PAYMENTS_NOT_SUMMED in this test." + ) + + # --- Reform-side copies of the benefit aggregates ------------------------- AGGREGATE_VARIABLES = ("household_benefits", "spm_unit_benefits") diff --git a/policyengine_us/variables/gov/states/az/des/liheap/income/az_liheap_countable_unearned_income.py b/policyengine_us/variables/gov/states/az/des/liheap/income/az_liheap_countable_unearned_income.py index 7716e5f7fca..80901bbf422 100644 --- a/policyengine_us/variables/gov/states/az/des/liheap/income/az_liheap_countable_unearned_income.py +++ b/policyengine_us/variables/gov/states/az/des/liheap/income/az_liheap_countable_unearned_income.py @@ -35,7 +35,7 @@ def formula(person, period, parameters): # the year. unearned = 0 for source in p.sources.unearned: - unearned = unearned + max_(person(source, period, options=[ADD]), 0) + unearned = unearned + max_(person(source, period), 0) # Section VI.A.6.e.ii.B counts "gambling profits" (page 9), so # gambling losses offset winnings but not other income. Plan Section # 1.9 leaves one-time lump sums such as "winnings from lotteries" diff --git a/policyengine_us/variables/gov/states/co/cdhs/leap/co_leap_countable_income_person.py b/policyengine_us/variables/gov/states/co/cdhs/leap/co_leap_countable_income_person.py index a6d89fc3e8d..5d5e84d7c97 100644 --- a/policyengine_us/variables/gov/states/co/cdhs/leap/co_leap_countable_income_person.py +++ b/policyengine_us/variables/gov/states/co/cdhs/leap/co_leap_countable_income_person.py @@ -28,7 +28,7 @@ def formula(person, period, parameters): # monthly SSA and SSI amounts lose cents separately for each person. # Medicare premiums remain in gross SSA income under Colorado rules. social_security = np.floor(person("social_security", period) / MONTHS_IN_YEAR) - ssi = np.floor(person("ssi", period, options=[ADD]) / MONTHS_IN_YEAR) + ssi = np.floor(person("ssi", period) / MONTHS_IN_YEAR) income = earned + unearned + (social_security + ssi) * MONTHS_IN_YEAR foster = person("is_in_foster_care", period.first_month) # Known foster individuals are outside the LEAP household; income of diff --git a/policyengine_us/variables/gov/states/ct/dss/ceap/eligibility/ct_ceap_categorically_eligible.py b/policyengine_us/variables/gov/states/ct/dss/ceap/eligibility/ct_ceap_categorically_eligible.py index 8672b1e54b0..3c3c1a82e9a 100644 --- a/policyengine_us/variables/gov/states/ct/dss/ceap/eligibility/ct_ceap_categorically_eligible.py +++ b/policyengine_us/variables/gov/states/ct/dss/ceap/eligibility/ct_ceap_categorically_eligible.py @@ -16,15 +16,13 @@ def formula(spm_unit, period, parameters): # generic veterans_benefits does not establish VA eligibility. person = spm_unit.members member = person("ct_ceap_household_member", period) - ssi = spm_unit.sum(member * person("ssi", period, options=[ADD])) + ssi = spm_unit.sum(member * person("ssi", period)) # Remove SSP attributable to members outside the household from the # household receipt, preserving a supplied household SSP total. The # floor stops a supplied total below the excluded members' SSP from # cancelling other receipt. - excluded_ssp = spm_unit.sum( - ~member * person("ct_ssp_person", period, options=[ADD]) - ) - ssp = max_(spm_unit("ct_ssp", period, options=[ADD]) - excluded_ssp, 0) + excluded_ssp = spm_unit.sum(~member * person("ct_ssp_person", period)) + ssp = max_(spm_unit("ct_ssp", period) - excluded_ssp, 0) reported_ssi = spm_unit.any( member & (add(person, period, ["receives_ssi"]) > 0) ) diff --git a/policyengine_us/variables/gov/states/dc/doee/liheap/dc_liheap_payment.py b/policyengine_us/variables/gov/states/dc/doee/liheap/dc_liheap_payment.py index d032fb8c39f..43a23bd7157 100644 --- a/policyengine_us/variables/gov/states/dc/doee/liheap/dc_liheap_payment.py +++ b/policyengine_us/variables/gov/states/dc/doee/liheap/dc_liheap_payment.py @@ -14,7 +14,8 @@ def formula(spm_unit, period, parameters): p = parameters(period).gov.states.dc.doee.liheap.payment housing_type = spm_unit("dc_liheap_housing_type", period) heating_type = spm_unit("dc_liheap_heating_type", period) - income_level = spm_unit("dc_liheap_income_level", period) + # The floor keeps units outside DC (income level 0) on a valid matrix key. + income_level = max_(spm_unit("dc_liheap_income_level", period), 1) capped_size = clip(spm_unit("spm_unit_size", period), 1, 4) types = heating_type.possible_values diff --git a/policyengine_us/variables/gov/states/fl/commerce/liheap/eligibility/fl_liheap_categorically_eligible.py b/policyengine_us/variables/gov/states/fl/commerce/liheap/eligibility/fl_liheap_categorically_eligible.py index ff4810b8eec..9bc7c20a041 100644 --- a/policyengine_us/variables/gov/states/fl/commerce/liheap/eligibility/fl_liheap_categorically_eligible.py +++ b/policyengine_us/variables/gov/states/fl/commerce/liheap/eligibility/fl_liheap_categorically_eligible.py @@ -14,7 +14,7 @@ class fl_liheap_categorically_eligible(Variable): def formula(spm_unit, period, parameters): p = parameters(period).gov.states.fl.commerce.liheap.eligibility - receipts = add(spm_unit, period, p.categorical_programs, options=[ADD]) + receipts = add(spm_unit, period, p.categorical_programs) # Positive take-up-gated SNAP, TANF, or SSI is the receipt proxy. # A minor's SSI may establish this status even when excluded from income. # This waives only the income test, not heating responsibility, residency, diff --git a/policyengine_us/variables/gov/states/fl/commerce/liheap/income/fl_liheap_countable_income.py b/policyengine_us/variables/gov/states/fl/commerce/liheap/income/fl_liheap_countable_income.py index 6a15de059a1..60e8c17c9fe 100644 --- a/policyengine_us/variables/gov/states/fl/commerce/liheap/income/fl_liheap_countable_income.py +++ b/policyengine_us/variables/gov/states/fl/commerce/liheap/income/fl_liheap_countable_income.py @@ -39,7 +39,7 @@ def formula(spm_unit, period, parameters): earned = earned + max_(person(source, period), 0) unearned = 0 for source in p.sources.unearned: - unearned = unearned + max_(person(source, period, options=[ADD]), 0) + unearned = unearned + max_(person(source, period), 0) # Plan section 1.9 counts "net gambling or lottery winnings", so # losses offset only winnings. gambling = max_( @@ -48,7 +48,7 @@ def formula(spm_unit, period, parameters): ) # Count TANF once at SPM level. The take-up-gated tanf total includes # fl_tca; fl_tca alone is the entitlement a nonrecipient could get. - cash_grant = add(spm_unit, period, p.sources.household, options=[ADD]) + cash_grant = add(spm_unit, period, p.sources.household) # Annual inputs approximate the prior 30 days/current economic situation. # Nonqualified members' countable income is retained in full. UI and # cash gifts are excluded by the plans. Gross SSA is included; the diff --git a/policyengine_us/variables/gov/states/hi/dhs/liheap/hi_liheap_eligible.py b/policyengine_us/variables/gov/states/hi/dhs/liheap/hi_liheap_eligible.py index e5292c13fac..57b58f8ea49 100644 --- a/policyengine_us/variables/gov/states/hi/dhs/liheap/hi_liheap_eligible.py +++ b/policyengine_us/variables/gov/states/hi/dhs/liheap/hi_liheap_eligible.py @@ -31,8 +31,7 @@ def formula(spm_unit, period, parameters): # for the first day of the application month. person = spm_unit.members receives_ssi = spm_unit.any( - (person("ssi", period, options=[ADD]) > 0) - | (add(person, period, ["receives_ssi"]) > 0) + (person("ssi", period) > 0) | (add(person, period, ["receives_ssi"]) > 0) ) categorical = receives_ssi | ( add(spm_unit, period, ["snap", "tanf", "general_assistance"]) > 0 diff --git a/policyengine_us/variables/gov/states/hi/dhs/liheap/hi_liheap_points.py b/policyengine_us/variables/gov/states/hi/dhs/liheap/hi_liheap_points.py index ccf15e3e147..98feb6c9703 100644 --- a/policyengine_us/variables/gov/states/hi/dhs/liheap/hi_liheap_points.py +++ b/policyengine_us/variables/gov/states/hi/dhs/liheap/hi_liheap_points.py @@ -57,7 +57,7 @@ def formula(spm_unit, period, parameters): # so is_disabled stands in for them. This affects points only. disabled = ( person("is_disabled", period) - | (person("ssi", period, options=[ADD]) > 0) + | (person("ssi", period) > 0) | (add(person, period, ["receives_ssi"]) > 0) | (person("social_security_disability", period) > 0) ) diff --git a/policyengine_us/variables/gov/states/ia/hhs/liheap/income/ia_liheap_countable_unearned_income.py b/policyengine_us/variables/gov/states/ia/hhs/liheap/income/ia_liheap_countable_unearned_income.py index 215ebc9e9b2..75aca29dcca 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/liheap/income/ia_liheap_countable_unearned_income.py +++ b/policyengine_us/variables/gov/states/ia/hhs/liheap/income/ia_liheap_countable_unearned_income.py @@ -49,7 +49,7 @@ def formula(person, period, parameters): unearned = 0 for source in p.sources.unearned: # ADD sums monthly sources such as ssi over the year. - unearned = unearned + max_(person(source, period, options=[ADD]), 0) + unearned = unearned + max_(person(source, period), 0) # Plan Section 1.9 (page 6) counts Social Security Administration # benefits "Excluding MediCare deduction". Section 6.10 (page 25) and # Appendix C, Social Security Benefits (page 114), count "the net diff --git a/policyengine_us/variables/gov/states/ia/hhs/liheap/income/ia_liheap_fixed_income_only.py b/policyengine_us/variables/gov/states/ia/hhs/liheap/income/ia_liheap_fixed_income_only.py index eb60d051ab5..229ef7f33c5 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/liheap/income/ia_liheap_fixed_income_only.py +++ b/policyengine_us/variables/gov/states/ia/hhs/liheap/income/ia_liheap_fixed_income_only.py @@ -54,6 +54,6 @@ def formula(spm_unit, period, parameters): other_unearned = [source for source in p.unearned if source not in p.fixed] other_income = 0 for source in list(p.earned) + other_unearned: - other_income = other_income + max_(person(source, period, options=[ADD]), 0) + other_income = other_income + max_(person(source, period), 0) non_fixed = spm_unit.sum(other_income) + add(spm_unit, period, p.other_recorded) return (fixed > 0) & (non_fixed == 0) diff --git a/policyengine_us/variables/gov/states/id/dhw/liheap/eligibility/id_liheap_income_eligible.py b/policyengine_us/variables/gov/states/id/dhw/liheap/eligibility/id_liheap_income_eligible.py index 10ac696b5c2..2dfab55f0eb 100644 --- a/policyengine_us/variables/gov/states/id/dhw/liheap/eligibility/id_liheap_income_eligible.py +++ b/policyengine_us/variables/gov/states/id/dhw/liheap/eligibility/id_liheap_income_eligible.py @@ -20,13 +20,12 @@ def formula(spm_unit, period, parameters): snap_period = period.offset( p.program_year_start_month - 1 - MONTHS_IN_YEAR, "month" ) - receives_snap = spm_unit("snap", snap_period, options=[ADD]) > 0 + receives_snap = spm_unit("snap", snap_period) > 0 # Annual SSI is a stable-current-receipt proxy because the application # month is not an input. Any member may establish categorical eligibility. person = spm_unit.members receives_ssi = spm_unit.any( - (person("ssi", period, options=[ADD]) > 0) - | (add(person, period, ["receives_ssi"]) > 0) + (person("ssi", period) > 0) | (add(person, period, ["receives_ssi"]) > 0) ) # Means-tested VA receipt also qualifies, but veterans_benefits does not # identify that category. This unmodeled route is not a legal exclusion. diff --git a/policyengine_us/variables/gov/states/il/dceo/liheap/payment/il_liheap_base_payment.py b/policyengine_us/variables/gov/states/il/dceo/liheap/payment/il_liheap_base_payment.py index 7373fed85ed..400a02fee54 100644 --- a/policyengine_us/variables/gov/states/il/dceo/liheap/payment/il_liheap_base_payment.py +++ b/policyengine_us/variables/gov/states/il/dceo/liheap/payment/il_liheap_base_payment.py @@ -13,7 +13,8 @@ class il_liheap_base_payment(Variable): def formula(spm_unit, period, parameters): p = parameters(period).gov.states.il.dceo.liheap.payment.matrix fuel_type = spm_unit("il_liheap_heating_type", period) - income_bracket = spm_unit("il_liheap_income_bracket", period) + # The floor keeps units outside Illinois (bracket 0) on a valid matrix key. + income_bracket = max_(spm_unit("il_liheap_income_bracket", period), 1) size = spm_unit("spm_unit_size", period) capped_size = clip(size, 1, 6) diff --git a/policyengine_us/variables/gov/states/la/lhc/liheap/income/la_liheap_countable_income.py b/policyengine_us/variables/gov/states/la/lhc/liheap/income/la_liheap_countable_income.py index dd9f690f702..a352fd9f913 100644 --- a/policyengine_us/variables/gov/states/la/lhc/liheap/income/la_liheap_countable_income.py +++ b/policyengine_us/variables/gov/states/la/lhc/liheap/income/la_liheap_countable_income.py @@ -55,7 +55,7 @@ def formula(spm_unit, period, parameters): - person("medicare_part_b_premium", period), 0, ) - ssi = person("ssi", period, options=[ADD]) + ssi = person("ssi", period) # Undocumented members keep all their income. FITAP is counted once at # the SPM level: the take-up-gated tanf total includes la_fitap, which # alone would count a grant a nonrecipient could get. diff --git a/policyengine_us/variables/gov/states/mn/commerce/eap/income/mn_eap_countable_income.py b/policyengine_us/variables/gov/states/mn/commerce/eap/income/mn_eap_countable_income.py index db60d91817f..71e882c897e 100644 --- a/policyengine_us/variables/gov/states/mn/commerce/eap/income/mn_eap_countable_income.py +++ b/policyengine_us/variables/gov/states/mn/commerce/eap/income/mn_eap_countable_income.py @@ -35,7 +35,7 @@ def formula(spm_unit, period, parameters): unearned = 0 for source in p.sources.unearned: # ADD sums monthly sources such as ssi over the year. - unearned = unearned + max_(person(source, period, options=[ADD]), 0) + unearned = unearned + max_(person(source, period), 0) # Retirement payments count from age 59 1/2. Early benefits before that # age count only when not penalized; no input shows whether a withdrawal # was penalized, so the age cut-off approximates the penalty test. diff --git a/policyengine_us/variables/gov/states/mt/dphhs/liheap/eligibility/mt_liheap_categorical_eligibility.py b/policyengine_us/variables/gov/states/mt/dphhs/liheap/eligibility/mt_liheap_categorical_eligibility.py index d8ed0849806..28bbd5bb873 100644 --- a/policyengine_us/variables/gov/states/mt/dphhs/liheap/eligibility/mt_liheap_categorical_eligibility.py +++ b/policyengine_us/variables/gov/states/mt/dphhs/liheap/eligibility/mt_liheap_categorical_eligibility.py @@ -34,7 +34,7 @@ def formula(spm_unit, period, parameters): # Monthly booleans are read month by month, never with an annual ADD, # so no summed value is cached under the annual key. # SNAP: the unit receives SNAP and every member is in the SNAP unit. - receives_snap = spm_unit("snap", period, options=[ADD]) > 0 + receives_snap = spm_unit("snap", period) > 0 in_snap_unit = np.any( [~person("is_snap_excluded_member", month) for month in months], axis=0, diff --git a/policyengine_us/variables/gov/states/mt/dphhs/liheap/income/mt_liheap_countable_income.py b/policyengine_us/variables/gov/states/mt/dphhs/liheap/income/mt_liheap_countable_income.py index c3273f7e930..f596942d881 100644 --- a/policyengine_us/variables/gov/states/mt/dphhs/liheap/income/mt_liheap_countable_income.py +++ b/policyengine_us/variables/gov/states/mt/dphhs/liheap/income/mt_liheap_countable_income.py @@ -34,7 +34,7 @@ def formula(spm_unit, period, parameters): # method are not modeled. income = 0 for source in p.sources.earned + p.sources.unearned: - income = income + max_(person(source, period, options=[ADD]), 0) + income = income + max_(person(source, period), 0) # ARM 37.70.407(1)(d): the dependents and survivors benefits that # continue for full-time students aged 18 to 22 are excluded. age = person("age", period) diff --git a/policyengine_us/variables/gov/states/mt/dphhs/liheap/payment/mt_liheap_benefit_income.py b/policyengine_us/variables/gov/states/mt/dphhs/liheap/payment/mt_liheap_benefit_income.py index 8e22fcdfc75..5135078d9e4 100644 --- a/policyengine_us/variables/gov/states/mt/dphhs/liheap/payment/mt_liheap_benefit_income.py +++ b/policyengine_us/variables/gov/states/mt/dphhs/liheap/payment/mt_liheap_benefit_income.py @@ -47,7 +47,7 @@ def formula(spm_unit, period, parameters): # The TANF payment standard for the assistance unit, rounded to whole # dollars as Appendix A prints it. monthly_tanf_standard = ( - spm_unit("mt_tanf_payment_standard", period, options=[ADD]) / MONTHS_IN_YEAR + spm_unit("mt_tanf_payment_standard", period) / MONTHS_IN_YEAR ) tanf_standard = where( spm_unit("tanf", period) > 0, diff --git a/policyengine_us/variables/gov/states/nm/hca/liheap/income/nm_liheap_countable_income.py b/policyengine_us/variables/gov/states/nm/hca/liheap/income/nm_liheap_countable_income.py index d1ea2b6fe39..f6c0d8b28be 100644 --- a/policyengine_us/variables/gov/states/nm/hca/liheap/income/nm_liheap_countable_income.py +++ b/policyengine_us/variables/gov/states/nm/hca/liheap/income/nm_liheap_countable_income.py @@ -30,7 +30,7 @@ def formula(spm_unit, period, parameters): earned = earned + max_(person(source, period), 0) unearned = 0 for source in p.sources.unearned: - unearned = unearned + max_(person(source, period, options=[ADD]), 0) + unearned = unearned + max_(person(source, period), 0) member_income = earned * earned_counted + unearned # The gross income of an ineligible non-citizen is divided by all # members, eligible and ineligible, and counted for the eligible diff --git a/policyengine_us/variables/gov/states/nv/dwss/liheap/income/nv_liheap_countable_income.py b/policyengine_us/variables/gov/states/nv/dwss/liheap/income/nv_liheap_countable_income.py index b68ba674389..d86456d8626 100644 --- a/policyengine_us/variables/gov/states/nv/dwss/liheap/income/nv_liheap_countable_income.py +++ b/policyengine_us/variables/gov/states/nv/dwss/liheap/income/nv_liheap_countable_income.py @@ -35,7 +35,7 @@ def formula(spm_unit, period, parameters): # Each unearned source is floored at zero. unearned = 0 for source in p.sources.unearned: - unearned = unearned + max_(person(source, period, options=[ADD]), 0) + unearned = unearned + max_(person(source, period), 0) # The only deductions from unearned income are the Medicare premiums # taken from a Social Security check; only the Part B premium has an # input. diff --git a/policyengine_us/variables/gov/states/oh/odjfs/liheap/income/oh_liheap_countable_income.py b/policyengine_us/variables/gov/states/oh/odjfs/liheap/income/oh_liheap_countable_income.py index fc1e302e2cc..09f362f9ca1 100644 --- a/policyengine_us/variables/gov/states/oh/odjfs/liheap/income/oh_liheap_countable_income.py +++ b/policyengine_us/variables/gov/states/oh/odjfs/liheap/income/oh_liheap_countable_income.py @@ -33,7 +33,7 @@ def formula(spm_unit, period, parameters): income = 0 for sources in [p.sources.earned, p.sources.unearned]: for source in sources: - income = income + max_(person(source, period, options=[ADD]), 0) + income = income + max_(person(source, period), 0) # Guidelines E-2.2 (page 7) count the income of members 18 or older and # a minor's SS/SSDI paid to an adult payee; a child's SSI and other # income are not counted. The FY2026 plan (page 6) excludes only diff --git a/policyengine_us/variables/gov/states/ok/dhs/liheap/income/ok_liheap_countable_income.py b/policyengine_us/variables/gov/states/ok/dhs/liheap/income/ok_liheap_countable_income.py index 942ab1e7d3f..f207f2517b3 100644 --- a/policyengine_us/variables/gov/states/ok/dhs/liheap/income/ok_liheap_countable_income.py +++ b/policyengine_us/variables/gov/states/ok/dhs/liheap/income/ok_liheap_countable_income.py @@ -26,6 +26,6 @@ def formula(person, period, parameters): for source in p.unearned: # SSI is monthly; ADD annualizes it exactly once here. Other # listed streams are disjoint annual personal income inputs. - income = income + max_(add(person, period, [source], options=[ADD]), 0) + income = income + max_(add(person, period, [source]), 0) # TANF is an SPM aggregate and is counted separately once. return income diff --git a/policyengine_us/variables/gov/states/ok/dhs/liheap/income/ok_liheap_net_income.py b/policyengine_us/variables/gov/states/ok/dhs/liheap/income/ok_liheap_net_income.py index 659048d3d96..a6ab9e7712d 100644 --- a/policyengine_us/variables/gov/states/ok/dhs/liheap/income/ok_liheap_net_income.py +++ b/policyengine_us/variables/gov/states/ok/dhs/liheap/income/ok_liheap_net_income.py @@ -22,7 +22,7 @@ def formula(spm_unit, period, parameters): person("ok_liheap_income_deductions", period) * eligible ) receives_subsidy = spm_unit("ok_child_care_subsidies", period) > 0 - copay = spm_unit("ok_ccs_copay", period, options=[ADD]) + copay = spm_unit("ok_ccs_copay", period) # The SPM copay has no payer attribution. Deduct it once at net, # without assigning it to an ineligible tax unit before the gross # test. Gross income can be overstated when that person paid it. diff --git a/policyengine_us/variables/gov/states/pa/dhs/liheap/payment/pa_liheap_supplement.py b/policyengine_us/variables/gov/states/pa/dhs/liheap/payment/pa_liheap_supplement.py index 608d6524eb4..65d1d4b65a2 100644 --- a/policyengine_us/variables/gov/states/pa/dhs/liheap/payment/pa_liheap_supplement.py +++ b/policyengine_us/variables/gov/states/pa/dhs/liheap/payment/pa_liheap_supplement.py @@ -25,7 +25,7 @@ def formula(spm_unit, period, parameters): disability_assistance = ( (spm_unit.members("social_security_disability", period) > 0) | (spm_unit.members("disability_benefits", period) > 0) - | ((spm_unit.members("ssi", period, options=[ADD]) > 0) & disabled_or_blind) + | ((spm_unit.members("ssi", period) > 0) & disabled_or_blind) ) # Aggregate veterans benefits do not identify disability assistance. # A disability flag alone does not establish the required cash receipt. diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/income/sd_liheap_countable_income.py b/policyengine_us/variables/gov/states/sd/dss/liheap/income/sd_liheap_countable_income.py index 9d931a93543..1caab3a6dea 100644 --- a/policyengine_us/variables/gov/states/sd/dss/liheap/income/sd_liheap_countable_income.py +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/income/sd_liheap_countable_income.py @@ -35,7 +35,7 @@ def formula(spm_unit, period, parameters): unearned = 0 for source in p.sources.unearned: # ADD sums monthly sources such as ssi over the year. - unearned = unearned + max_(person(source, period, options=[ADD]), 0) + unearned = unearned + max_(person(source, period), 0) # Rule 67:15:01:18(12) excludes "Deductions from social security benefit # payments for Medicare", and the manual (page 28) counts Social Security # less the Part B and Part D premiums. medicare_part_b_premium is zero for diff --git a/policyengine_us/variables/gov/states/tn/thda/liheap/income/tn_liheap_countable_income.py b/policyengine_us/variables/gov/states/tn/thda/liheap/income/tn_liheap_countable_income.py index bb871b4808b..48d394afed9 100644 --- a/policyengine_us/variables/gov/states/tn/thda/liheap/income/tn_liheap_countable_income.py +++ b/policyengine_us/variables/gov/states/tn/thda/liheap/income/tn_liheap_countable_income.py @@ -32,7 +32,7 @@ def formula(spm_unit, period, parameters): earned = earned + max_(person(source, period), 0) unearned = 0 for source in p.sources.unearned: - unearned = unearned + max_(person(source, period, options=[ADD]), 0) + unearned = unearned + max_(person(source, period), 0) # Section 2.13 counts net income from Social Security, pensions # (including military retirement pay) and VA benefits excluding # Medicare premiums, and FY2026 plan Section 1.9 counts benefits diff --git a/policyengine_us/variables/gov/states/va/dss/eap/va_eap_countable_income.py b/policyengine_us/variables/gov/states/va/dss/eap/va_eap_countable_income.py index f900cc66e36..8003ef047d6 100644 --- a/policyengine_us/variables/gov/states/va/dss/eap/va_eap_countable_income.py +++ b/policyengine_us/variables/gov/states/va/dss/eap/va_eap_countable_income.py @@ -48,7 +48,7 @@ def formula(spm_unit, period, parameters): unearned = 0 for source in p.income.sources.unearned: # ADD sums the MONTH-defined ssi and va_ag over the year. - unearned = unearned + max_(person(source, period, options=[ADD]), 0) + unearned = unearned + max_(person(source, period), 0) # TANF the household receives: the take-up-gated aggregate, which # includes va_tanf. tanf = spm_unit("tanf", period) diff --git a/policyengine_us/variables/gov/states/wi/doa/wheap/eligibility/wi_wheap_categorically_eligible.py b/policyengine_us/variables/gov/states/wi/doa/wheap/eligibility/wi_wheap_categorically_eligible.py index 1f32b8c8bec..24eda8cb225 100644 --- a/policyengine_us/variables/gov/states/wi/doa/wheap/eligibility/wi_wheap_categorically_eligible.py +++ b/policyengine_us/variables/gov/states/wi/doa/wheap/eligibility/wi_wheap_categorically_eligible.py @@ -35,7 +35,7 @@ def formula(spm_unit, period, parameters): [~person("is_snap_excluded_member", month) for month in months], axis=0, ) - receives_snap = spm_unit("snap", period, options=[ADD]) > 0 + receives_snap = spm_unit("snap", period) > 0 foodshare_member = in_snap_unit & spm_unit.project(receives_snap) receives_w2_cash = spm_unit("tanf", period) > 0 w2_member = person("is_citizen_or_legal_immigrant", period) & spm_unit.project( diff --git a/policyengine_us/variables/gov/states/wi/doa/wheap/income/wi_wheap_countable_income.py b/policyengine_us/variables/gov/states/wi/doa/wheap/income/wi_wheap_countable_income.py index b7f5dfc8895..000eac6b738 100644 --- a/policyengine_us/variables/gov/states/wi/doa/wheap/income/wi_wheap_countable_income.py +++ b/policyengine_us/variables/gov/states/wi/doa/wheap/income/wi_wheap_countable_income.py @@ -32,7 +32,7 @@ def formula(spm_unit, period, parameters): ) + add(person, period, p.sources.rental) unearned = 0 for source in p.sources.unearned: - unearned = unearned + max_(person(source, period, options=[ADD]), 0) + unearned = unearned + max_(person(source, period), 0) # All withdrawals count from age 73. Regular withdrawals before 73 # also count but cannot be told apart from lump sums, which are # ignored as asset drawdowns. From 6bee540633ac0eb243faaadcd00e33f08c9db432 Mon Sep 17 00:00:00 2001 From: Ziming Date: Wed, 11 Nov 2026 22:13:19 -0500 Subject: [PATCH 07/11] Drop the options=[ADD] code-health check PolicyEngine/policyengine-core#647 fixes the cause in Core: it lets a Microsimulation accept the keyword an entity call passes with options=[ADD] or options=[DIVIDE]. Once that is released the option works again, so a check that bans it here would be wrong. The 26 formula files stay without the option, which was redundant and lets `liheap` run on today's Core. Co-Authored-By: Claude Fable 5.2 --- ...o_add_or_divide_options_in_entity_calls.py | 40 ------------------- 1 file changed, 40 deletions(-) delete mode 100644 policyengine_us/tests/code_health/test_no_add_or_divide_options_in_entity_calls.py diff --git a/policyengine_us/tests/code_health/test_no_add_or_divide_options_in_entity_calls.py b/policyengine_us/tests/code_health/test_no_add_or_divide_options_in_entity_calls.py deleted file mode 100644 index c0966626caf..00000000000 --- a/policyengine_us/tests/code_health/test_no_add_or_divide_options_in_entity_calls.py +++ /dev/null @@ -1,40 +0,0 @@ -import ast - -from policyengine_us.model_api import REPO - - -VARIABLES_ROOT = REPO / "variables" -PERIOD_OPTIONS = {"ADD", "DIVIDE"} - - -def passes_period_option(node: ast.AST) -> bool: - if not isinstance(node, ast.Call): - return False - return any( - keyword.arg == "options" - and any( - isinstance(child, ast.Name) and child.id in PERIOD_OPTIONS - for child in ast.walk(keyword.value) - ) - for keyword in node.keywords - ) - - -def test_entity_calls_do_not_pass_add_or_divide_options(): - violations = [] - - for file_name in sorted(VARIABLES_ROOT.glob("**/*.py")): - tree = ast.parse(file_name.read_text(), filename=str(file_name)) - for node in ast.walk(tree): - if passes_period_option(node): - violations.append(f"{file_name.relative_to(REPO)}:{node.lineno}") - - assert not violations, ( - "Read the variable with the bare period, or use add(entity, period, " - "sources): Core already sums a monthly variable over a year and " - "divides a yearly one over a month. options=[ADD] and " - "options=[DIVIDE] raise a TypeError in a Microsimulation, whose " - "calculate_add and calculate_divide do not accept the decode_enums " - "keyword that an entity call passes, so household tests pass while " - "the formula fails over microdata. Violations:\n" + "\n".join(violations) - ) From 6fd97808c9ef70e9a89015fa0229f3ab26642833 Mon Sep 17 00:00:00 2001 From: Ziming Date: Wed, 11 Nov 2026 22:54:08 -0500 Subject: [PATCH 08/11] Stop Alabama LIHEAP income limits at the federal maximum and drop a removed plan link Co-Authored-By: Claude Fable 5.2 --- changelog.d/al-liheap-cooling.fixed.md | 1 + .../al/adeca/liheap/cooling/fpg_year_lag.yaml | 2 - .../adeca/liheap/payment/iija_supplement.yaml | 2 - .../al_liheap_cooling_income_limit.yaml | 16 ++++++++ .../eligibility/al_liheap_income_limit.yaml | 9 +++- .../adeca/liheap/cooling/al_liheap_cooling.py | 5 +-- .../cooling/al_liheap_cooling_income_limit.py | 4 +- .../eligibility/al_liheap_income_limit.py | 14 ++++++- source_conflicts/candidates.yaml | 41 +++++++++++++++++++ 9 files changed, 81 insertions(+), 13 deletions(-) create mode 100644 changelog.d/al-liheap-cooling.fixed.md diff --git a/changelog.d/al-liheap-cooling.fixed.md b/changelog.d/al-liheap-cooling.fixed.md new file mode 100644 index 00000000000..a38b9cfb862 --- /dev/null +++ b/changelog.d/al-liheap-cooling.fixed.md @@ -0,0 +1 @@ +Stop Alabama LIHEAP income limits at the federal maximum, the greater of HHS's 60% state median income amount and 150% of the poverty guideline. diff --git a/policyengine_us/parameters/gov/states/al/adeca/liheap/cooling/fpg_year_lag.yaml b/policyengine_us/parameters/gov/states/al/adeca/liheap/cooling/fpg_year_lag.yaml index bcdb376276b..57264ea7b25 100644 --- a/policyengine_us/parameters/gov/states/al/adeca/liheap/cooling/fpg_year_lag.yaml +++ b/policyengine_us/parameters/gov/states/al/adeca/liheap/cooling/fpg_year_lag.yaml @@ -11,7 +11,5 @@ metadata: reference: - title: "Alabama FY2026 LIHEAP State Plan, sections 1.1 and 3.5 (May-September cooling season; adopt the 2026 guidelines upon release) #page 4,12" href: https://liheapch.acf.gov/docs/2026/state-plans/AL_Plan_2026.pdf#page=4 - - title: Alabama draft FY2027 LIHEAP State Plan, Attachment A (PY 2026 Payment Assistance Chart effective May 15, 2026; removed from the ADECA site by November 2026) - href: https://adeca.alabama.gov/wp-content/uploads/FY27-LIHEAP-State-Plan.pdf#page=53 - title: Alabama FY2027 LIHEAP State Plan, section 3.5 (the 2026 guidelines set the $1,995 one-person limit and $665, $1,331 and $1,995 band tops; adopt the 2027 guidelines upon release) href: https://liheapch.acf.gov/docs/2027/state-plans/AL_Plan_2027.pdf#page=12 diff --git a/policyengine_us/parameters/gov/states/al/adeca/liheap/payment/iija_supplement.yaml b/policyengine_us/parameters/gov/states/al/adeca/liheap/payment/iija_supplement.yaml index cf20564b110..32b43d01e4c 100644 --- a/policyengine_us/parameters/gov/states/al/adeca/liheap/payment/iija_supplement.yaml +++ b/policyengine_us/parameters/gov/states/al/adeca/liheap/payment/iija_supplement.yaml @@ -17,8 +17,6 @@ metadata: href: https://liheapch.acf.gov/docs/2026/state-plans/AL_Plan_2026.pdf#page=10 - title: HHS TAGGS award 2601ALLIEI, Alabama LIHEAP Infrastructure Supplement (FY2026 funding) href: https://taggs.hhs.gov/Detail/AwardDetail?arg_AwardNum=2601ALLIEI&arg_ProgOfficeCode=31 - - title: "Alabama draft FY2027 LIHEAP State Plan, sections 2.5 and 3.5 (no IIJA supplement) #page 9-10,12" - href: https://adeca.alabama.gov/wp-content/uploads/FY27-LIHEAP-State-Plan.pdf#page=9 - title: "Alabama FY2027 LIHEAP State Plan, sections 2.5 and 3.5 (no IIJA supplement) #page 9-10,12" href: https://liheapch.acf.gov/docs/2027/state-plans/AL_Plan_2027.pdf#page=9 - title: Infrastructure Investment and Jobs Act, Pub. L. 117-58, Division J, Title VII, 135 Stat. 1412 diff --git a/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_income_limit.yaml b/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_income_limit.yaml index 0bfdd4325f7..7acfacfdbf2 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_income_limit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_income_limit.yaml @@ -14,3 +14,19 @@ al_liheap_household_size: [1, 2, 3, 4, 5, 15] output: al_liheap_cooling_income_limit: [23_940, 32_460, 40_980, 49_500, 58_020, 143_220] + +# Summer 2025 used the chart effective February 17, 2025, on the 2025 guidelines of $15,650 plus $5,500 per +# additional person. Size 7: 48,650 x 1.5 / 12 = 6,081.25, printed as 6,081 (x 12 = 72,972). Size 8: 54,150 x 1.5 / 12 +# = 6,768.75, printed as 6,769 (x 12 = 81,228). 42 U.S.C. 8624(b)(2)(B) pays no household above the greater of 150% +# of the guideline (81,225 at size 8) and the FY2025 60% state median income amount (57,273 for four x 1.38 = +# 79,036), so the size 8 limit stops at 81,225. +- name: Case 2, a summer 2025 limit that rounds above the federal maximum stops there. + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: [AL, AL] + heating_type: [ELECTRICITY, ELECTRICITY] + has_cooling_expense: [true, true] + al_liheap_household_size: [7, 8] + output: + al_liheap_cooling_income_limit: [72_972, 81_225] diff --git a/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/eligibility/al_liheap_income_limit.yaml b/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/eligibility/al_liheap_income_limit.yaml index 40b7d744bbc..52a84b7ba28 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/eligibility/al_liheap_income_limit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/eligibility/al_liheap_income_limit.yaml @@ -1,7 +1,12 @@ # Source: ADECA FY2026 LIHEAP Policy Manual and its Appendix D Payment Assistance Chart. # Appendix D, sizes 1-15: each published monthly limit is annualized once; this isolated unit test supplies size directly. -- name: Case 1, all fifteen published monthly limits are reproduced annually. +# Each limit stops at the federal maximum of 42 U.S.C. 8624(b)(2)(B): the greater of 150% of the 2025 guideline +# ($15,650 plus $5,500 per additional person) and HHS's FY2026 60% state median income amount (59,220 for four, +# times the size factor, rounded down). The chart is $3 higher at sizes 10, 12 and 14: 8,144 x 12 = 97,728 against +# 65,150 x 1.5 = 97,725; 9,519 x 12 = 114,228 against 114,225; 10,894 x 12 = 130,728 against 130,725. At size 8 the +# chart's 6,769 x 12 = 81,228 is below the 60% amount (59,220 x 1.38 = 81,723), so it stays. +- name: Case 1, all fifteen published monthly limits are annualized and stop at the federal maximum. period: 2026 absolute_error_margin: 0.01 input: @@ -11,7 +16,7 @@ al_liheap_household_size: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] output: al_liheap_income_limit: [23_472, 31_728, 39_972, 48_228, 56_472, 64_728, 72_972, 81_228, 89_472, - 97_728, 105_972, 114_228, 122_472, 130_728, 138_972] + 97_725, 105_972, 114_225, 122_472, 130_725, 138_972] # The 2025 model year uses the 2024 guidelines of $15,060 and $20,440: 150% a month is $1,882.50 and $2,555, diff --git a/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling.py b/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling.py index 66eb449f8ec..33278c0614a 100644 --- a/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling.py +++ b/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling.py @@ -33,9 +33,8 @@ class al_liheap_cooling(Variable): "https://liheapch.acf.gov/docs/2025/state-plans/AL_Plan_2025.pdf#page=4", # The chart effective February 17, 2025, on the 2025 guidelines. "https://liheapch.acf.gov/docs/2026/benefits-matricies/AL_BenefitMatrix_Cooling_2026.pdf", - # The chart effective May 15, 2026, attached to the draft FY2027 plan. - "https://adeca.alabama.gov/wp-content/uploads/FY27-LIHEAP-State-Plan.pdf#page=53", - # Section 3.5: the 2026 guideline limit and band arithmetic. + # Section 3.5: the 2026 guideline limit and band arithmetic of the chart + # effective May 15, 2026. "https://liheapch.acf.gov/docs/2027/state-plans/AL_Plan_2027.pdf#page=12", ) diff --git a/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_income_limit.py b/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_income_limit.py index 4139c13015b..62b04763644 100644 --- a/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_income_limit.py +++ b/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_income_limit.py @@ -12,10 +12,8 @@ class al_liheap_cooling_income_limit(Variable): label = "Alabama LIHEAP regular cooling annualized income limit" defined_for = StateCode.AL reference = ( - # The chart effective May 15, 2026, attached to the draft FY2027 plan. - "https://adeca.alabama.gov/wp-content/uploads/FY27-LIHEAP-State-Plan.pdf#page=53", # Section 3.5: $15,960 x 1.5 = $23,940 a year, $1,995 a month for one - # person, on the 2026 guidelines. + # person, on the 2026 guidelines, as in the chart effective May 15, 2026. "https://liheapch.acf.gov/docs/2027/state-plans/AL_Plan_2027.pdf#page=12", # PDF pages 11-12 "https://liheapch.acf.gov/docs/2026/state-plans/AL_Plan_2026.pdf#page=11", diff --git a/policyengine_us/variables/gov/states/al/adeca/liheap/eligibility/al_liheap_income_limit.py b/policyengine_us/variables/gov/states/al/adeca/liheap/eligibility/al_liheap_income_limit.py index c6e63cb4eff..b3c4edb99e6 100644 --- a/policyengine_us/variables/gov/states/al/adeca/liheap/eligibility/al_liheap_income_limit.py +++ b/policyengine_us/variables/gov/states/al/adeca/liheap/eligibility/al_liheap_income_limit.py @@ -1,10 +1,13 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.hhs.hhs_smi import liheap_smi_limit from policyengine_us.variables.gov.hhs.tax_unit_fpg import fpg def al_liheap_income_limit_for_lag(spm_unit, period, parameters, year_lag): p = parameters(period).gov.states.al.adeca.liheap.eligibility + p_hhs = parameters(period).gov.hhs.liheap size = spm_unit("al_liheap_household_size", period) + state = spm_unit.household("state_code_str", period) state_group = spm_unit.household("state_group_str", period) guideline = fpg( max_(size, 1), @@ -15,7 +18,13 @@ def al_liheap_income_limit_for_lag(spm_unit, period, parameters, year_lag): ) monthly_limit = guideline * p.fpg_rate / MONTHS_IN_YEAR # Nearest-dollar, half-up rounding reproduces all 15 printed limits. - return np.floor(monthly_limit + 0.5) * MONTHS_IN_YEAR + rounded_limit = np.floor(monthly_limit + 0.5) * MONTHS_IN_YEAR + # 42 U.S.C. 8624(b)(2)(B) pays no household above the greater of 150% of the + # poverty guideline and 60% of state median income, in the amounts HHS + # publishes by household size. A published monthly amount that rounds above + # that maximum stops there. + smi_maximum = liheap_smi_limit(size, state, p_hhs.smi_limit, period, parameters) + return min_(rounded_limit, max_(guideline * p_hhs.fpg_limit, smi_maximum)) class al_liheap_income_limit(Variable): @@ -26,6 +35,9 @@ class al_liheap_income_limit(Variable): label = "Alabama LIHEAP annualized income limit" defined_for = StateCode.AL reference = ( + "https://www.law.cornell.edu/uscode/text/42/8624#b_2_B", + # FY2026 federal amounts + "https://acf.gov/sites/default/files/documents/ocs/COMM_LIHEAP_IM2025-02_SMIStateTable_Att4.pdf#page=1", "https://adeca.alabama.gov/wp-content/uploads/FY-2026-LIHEAP-Manual-1.pdf#page=57", "https://liheapch.acf.gov/docs/2026/state-plans/AL_Plan_2026.pdf#page=9", ) diff --git a/source_conflicts/candidates.yaml b/source_conflicts/candidates.yaml index 4e687a33ca4..b242d6ab757 100644 --- a/source_conflicts/candidates.yaml +++ b/source_conflicts/candidates.yaml @@ -368,6 +368,47 @@ candidates: note: 'PE-US follows the statute and current rule: estate_income counts signed in Alabama gross income, so a beneficiary''s loss passes through (gross_income_sources.yaml, read by al_agi.py). estate_income does not separate trusts from estates. Identify booklet editions repeating the obsolete restriction.' +- id: us-al:liheap-income-limit-above-federal-ceiling + program: liheap + years: + from: 2025 + to: 2026 + type: federal-fiscal + question: Can Alabama's whole-dollar monthly LIHEAP limits pay a household whose income exceeds both 150% of the + poverty guideline and HHS's 60% state median income amount? + sources: + - kind: statute + citation: 42 U.S.C. 8624(b)(2)(B) + url: https://www.law.cornell.edu/uscode/text/42/8624#b_2_B + - kind: guidance + citation: HHS LIHEAP IM 2025-02 Attachment 4, Mandatory Use in LIHEAP for FY26, Alabama, page 1 + url: https://acf.gov/sites/default/files/documents/ocs/COMM_LIHEAP_IM2025-02_SMIStateTable_Att4.pdf#page=1 + - kind: manual + citation: ADECA FY2026 LIHEAP Policy Manual, Appendix D Payment Assistance Chart, PDF page 57 + url: https://adeca.alabama.gov/wp-content/uploads/FY-2026-LIHEAP-Manual-1.pdf#page=57 + - kind: guidance + citation: ADECA Payment Assistance Chart effective February 17, 2025 (cooling) + url: https://liheapch.acf.gov/docs/2026/benefits-matricies/AL_BenefitMatrix_Cooling_2026.pdf + readings: + statute: Payments go only to households whose income does not exceed the greater of 150% of the poverty guideline + and 60% of the state median income that HHS publishes and adjusts for household size. + other: The charts round 150% of the monthly guideline to the nearest dollar, which puts the limit $3 a year above + the federal maximum at the large even household sizes where 150% of the guideline is the greater amount. + example: In FY2026, ten people with $97,728 of countable income ($8,144 a month) are within the printed heating limit + and would be paid, but exceed the $97,725 federal maximum and receive $0 in the PR. + pe_us: + follows: statute + code: + - policyengine_us/variables/gov/states/al/adeca/liheap/eligibility/al_liheap_income_limit.py + prs: + - https://github.com/PolicyEngine/policyengine-us/pull/10756 + state: ruled + note: 'Ruled under the 2026-10-19 statute-first rule. The PR stops each limit at the federal maximum. Heating FY2026, on + the 2025 guidelines: sizes 10, 12 and 14 are $3 lower ($97,728 and $97,725 at 10); size 8 keeps the printed $81,228 + because HHS''s 60% amount, $81,723, is the higher maximum. Summer 2025 cooling, on the 2025 guidelines, with the 60% + amounts computed from the $95,455 four-person median because HHS''s FY2025 table was not located: sizes 8, 10, 12 + and 14 are $3 lower ($81,228 and $81,225 at 8). The summer 2026 chart, on the 2026 guidelines, prints whole-dollar + amounts equal to 150% of the guideline, so nothing is lowered. Verify each later chart.' - id: us-al:liheap-insurance-payments program: liheap years: From f8a33ee0a35a8a70641be790ecd1b6c3ad624676 Mon Sep 17 00:00:00 2001 From: Ziming Date: Wed, 11 Nov 2026 23:22:33 -0500 Subject: [PATCH 09/11] Cite HHS's FY2025 median income table for the capped limits Co-Authored-By: Claude Opus 5.5 --- .../states/al/adeca/liheap/cooling/al_liheap_cooling.yaml | 4 ++-- .../liheap/cooling/al_liheap_cooling_income_limit.yaml | 3 ++- source_conflicts/candidates.yaml | 7 +++++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling.yaml b/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling.yaml index 1a7654d37ac..0089fd106a9 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling.yaml @@ -1,6 +1,6 @@ # Source: ADECA FY2026 LIHEAP Policy Manual (pages 9, 21-23), FY2026 plan sections 3.5-3.6 -# (pages 12-13), and the PY 2026 Payment Assistance Chart effective May 15, 2026 (draft FY2027 -# plan, page 53). Regular Cooling pays the chart's Electric column, minus one month of HUD +# (pages 12-13), and the PY 2026 Payment Assistance Chart effective May 15, 2026 (no live copy; +# the FY2027 plan, section 3.5, page 12, prints the same 2026-guideline limit and bands). Regular Cooling pays the chart's Electric column, minus one month of HUD # utility allowance, plus the $100 FY2026 IIJA supplement when the regular payment is positive. # 2026 monthly band tops (2026 guidelines): size 1 $665 / $1,331 / $1,995; size 4 $1,375 / # $2,751 / $4,125; size 5 $1,612 / $3,225 / $4,835. diff --git a/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_income_limit.yaml b/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_income_limit.yaml index 7acfacfdbf2..8e6f3378d2d 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_income_limit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_income_limit.yaml @@ -1,4 +1,5 @@ -# Source: ADECA PY 2026 Payment Assistance Chart effective May 15, 2026 (draft FY2027 plan, page 53), +# Source: ADECA PY 2026 Payment Assistance Chart effective May 15, 2026 (no live copy; the FY2027 plan, +# section 3.5, page 12, prints the same arithmetic), # built on the 2026 guidelines of $15,960 plus $5,680 per additional person. # 150% a month: size 1 = 15,960 x 1.5 / 12 = 1,995; size 2 = 21,640 x 1.5 / 12 = 2,705; diff --git a/source_conflicts/candidates.yaml b/source_conflicts/candidates.yaml index b242d6ab757..0f71e9be76f 100644 --- a/source_conflicts/candidates.yaml +++ b/source_conflicts/candidates.yaml @@ -389,6 +389,9 @@ candidates: - kind: guidance citation: ADECA Payment Assistance Chart effective February 17, 2025 (cooling) url: https://liheapch.acf.gov/docs/2026/benefits-matricies/AL_BenefitMatrix_Cooling_2026.pdf + - kind: guidance + citation: HHS LIHEAP IM 2024-02 Attachment 4, Mandatory Use in LIHEAP for FY25, Alabama, pages 1 and 4 + url: https://acf.gov/sites/default/files/documents/ocs/COMM_LIHEAP_IM%202024-02_Att4SMITable.pdf#page=1 readings: statute: Payments go only to households whose income does not exceed the greater of 150% of the poverty guideline and 60% of the state median income that HHS publishes and adjusts for household size. @@ -405,8 +408,8 @@ candidates: state: ruled note: 'Ruled under the 2026-10-19 statute-first rule. The PR stops each limit at the federal maximum. Heating FY2026, on the 2025 guidelines: sizes 10, 12 and 14 are $3 lower ($97,728 and $97,725 at 10); size 8 keeps the printed $81,228 - because HHS''s 60% amount, $81,723, is the higher maximum. Summer 2025 cooling, on the 2025 guidelines, with the 60% - amounts computed from the $95,455 four-person median because HHS''s FY2025 table was not located: sizes 8, 10, 12 + because HHS''s 60% amount, $81,723, is the higher maximum. Summer 2025 cooling, on the 2025 guidelines, with HHS''s FY2025 + amounts (LIHEAP IM 2024-02 Attachment 4): sizes 8, 10, 12 and 14 are $3 lower ($81,228 and $81,225 at 8). The summer 2026 chart, on the 2026 guidelines, prints whole-dollar amounts equal to 150% of the guideline, so nothing is lowered. Verify each later chart.' - id: us-al:liheap-insurance-payments From 4625f4e59ad10068e346d45bf5170289d03c6490 Mon Sep 17 00:00:00 2001 From: Ziming Date: Thu, 12 Nov 2026 12:09:13 -0500 Subject: [PATCH 10/11] Pin the Michigan, California and Illinois list entries and add the program prefix Co-Authored-By: Claude Opus 5.5 --- policyengine_us/programs.yaml | 3 +- .../baseline/gov/hhs/liheap/liheap.yaml | 48 +++++++++++++++++-- .../liheap/il_liheap_regular_heating.yaml | 2 + .../tests/test_aggregate_list_integrity.py | 46 +++++++++++++----- .../variables/gov/hhs/liheap/liheap.py | 6 +++ .../az_liheap_countable_unearned_income.py | 4 +- .../ia_liheap_countable_unearned_income.py | 3 +- .../eap/income/mn_eap_countable_income.py | 3 +- .../income/ok_liheap_countable_income.py | 2 +- .../income/sd_liheap_countable_income.py | 3 +- .../va/dss/eap/va_eap_countable_income.py | 3 +- 11 files changed, 98 insertions(+), 25 deletions(-) diff --git a/policyengine_us/programs.yaml b/policyengine_us/programs.yaml index 8cdd3f0d812..943411659ef 100644 --- a/policyengine_us/programs.yaml +++ b/policyengine_us/programs.yaml @@ -1037,6 +1037,7 @@ programs: status: partial coverage: AK, AR, AL, AZ, CA, CO, CT, DC, DE, GA, HI, Riverside County, MA, FL, IL, IA, ID, IN, KS, KY, LA, MD, ME, MI, MN, MO, MS, MT, NC, ND, NE, NH, NJ, NM, NV, NY, OH, OK, OR, PA, SC, SD, RI, TN, TX, UT, VA, VT, WA, WI, WV, WY variable: liheap + parameter_prefix: gov.hhs.liheap state_implementations: - state: AL status: partial @@ -1418,7 +1419,7 @@ programs: variable: wv_lieap parameter_prefix: gov.states.wv.dhhr.lieap verified_years: "2026-2027" - notes: FY2026-FY2027 regular heating payments for households of one to twenty qualified members. West Virginia has no regular cooling payment, and cooling help comes only through Emergency Cooling crisis payments and unit repair or replacement; crisis, equipment, weatherization and the utility discount are out of scope. + notes: FY2026-FY2027 regular heating payments for households of one to twenty qualified members. West Virginia's plans reserve a regular cooling payment of up to $2,000 if funds remain, which is not modeled; Emergency Cooling crisis payments, unit repair or replacement, weatherization and the utility discount are out of scope. - state: WY status: partial name: Wyoming LIEAP diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/liheap/liheap.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/liheap/liheap.yaml index 3d44d473075..cd2f6750493 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/liheap/liheap.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/liheap/liheap.yaml @@ -29,14 +29,15 @@ tx_ceap: 1_200 liheap: 1_200 -- name: Case 2, a Michigan household gets nothing because the Home Heating Credit is not in the total. - period: 2026 +- name: Case 2, a Michigan household's Home Heating Credit is not in the total. + period: 2025 absolute_error_margin: 0.01 input: people: person1: age: 41 - employment_income_before_lsr: 24_000 + # Part-time wages. + employment_income_before_lsr: 9_000 spm_units: spm_unit: members: [person1] @@ -49,8 +50,12 @@ members: [person1] state_code: MI output: - # Michigan's regular heating help is the refundable Home Heating Credit, - # which the list leaves out, and Michigan has no regular cooling payment. + # TY2025 MI-1040CR-7 standard credit: the one-exemption allowance of 604 + # less 3.5% of 9,000 household resources (315) is 289, paid at 60%: + # 173.40. The alternate credit is 70% of (1,020 - 11% of 9,000) = 21. + # Michigan's regular heating help is this refundable credit, which the + # list leaves out, and Michigan has no regular cooling payment. + mi_home_heating_credit: 173.4 liheap: 0 - name: Case 3, Illinois counts the regular heating payment but not the crisis amount. @@ -205,3 +210,36 @@ il_liheap_regular_heating: [0, 0, 425] # No household is paid by another state's program. liheap: [1_200, 529, 425] + +# California's ordinary liquid-fuel payment applies in Madera County only. Base +# benefit 411 for one person in burden group 4, plus 105 for the priority +# category at exactly 20 points (ca_liheap_covered_liquid_heating.yaml Case 5). +# A liquid-fuel payment rules out the utility payment the same year. +- name: Case 6, a Madera County propane household's liquid-fuel payment counts once. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + is_citizen_or_legal_immigrant: true + ssi: 0 + employment_income: 12_000 + spm_units: + spm_unit: + members: [person1] + heating_type: PROPANE + snap: 0 + tanf: 0 + ca_state_supplement: 0 + bottled_gas_expense: 12 + pre_subsidy_electricity_expense: 2_028 + households: + household: + members: [person1] + state_code: CA + county: MADERA_COUNTY_CA + output: + ca_liheap_covered_liquid_heating: 516 + ca_liheap_utility_heating: 0 + liheap: 516 diff --git a/policyengine_us/tests/policy/baseline/gov/states/il/dceo/liheap/il_liheap_regular_heating.yaml b/policyengine_us/tests/policy/baseline/gov/states/il/dceo/liheap/il_liheap_regular_heating.yaml index 879ce06541d..39d4e161b03 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/il/dceo/liheap/il_liheap_regular_heating.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/il/dceo/liheap/il_liheap_regular_heating.yaml @@ -63,3 +63,5 @@ # but the unit is ineligible, so the regular heating payment is zero. il_liheap_base_payment: 310 il_liheap_regular_heating: 0 + # The national total sums the regular heating payment, not the base. + liheap: 0 diff --git a/policyengine_us/tests/test_aggregate_list_integrity.py b/policyengine_us/tests/test_aggregate_list_integrity.py index 1f5ac0922ff..29c0492bf9c 100644 --- a/policyengine_us/tests/test_aggregate_list_integrity.py +++ b/policyengine_us/tests/test_aggregate_list_integrity.py @@ -177,13 +177,20 @@ def test_no_silent_removals_between_year_blocks(list_path): LIHEAP_PAYMENTS = "gov/hhs/liheap/regular_payments.yaml" -# Registry payment variables the national LIHEAP total leaves out on purpose. -LIHEAP_PAYMENTS_NOT_SUMMED = { - # il_liheap_regular_heating is summed instead: il_liheap also counts - # crisis assistance. - "il_liheap", +# Registry payment variables that reach the national LIHEAP total through +# other list entries, or not at all. Every other registry payment variable must +# be in the list itself. +LIHEAP_PAYMENT_STAND_INS = { + # il_liheap also counts crisis assistance; its regular part is summed. + "il_liheap": {"il_liheap_regular_heating"}, + # California's three regular components are exclusive by eligibility. + "ca_liheap_utility_heating": { + "ca_liheap_utility_heating", + "ca_liheap_covered_liquid_heating", + "ca_liheap_wood_heating", + }, # The Home Heating Credit already counts as a refundable state credit. - "mi_home_heating_credit", + "mi_home_heating_credit": set(), } @@ -191,9 +198,11 @@ def test_liheap_total_sums_every_registered_state_payment(variables): """A state payment in the registry must reach the national total. The LIHEAP registry entry names one variable per state implementation. - Each one that is a dollar amount belongs in the list `liheap` sums, so a - new state payment, or a list line deleted by mistake, cannot drop a state - from the total unnoticed. Eligibility flags carry no amount. + Each one that is a dollar amount belongs in the list `liheap` sums, or its + stand-ins in LIHEAP_PAYMENT_STAND_INS do, so a new state payment, or a list + line deleted or swapped by mistake, cannot drop a state from the total + unnoticed. A registry variable that a stand-in replaces must stay out of + the list, so it is not counted twice. Eligibility flags carry no amount. """ programs = yaml.safe_load((PACKAGE / "programs.yaml").read_text())["programs"] liheap = next(program for program in programs if program["id"] == "liheap") @@ -202,13 +211,26 @@ def test_liheap_total_sums_every_registered_state_payment(variables): for implementation in liheap["state_implementations"] if variables[implementation["variable"]].value_type is float } + listed = members_of(LIHEAP_PAYMENTS) missing = sorted( - payments - LIHEAP_PAYMENTS_NOT_SUMMED - members_of(LIHEAP_PAYMENTS) + entry + for payment in payments + for entry in LIHEAP_PAYMENT_STAND_INS.get(payment, {payment}) + if entry not in listed ) assert not missing, ( f"{LIHEAP_PAYMENTS} lacks these state payments from the LIHEAP " - f"registry entry: {missing}. Add each to the list, or record why it " - "is left out in LIHEAP_PAYMENTS_NOT_SUMMED in this test." + f"registry entry: {missing}. Add each to the list, or record what " + "stands in for it in LIHEAP_PAYMENT_STAND_INS in this test." + ) + double_counted = sorted( + payment + for payment, stand_ins in LIHEAP_PAYMENT_STAND_INS.items() + if payment not in stand_ins and payment in listed + ) + assert not double_counted, ( + f"{LIHEAP_PAYMENTS} lists {double_counted}, which " + "LIHEAP_PAYMENT_STAND_INS replaces or leaves out on purpose." ) diff --git a/policyengine_us/variables/gov/hhs/liheap/liheap.py b/policyengine_us/variables/gov/hhs/liheap/liheap.py index 558cc05ff3c..892a323b57d 100644 --- a/policyengine_us/variables/gov/hhs/liheap/liheap.py +++ b/policyengine_us/variables/gov/hhs/liheap/liheap.py @@ -6,6 +6,12 @@ class liheap(Variable): entity = SPMUnit definition_period = YEAR label = "LIHEAP regular payments" + documentation = ( + "Each state's regular LIHEAP heating and cooling payment. Leaves out " + "Michigan's Home Heating Credit, which counts as a refundable state " + "tax credit, and crisis, weatherization and equipment assistance. Not " + "yet counted in household benefits or net income." + ) unit = USD reference = "https://www.law.cornell.edu/uscode/text/42/8624#b_1_A" adds = "gov.hhs.liheap.regular_payments" diff --git a/policyengine_us/variables/gov/states/az/des/liheap/income/az_liheap_countable_unearned_income.py b/policyengine_us/variables/gov/states/az/des/liheap/income/az_liheap_countable_unearned_income.py index 80901bbf422..16b7c54dec8 100644 --- a/policyengine_us/variables/gov/states/az/des/liheap/income/az_liheap_countable_unearned_income.py +++ b/policyengine_us/variables/gov/states/az/des/liheap/income/az_liheap_countable_unearned_income.py @@ -31,8 +31,8 @@ def formula(person, period, parameters): # Gross Income is "The total Household income for all Household # members before taxes and other deductions" (page 4), and a loss is # not income, so each source is floored at zero and a rental loss - # cannot offset another source. ADD sums the MONTH-defined ssi over - # the year. + # cannot offset another source. Reading the MONTH-defined ssi over the + # year sums its months. unearned = 0 for source in p.sources.unearned: unearned = unearned + max_(person(source, period), 0) diff --git a/policyengine_us/variables/gov/states/ia/hhs/liheap/income/ia_liheap_countable_unearned_income.py b/policyengine_us/variables/gov/states/ia/hhs/liheap/income/ia_liheap_countable_unearned_income.py index 75aca29dcca..d00c1c81ff4 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/liheap/income/ia_liheap_countable_unearned_income.py +++ b/policyengine_us/variables/gov/states/ia/hhs/liheap/income/ia_liheap_countable_unearned_income.py @@ -48,7 +48,8 @@ def formula(person, period, parameters): # a loss in one source cannot offset another. unearned = 0 for source in p.sources.unearned: - # ADD sums monthly sources such as ssi over the year. + # Reading a monthly source such as ssi over the year sums its + # months. unearned = unearned + max_(person(source, period), 0) # Plan Section 1.9 (page 6) counts Social Security Administration # benefits "Excluding MediCare deduction". Section 6.10 (page 25) and diff --git a/policyengine_us/variables/gov/states/mn/commerce/eap/income/mn_eap_countable_income.py b/policyengine_us/variables/gov/states/mn/commerce/eap/income/mn_eap_countable_income.py index 71e882c897e..0fae08a51cb 100644 --- a/policyengine_us/variables/gov/states/mn/commerce/eap/income/mn_eap_countable_income.py +++ b/policyengine_us/variables/gov/states/mn/commerce/eap/income/mn_eap_countable_income.py @@ -34,7 +34,8 @@ def formula(spm_unit, period, parameters): earned_and_unemployment = where(student, 0, earned + unemployment) unearned = 0 for source in p.sources.unearned: - # ADD sums monthly sources such as ssi over the year. + # Reading a monthly source such as ssi over the year sums its + # months. unearned = unearned + max_(person(source, period), 0) # Retirement payments count from age 59 1/2. Early benefits before that # age count only when not penalized; no input shows whether a withdrawal diff --git a/policyengine_us/variables/gov/states/ok/dhs/liheap/income/ok_liheap_countable_income.py b/policyengine_us/variables/gov/states/ok/dhs/liheap/income/ok_liheap_countable_income.py index f207f2517b3..6dbff35ee8b 100644 --- a/policyengine_us/variables/gov/states/ok/dhs/liheap/income/ok_liheap_countable_income.py +++ b/policyengine_us/variables/gov/states/ok/dhs/liheap/income/ok_liheap_countable_income.py @@ -24,7 +24,7 @@ def formula(person, period, parameters): # Each source is floored at zero, so a loss in one source cannot # offset another. for source in p.unearned: - # SSI is monthly; ADD annualizes it exactly once here. Other + # SSI is monthly; add() sums its months exactly once here. Other # listed streams are disjoint annual personal income inputs. income = income + max_(add(person, period, [source]), 0) # TANF is an SPM aggregate and is counted separately once. diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/income/sd_liheap_countable_income.py b/policyengine_us/variables/gov/states/sd/dss/liheap/income/sd_liheap_countable_income.py index 1caab3a6dea..9281a0acf0d 100644 --- a/policyengine_us/variables/gov/states/sd/dss/liheap/income/sd_liheap_countable_income.py +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/income/sd_liheap_countable_income.py @@ -34,7 +34,8 @@ def formula(spm_unit, period, parameters): earned = earned + max_(person(source, period), 0) unearned = 0 for source in p.sources.unearned: - # ADD sums monthly sources such as ssi over the year. + # Reading a monthly source such as ssi over the year sums its + # months. unearned = unearned + max_(person(source, period), 0) # Rule 67:15:01:18(12) excludes "Deductions from social security benefit # payments for Medicare", and the manual (page 28) counts Social Security diff --git a/policyengine_us/variables/gov/states/va/dss/eap/va_eap_countable_income.py b/policyengine_us/variables/gov/states/va/dss/eap/va_eap_countable_income.py index 8003ef047d6..f9db707156e 100644 --- a/policyengine_us/variables/gov/states/va/dss/eap/va_eap_countable_income.py +++ b/policyengine_us/variables/gov/states/va/dss/eap/va_eap_countable_income.py @@ -47,7 +47,8 @@ def formula(spm_unit, period, parameters): # offset other income. unearned = 0 for source in p.income.sources.unearned: - # ADD sums the MONTH-defined ssi and va_ag over the year. + # Reading the MONTH-defined ssi and va_ag over the year sums + # their months. unearned = unearned + max_(person(source, period), 0) # TANF the household receives: the take-up-gated aggregate, which # includes va_tanf. From eb13ec6206195ca74edfe2255c8117c6575e2865 Mon Sep 17 00:00:00 2001 From: Ziming Date: Fri, 13 Nov 2026 15:25:35 -0500 Subject: [PATCH 11/11] Fix issues from review: 2025 heating cap sizes, fuel condition, pinning cases Addresses DTrim99's round-1 review; upstream main merged first. Co-Authored-By: Claude Opus 5.5 --- .../al/adeca/liheap/cooling/fpg_year_lag.yaml | 6 +- .../liheap/cooling/al_liheap_cooling.yaml | 31 ++++++++-- .../cooling/al_liheap_cooling_eligible.yaml | 56 +++++++++++++++++++ .../eligibility/al_liheap_income_limit.yaml | 18 ++++++ .../adeca/liheap/cooling/al_liheap_cooling.py | 6 +- .../cooling/al_liheap_cooling_eligible.py | 12 ++-- .../cooling/al_liheap_cooling_income_limit.py | 9 +-- .../eligibility/al_liheap_income_limit.py | 2 +- source_conflicts/candidates.yaml | 16 ++++-- 9 files changed, 129 insertions(+), 27 deletions(-) diff --git a/policyengine_us/parameters/gov/states/al/adeca/liheap/cooling/fpg_year_lag.yaml b/policyengine_us/parameters/gov/states/al/adeca/liheap/cooling/fpg_year_lag.yaml index 57264ea7b25..a9169a58452 100644 --- a/policyengine_us/parameters/gov/states/al/adeca/liheap/cooling/fpg_year_lag.yaml +++ b/policyengine_us/parameters/gov/states/al/adeca/liheap/cooling/fpg_year_lag.yaml @@ -1,7 +1,9 @@ description: Alabama uses federal poverty guidelines from this many years before the benefit year for regular cooling assistance under the Low Income Home Energy Assistance Program. # The May-September cooling season runs on the chart ADECA updates to the -# calendar year's guidelines (May 15, 2026 for summer 2026). The model applies -# that chart to the whole season, including May 1-14 applications. +# calendar year's guidelines once they are released. The May 15, 2026 effective +# date of the summer 2026 chart is reported from an attachment to the draft +# FY2027 plan that ADECA later removed; no live copy was found. The model applies +# the updated chart to the whole season, including applications before the update. values: 2025-10-01: 0 metadata: diff --git a/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling.yaml b/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling.yaml index 0089fd106a9..3b61f7d8b4f 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling.yaml @@ -176,9 +176,11 @@ output: al_liheap_cooling: 540 -# Case 1's worker with the heating fuel left unspecified. The standing LIHEAP rule pays -# nothing when heating_type is UNSPECIFIED. -- name: Case 10, an unspecified heating fuel pays no cooling. +# Case 1's worker with the heating fuel left unspecified. Regular Cooling pays the Electric +# column whatever the heating fuel, and plan item 3.2 adds no cooling requirement, so band 1 +# pays $440 + $100 = $540. +# Was false and $0, now true and $540: cooling eligibility no longer requires a reported heating fuel. +- name: Case 10, an unspecified heating fuel still receives the cooling payment. period: 2026 absolute_error_margin: 0.01 input: @@ -189,8 +191,8 @@ has_cooling_expense: true hud_utility_allowance: 0 output: - al_liheap_cooling_eligible: false - al_liheap_cooling: 0 + al_liheap_cooling_eligible: true + al_liheap_cooling: 540 # FY2025 plan: cooling runs May 1 to September 30, 2025, pays $320 to $520, and adds the # $100 IIJA supplement. The chart effective February 17, 2025 uses the 2025 guidelines: @@ -211,3 +213,22 @@ output: al_liheap_cooling_income_limit: 23_472 al_liheap_cooling: 510 + +# 21 hours a week at the $7.25 federal minimum wage (Alabama has none): 21 x 7.25 x 52 = +# $7,917 a year, $659.75 a month. On the cooling limit, band 1 runs to +# floor(1,995 / 3 + 0.5) = $665, so Electric $440 + $100 = $540. On the heating limit of +# $1,956, band 1 ends at floor(1,956 / 3 + 0.5) = $652, so the heating band is 2, which +# would pay $410 + $100 = $510. +- name: Case 12, the cooling payment reads its band from the cooling limit. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: AL + age: 45 + employment_income_before_lsr: 7_917 + heating_type: ELECTRICITY + has_cooling_expense: true + hud_utility_allowance: 0 + output: + al_liheap_income_band: 2 + al_liheap_cooling: 540 diff --git a/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_eligible.yaml index 4adf975a1c7..548bcb4c8f2 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_eligible.yaml @@ -94,3 +94,59 @@ output: al_liheap_cooling_eligible: [true, false] al_liheap_cooling: [540, 0] + +# Manual section 4 requires at least one citizen or qualified household member, as for +# heating. An undocumented adult alone gives household size 0, so the unit is not eligible +# although its $628.33 a month and its cooling cost would qualify. +- name: Case 7, a unit with no qualified member is not cooling eligible. + period: 2026 + input: + state_code: AL + age: 45 + immigration_status: UNDOCUMENTED + employment_income_before_lsr: 7_540 + heating_type: ELECTRICITY + has_cooling_expense: true + output: + al_liheap_household_size: 0 + al_liheap_cooling_eligible: false + +# Two nonsubsidized units without a separate cooling cost, each missing one condition of the +# rent route. Unit 1 has its utilities in the rent but pays no rent. Unit 2 pays the FY2026 +# HUD Fair Market Rent for a one-bedroom unit in Jefferson County, $1,155 a month (x12 = +# 13,860), but pays its own utilities, the default. +- name: Case 8, the rent route needs both utilities in the rent and a positive rent. + period: 2026 + input: + state_code: [AL, AL] + age: [45, 45] + employment_income_before_lsr: [7_540, 7_540] + heating_type: [NATURAL_GAS, NATURAL_GAS] + has_cooling_expense: [false, false] + tenant_pays_utilities: [false, true] + rent: [0, 13_860] + output: + al_liheap_cooling_eligible: [false, false] + +# Case 6's worker in public housing, with no other housing assistance, energy costs in the +# rent and no out-of-pocket cooling cost. Public housing is subsidized housing, and under +# manual 6.1.1 a subsidized tenant who pays nothing out of pocket is not eligible. The rent +# is the public housing tenant payment of 30% of adjusted income (24 CFR 5.628), which for a +# single adult without deductions is 0.3 x 7,540 = 2,262. +- name: Case 9, a public housing tenant with energy costs in the rent is not cooling eligible. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: AL + age: 45 + employment_income_before_lsr: 7_540 + heating_type: NATURAL_GAS + has_cooling_expense: false + tenant_pays_utilities: false + rent: 2_262 + receives_housing_assistance: false + is_in_public_housing: true + hud_utility_allowance: 0 + output: + al_liheap_cooling_eligible: false + al_liheap_cooling: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/eligibility/al_liheap_income_limit.yaml b/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/eligibility/al_liheap_income_limit.yaml index 52a84b7ba28..e15a49cc6dd 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/eligibility/al_liheap_income_limit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/al/adeca/liheap/eligibility/al_liheap_income_limit.yaml @@ -30,3 +30,21 @@ al_liheap_household_size: [1, 2] output: al_liheap_income_limit: [22_596, 30_660] + +# ADECA's PY 2025 chart and Summarized Eligibility Requirements effective October 1, 2024 to February 16, 2025, on +# the 2024 guidelines ($15,060 plus $5,380 per additional person), print 5,918, 6,590, 7,263, 7,935, 8,608, 9,280, +# 9,953, 10,625 and 11,298 a month for sizes 7-15. At odd sizes 150% a month ends in $0.50 and rounds up, so the +# printed limit is $6 a year above 150% of the guideline: size 9 prints 7,263 x 12 = 87,156 against 58,100 x 1.5 = +# 87,150. From size 8, 150% of the guideline exceeds HHS's FY2025 60% state median income amount (57,273 for four, +# times the size factor, rounded down: 79,036 at size 8 against 1.5 x 52,720 = 79,080, and 80,754 at size 9), so the +# odd sizes 9, 11, 13 and 15 stop at 87,150, 103,290, 119,430 and 135,570. At size 7 the 60% amount (57,273 x 1.35 = 77,318) is the higher maximum, so the printed 71,016 stays. +# Even sizes print whole dollars. Historical estimate; 2025 is not a verified year. +- name: Case 3, the 2025 limits at sizes 9, 11, 13 and 15 stop at the federal maximum. + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: [AL, AL, AL, AL, AL, AL, AL, AL, AL] + heating_type: [PROPANE, PROPANE, PROPANE, PROPANE, PROPANE, PROPANE, PROPANE, PROPANE, PROPANE] + al_liheap_household_size: [7, 8, 9, 10, 11, 12, 13, 14, 15] + output: + al_liheap_income_limit: [71_016, 79_080, 87_150, 95_220, 103_290, 111_360, 119_430, 127_500, 135_570] diff --git a/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling.py b/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling.py index 33278c0614a..83094d7cbfa 100644 --- a/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling.py +++ b/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling.py @@ -26,15 +26,15 @@ class al_liheap_cooling(Variable): reference = ( # PDF pages 9, 21-23 "https://adeca.alabama.gov/wp-content/uploads/FY-2026-LIHEAP-Manual-1.pdf#page=9", - # PDF pages 12-13 + # PDF pages 12-13: section 3.5 adopts the 2026 guidelines upon release. "https://liheapch.acf.gov/docs/2026/state-plans/AL_Plan_2026.pdf#page=12", # PDF pages 4, 12-13: the FY2025 cooling season, IIJA supplement and # $320 to $520 range. "https://liheapch.acf.gov/docs/2025/state-plans/AL_Plan_2025.pdf#page=4", # The chart effective February 17, 2025, on the 2025 guidelines. "https://liheapch.acf.gov/docs/2026/benefits-matricies/AL_BenefitMatrix_Cooling_2026.pdf", - # Section 3.5: the 2026 guideline limit and band arithmetic of the chart - # effective May 15, 2026. + # Section 3.5: the PY2027 chart, from October 1, 2026, shows the limit + # and band arithmetic on the 2026 guidelines. "https://liheapch.acf.gov/docs/2027/state-plans/AL_Plan_2027.pdf#page=12", ) diff --git a/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_eligible.py b/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_eligible.py index 84e3a716cc3..821849400c9 100644 --- a/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_eligible.py +++ b/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_eligible.py @@ -25,15 +25,15 @@ def formula(spm_unit, period, parameters): subsidized = spm_unit("receives_housing_assistance", period) | ( spm_unit.household("is_in_public_housing", period) ) + # Heating's rent route reads heat_expense_included_in_rent instead, so a + # renter with energy costs in the rent also sets tenant_pays_utilities + # to false for this route. utilities_in_rent = ~spm_unit.household("tenant_pays_utilities", period) & ( add(spm_unit, period, ["rent"]) > 0 ) responsible = spm_unit("has_cooling_expense", period) | ( utilities_in_rent & ~subsidized ) - # The heating fuel must be reported: UNSPECIFIED pays nothing, while - # NONE and every known fuel qualify, since cooling pays the Electric - # column whatever the heating fuel. - heating_type = spm_unit("heating_type", period) - fuel_reported = heating_type != heating_type.possible_values.UNSPECIFIED - return (size > 0) & (income <= limit) & responsible & fuel_reported + # Cooling pays the Electric column whatever the heating fuel, so + # heating_type, including UNSPECIFIED, does not affect eligibility. + return (size > 0) & (income <= limit) & responsible diff --git a/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_income_limit.py b/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_income_limit.py index 62b04763644..92b0ec48476 100644 --- a/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_income_limit.py +++ b/policyengine_us/variables/gov/states/al/adeca/liheap/cooling/al_liheap_cooling_income_limit.py @@ -12,11 +12,12 @@ class al_liheap_cooling_income_limit(Variable): label = "Alabama LIHEAP regular cooling annualized income limit" defined_for = StateCode.AL reference = ( - # Section 3.5: $15,960 x 1.5 = $23,940 a year, $1,995 a month for one - # person, on the 2026 guidelines, as in the chart effective May 15, 2026. + # Section 3.5: the PY2027 chart, from October 1, 2026, shows the 2026 + # guideline arithmetic, $15,960 x 1.5 = $23,940 a year or $1,995 a + # month for one person. "https://liheapch.acf.gov/docs/2027/state-plans/AL_Plan_2027.pdf#page=12", - # PDF pages 11-12 - "https://liheapch.acf.gov/docs/2026/state-plans/AL_Plan_2026.pdf#page=11", + # Section 3.5: ADECA adopts the 2026 guidelines upon release. + "https://liheapch.acf.gov/docs/2026/state-plans/AL_Plan_2026.pdf#page=12", ) def formula(spm_unit, period, parameters): diff --git a/policyengine_us/variables/gov/states/al/adeca/liheap/eligibility/al_liheap_income_limit.py b/policyengine_us/variables/gov/states/al/adeca/liheap/eligibility/al_liheap_income_limit.py index b3c4edb99e6..3c01905790a 100644 --- a/policyengine_us/variables/gov/states/al/adeca/liheap/eligibility/al_liheap_income_limit.py +++ b/policyengine_us/variables/gov/states/al/adeca/liheap/eligibility/al_liheap_income_limit.py @@ -36,7 +36,7 @@ class al_liheap_income_limit(Variable): defined_for = StateCode.AL reference = ( "https://www.law.cornell.edu/uscode/text/42/8624#b_2_B", - # FY2026 federal amounts + # FY2026 federal amounts, PDF pages 1, 4 "https://acf.gov/sites/default/files/documents/ocs/COMM_LIHEAP_IM2025-02_SMIStateTable_Att4.pdf#page=1", "https://adeca.alabama.gov/wp-content/uploads/FY-2026-LIHEAP-Manual-1.pdf#page=57", "https://liheapch.acf.gov/docs/2026/state-plans/AL_Plan_2026.pdf#page=9", diff --git a/source_conflicts/candidates.yaml b/source_conflicts/candidates.yaml index 2e6d1dd79fd..56861541b15 100644 --- a/source_conflicts/candidates.yaml +++ b/source_conflicts/candidates.yaml @@ -381,7 +381,7 @@ candidates: citation: 42 U.S.C. 8624(b)(2)(B) url: https://www.law.cornell.edu/uscode/text/42/8624#b_2_B - kind: guidance - citation: HHS LIHEAP IM 2025-02 Attachment 4, Mandatory Use in LIHEAP for FY26, Alabama, page 1 + citation: HHS LIHEAP IM 2025-02 Attachment 4, Mandatory Use in LIHEAP for FY26, Alabama, pages 1 and 4 url: https://acf.gov/sites/default/files/documents/ocs/COMM_LIHEAP_IM2025-02_SMIStateTable_Att4.pdf#page=1 - kind: manual citation: ADECA FY2026 LIHEAP Policy Manual, Appendix D Payment Assistance Chart, PDF page 57 @@ -389,14 +389,17 @@ candidates: - kind: guidance citation: ADECA Payment Assistance Chart effective February 17, 2025 (cooling) url: https://liheapch.acf.gov/docs/2026/benefits-matricies/AL_BenefitMatrix_Cooling_2026.pdf + - kind: guidance + citation: ADECA LIHEAP Summarized Eligibility Requirements, Effective October 1, 2024 - February 16, 2025, page 1 + url: https://adeca.alabama.gov/wp-content/uploads/LIHEAP-Summarized-Eligibility-Requirements.pdf#page=1 - kind: guidance citation: HHS LIHEAP IM 2024-02 Attachment 4, Mandatory Use in LIHEAP for FY25, Alabama, pages 1 and 4 url: https://acf.gov/sites/default/files/documents/ocs/COMM_LIHEAP_IM%202024-02_Att4SMITable.pdf#page=1 readings: statute: Payments go only to households whose income does not exceed the greater of 150% of the poverty guideline and 60% of the state median income that HHS publishes and adjusts for household size. - other: The charts round 150% of the monthly guideline to the nearest dollar, which puts the limit $3 a year above - the federal maximum at the large even household sizes where 150% of the guideline is the greater amount. + other: The charts round 150% of the monthly guideline to the nearest dollar, which puts the limit $3 or $6 a year + above the federal maximum at the large household sizes where 150% of the guideline is the greater amount. example: In FY2026, ten people with $97,728 of countable income ($8,144 a month) are within the printed heating limit and would be paid, but exceed the $97,725 federal maximum and receive $0 in the PR. pe_us: @@ -408,9 +411,10 @@ candidates: state: ruled note: 'Ruled under the 2026-10-19 statute-first rule. The PR stops each limit at the federal maximum. Heating FY2026, on the 2025 guidelines: sizes 10, 12 and 14 are $3 lower ($97,728 and $97,725 at 10); size 8 keeps the printed $81,228 - because HHS''s 60% amount, $81,723, is the higher maximum. Summer 2025 cooling, on the 2025 guidelines, with HHS''s FY2025 - amounts (LIHEAP IM 2024-02 Attachment 4): sizes 8, 10, 12 - and 14 are $3 lower ($81,228 and $81,225 at 8). The summer 2026 chart, on the 2026 guidelines, prints whole-dollar + because HHS''s 60% amount, $81,723, is the higher maximum. Heating FY2025, on the 2024 guidelines of the October 1, 2024 + to February 16, 2025 chart, with HHS''s FY2025 amounts (LIHEAP IM 2024-02 Attachment 4): sizes 9, 11, 13 and 15 are + $6 lower ($87,156 and $87,150 at 9). Summer 2025 cooling, on the 2025 guidelines, with the same FY2025 amounts: sizes + 8, 10, 12 and 14 are $3 lower ($81,228 and $81,225 at 8). The summer 2026 chart, on the 2026 guidelines, prints whole-dollar amounts equal to 150% of the guideline, so nothing is lowered. Verify each later chart.' - id: us-al:liheap-insurance-payments program: liheap