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 2d610db3bffd84b2d770e5916c2100096f16000b Mon Sep 17 00:00:00 2001 From: Ziming Date: Sun, 8 Nov 2026 22:05:59 -0500 Subject: [PATCH 02/11] Add Arkansas LIHEAP summer regular cooling assistance Co-Authored-By: Claude Opus 5.5 --- changelog.d/ar-liheap-cooling.added.md | 1 + .../liheap/payment/household_size_group.yaml | 2 +- .../ar/ee/aeo/liheap/payment/income_band.yaml | 2 +- .../liheap/payment/matrix/electricity.yaml | 2 +- policyengine_us/programs.yaml | 10 +- .../aeo/liheap/cooling/ar_liheap_cooling.yaml | 157 ++++++++++++++++++ .../cooling/ar_liheap_cooling_eligible.yaml | 89 ++++++++++ .../aeo/liheap/cooling/ar_liheap_cooling.py | 33 ++++ .../cooling/ar_liheap_cooling_eligible.py | 38 +++++ 9 files changed, 330 insertions(+), 4 deletions(-) create mode 100644 changelog.d/ar-liheap-cooling.added.md create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling_eligible.yaml create mode 100644 policyengine_us/variables/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling.py create mode 100644 policyengine_us/variables/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling_eligible.py diff --git a/changelog.d/ar-liheap-cooling.added.md b/changelog.d/ar-liheap-cooling.added.md new file mode 100644 index 00000000000..9dd31de0604 --- /dev/null +++ b/changelog.d/ar-liheap-cooling.added.md @@ -0,0 +1 @@ +Add Arkansas LIHEAP summer regular cooling assistance. diff --git a/policyengine_us/parameters/gov/states/ar/ee/aeo/liheap/payment/household_size_group.yaml b/policyengine_us/parameters/gov/states/ar/ee/aeo/liheap/payment/household_size_group.yaml index 515b1313e43..56399f52bdb 100644 --- a/policyengine_us/parameters/gov/states/ar/ee/aeo/liheap/payment/household_size_group.yaml +++ b/policyengine_us/parameters/gov/states/ar/ee/aeo/liheap/payment/household_size_group.yaml @@ -1,4 +1,4 @@ -description: Arkansas uses these household size group boundaries for the regular heating benefit under the Low Income Home Energy Assistance Program. +description: Arkansas uses these household size group boundaries for the winter and summer regular benefits under the Low Income Home Energy Assistance Program. brackets: - threshold: 2024-10-01: 0 diff --git a/policyengine_us/parameters/gov/states/ar/ee/aeo/liheap/payment/income_band.yaml b/policyengine_us/parameters/gov/states/ar/ee/aeo/liheap/payment/income_band.yaml index eb56a8db58a..8e8872ec56a 100644 --- a/policyengine_us/parameters/gov/states/ar/ee/aeo/liheap/payment/income_band.yaml +++ b/policyengine_us/parameters/gov/states/ar/ee/aeo/liheap/payment/income_band.yaml @@ -1,4 +1,4 @@ -description: Arkansas uses these monthly countable income band boundaries for the regular heating benefit under the Low Income Home Energy Assistance Program. +description: Arkansas uses these monthly countable income band boundaries for the winter and summer regular benefits under the Low Income Home Energy Assistance Program. # Published lower bounds are inclusive and apply to monthly countable income # rounded to the nearest whole dollar. Bands with one threshold entry apply in # both FY2025 and FY2026; the top five lower bounds changed in FY2026. diff --git a/policyengine_us/parameters/gov/states/ar/ee/aeo/liheap/payment/matrix/electricity.yaml b/policyengine_us/parameters/gov/states/ar/ee/aeo/liheap/payment/matrix/electricity.yaml index 634e598cf92..f0e249395cd 100644 --- a/policyengine_us/parameters/gov/states/ar/ee/aeo/liheap/payment/matrix/electricity.yaml +++ b/policyengine_us/parameters/gov/states/ar/ee/aeo/liheap/payment/matrix/electricity.yaml @@ -1,4 +1,4 @@ -description: Arkansas provides this regular heating benefit for households using electricity under the Low Income Home Energy Assistance Program. +description: Arkansas provides this regular benefit for households heating with electricity in winter and for every household in summer under the Low Income Home Energy Assistance Program. # Cells with one entry apply in both FY2025 and FY2026. 1: diff --git a/policyengine_us/programs.yaml b/policyengine_us/programs.yaml index a7cef450f8b..8d950c144f3 100644 --- a/policyengine_us/programs.yaml +++ b/policyengine_us/programs.yaml @@ -1011,7 +1011,15 @@ programs: full_name: Arkansas Low Income Home Energy Assistance Program variable: ar_liheap parameter_prefix: gov.states.ar.ee.aeo.liheap - notes: FY2025-FY2026 regular heating matrices and income limits are checked; income rules follow the FFY 2025 manual (no FY2026 manual was found), and other years are unverified. Crisis, cooling, weatherization and equipment assistance are out of scope. + notes: FY2025-FY2026 regular heating matrices and income limits are checked; income rules follow the FFY 2025 manual (no FY2026 manual was found), and other years are unverified. Summer cooling is the separate Arkansas LIHEAP cooling entry; crisis, weatherization and equipment assistance are out of scope. + - state: AR + status: partial + name: Arkansas LIHEAP cooling + full_name: Arkansas Low Income Home Energy Assistance Program summer regular cooling assistance + variable: ar_liheap_cooling + parameter_prefix: gov.states.ar.ee.aeo.liheap + verified_years: "2025-2026" + notes: FY2025-FY2026 summer regular benefit from the Electricity matrix, paid once per summer on the electric bill; the planned FY2027 increase has no posted matrix. Summer crisis, equipment and electricity included in rent are out of scope. - state: AZ status: partial name: Arizona LIHEAP diff --git a/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling.yaml b/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling.yaml new file mode 100644 index 00000000000..9582769d361 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling.yaml @@ -0,0 +1,157 @@ +# Arkansas summer regular (cooling) benefit: FFY 2025 manual section 6.1 (p78) uses the +# Electricity matrix "for both Winter and Summer Regular Assistance". Wages are full time +# at the $11.00 Arkansas minimum wage: 11 x 40 x 52 = 22_880. The 20% earned disregard +# gives 22_880 x 0.8 = 18_304; MCI 1_525.33 rounds to 1_525 (x12 = 18_300). The $1,800 +# electric bill only shows an electric account; it does not change the grant. TEA is +# pinned to 0, as in the heating tests. + +# FY2026: size three uses the 3&4 column; the size-three limit is 3_792 x 12 = 45_504. +# Electric row 1510-1569 pays 114 for 3&4. +- name: Case 1, a minimum wage parent with two children receives the 3 and 4 person summer grant. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 32 + immigration_status: CITIZEN + ssi: 0 + employment_income_before_lsr: 22_880 + person2: + age: 9 + immigration_status: CITIZEN + ssi: 0 + person3: + age: 6 + immigration_status: CITIZEN + ssi: 0 + spm_units: + spm_unit: + members: [person1, person2, person3] + heating_type: NATURAL_GAS + has_cooling_expense: true + pre_subsidy_electricity_expense: 1_800 + ar_tea: 0 + households: + household: + members: [person1, person2, person3] + state_code: AR + output: + ar_liheap_household_size: 3 + ar_liheap_countable_income: 18_300 + ar_liheap_income_limit: 45_504 + ar_liheap_cooling_eligible: true + ar_liheap_cooling: 114 + +# FY2026: zero income falls in the 0-69 row; size five uses the 5 & UP column, which pays +# the 344 maximum (plan section 3.6, p13). The size-five limit is 5_236 x 12 = 62_832. +- name: Case 2, a five person household with no income receives the maximum summer grant. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + ssi: 0 + person2: + age: 39 + immigration_status: CITIZEN + ssi: 0 + person3: + age: 12 + immigration_status: CITIZEN + ssi: 0 + person4: + age: 8 + immigration_status: CITIZEN + ssi: 0 + person5: + age: 4 + immigration_status: CITIZEN + ssi: 0 + spm_units: + spm_unit: + members: [person1, person2, person3, person4, person5] + heating_type: PROPANE + has_cooling_expense: true + pre_subsidy_electricity_expense: 2_400 + ar_tea: 0 + households: + household: + members: [person1, person2, person3, person4, person5] + state_code: AR + output: + ar_liheap_household_size: 5 + ar_liheap_countable_income: 0 + ar_liheap_income_limit: 62_832 + ar_liheap_cooling: 344 + +# Boundary: printed lower bounds are inclusive. MCI 69 (828 / 12) is the top of the 0-69 +# row, which pays 242 for 1&2; MCI 70 (840 / 12) starts the 70-129 row, which pays 233. +- name: Case 3, the band top and the next band start set the one person summer grant. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [AR, AR] + age: [30, 30] + immigration_status: [CITIZEN, CITIZEN] + ssi: [0, 0] + heating_type: [ELECTRICITY, ELECTRICITY] + has_cooling_expense: [true, true] + pre_subsidy_electricity_expense: [1_800, 1_800] + ar_liheap_countable_income: [828, 840] + output: + ar_liheap_cooling: [242, 233] + +# FY2025: MCI 1_525 is within the 2_251 size-one limit (x12 = 27_012). The FFY 2025 +# electric row 1,510-1,569 pays 84 for 1&2 (the FY2026 row pays 77). With electric heat +# the winter grant reads the same cell, so the household receives 84 in each season. +- name: Case 4, FY2025 summer grant for a one person household with electric heat. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + immigration_status: CITIZEN + ssi: 0 + employment_income_before_lsr: 22_880 + spm_units: + spm_unit: + members: [person1] + heating_type: ELECTRICITY + has_cooling_expense: true + pre_subsidy_electricity_expense: 1_800 + ar_tea: 0 + households: + household: + members: [person1] + state_code: AR + output: + ar_liheap_countable_income: 18_300 + ar_liheap_income_limit: 27_012 + ar_liheap_cooling_eligible: true + ar_liheap_cooling: 84 + ar_liheap: 84 + +# The summer program pays electric benefits only (manual section 2.1, p29), so the heating +# fuel does not matter: gas heat and no heat both receive the FY2026 1&2 cell for MCI +# 1_525 (row 1510-1569), 77. +- name: Case 5, the summer grant does not depend on the heating fuel. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [AR, AR] + age: [45, 45] + immigration_status: [CITIZEN, CITIZEN] + ssi: [0, 0] + ar_tea: [0, 0] + employment_income_before_lsr: [22_880, 22_880] + heating_type: [NATURAL_GAS, NONE] + has_cooling_expense: [true, true] + pre_subsidy_electricity_expense: [1_800, 1_800] + output: + ar_liheap_countable_income: [18_300, 18_300] + ar_liheap_cooling_eligible: [true, true] + ar_liheap_cooling: [77, 77] diff --git a/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling_eligible.yaml new file mode 100644 index 00000000000..2b6c60ce20a --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling_eligible.yaml @@ -0,0 +1,89 @@ +# Arkansas summer regular (cooling) eligibility: FY2026 plan sections 3.1-3.3 (pp12-13) +# and FFY 2025 manual section 2.1 (p29, "Only electric benefits are available during the +# Summer Program"). + +# Boundary: the FY2026 size-one limit is MCI 2_347 (x12 = 28_164) and is inclusive. +# 28_164 is eligible and falls in the 2130-2347 row, which pays 60 for 1&2; 28_164.12 is +# above the limit. +- name: Case 1, the summer income limit is inclusive. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [AR, AR] + age: [40, 40] + immigration_status: [CITIZEN, CITIZEN] + ssi: [0, 0] + heating_type: [ELECTRICITY, ELECTRICITY] + has_cooling_expense: [true, true] + pre_subsidy_electricity_expense: [1_800, 1_800] + ar_liheap_countable_income: [28_164, 28_164.12] + output: + ar_liheap_income_limit: [28_164, 28_164] + ar_liheap_cooling_eligible: [true, false] + ar_liheap_cooling: [60, 0] + +# The energy burden is a cooling cost on the household's own electric account. Full-time +# Arkansas minimum wage (11 x 40 x 52 = 22_880; x 0.8 = 18_304; MCI 1_525) puts each +# household in the FY2026 1510-1569 row, 77 for 1&2. No cooling cost, or no electric bill, +# gives no summer grant. +- name: Case 2, a cooling cost and an electric bill are both required. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [AR, AR, AR] + age: [45, 45, 45] + immigration_status: [CITIZEN, CITIZEN, CITIZEN] + ssi: [0, 0, 0] + ar_tea: [0, 0, 0] + employment_income_before_lsr: [22_880, 22_880, 22_880] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] + has_cooling_expense: [true, false, true] + pre_subsidy_electricity_expense: [1_800, 1_800, 0] + output: + ar_liheap_cooling_eligible: [true, false, false] + ar_liheap_cooling: [77, 0, 0] + +# With no income both households pass the income test (0 <= limit), so only the qualified +# member requirement separates them: an undocumented adult alone has household size 0. +- name: Case 3, a household with no qualified member is not eligible. + period: 2026 + input: + state_code: [AR, AR] + age: [40, 40] + immigration_status: [CITIZEN, UNDOCUMENTED] + ssi: [0, 0] + ar_tea: [0, 0] + heating_type: [ELECTRICITY, ELECTRICITY] + has_cooling_expense: [true, true] + pre_subsidy_electricity_expense: [1_800, 1_800] + output: + ar_liheap_household_size: [1, 0] + ar_liheap_cooling_eligible: [true, false] + +# An unspecified heating fuel pays $0, as in every state LIHEAP program (issue 9754). Case +# 2's worker (MCI 1_525) has a cooling cost and an electric bill but no stated fuel. +- name: Case 4, an unspecified heating fuel receives no summer grant. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + immigration_status: CITIZEN + ssi: 0 + employment_income_before_lsr: 22_880 + spm_units: + spm_unit: + members: [person1] + heating_type: UNSPECIFIED + has_cooling_expense: true + pre_subsidy_electricity_expense: 1_800 + ar_tea: 0 + households: + household: + members: [person1] + state_code: AR + output: + ar_liheap_countable_income: 18_300 + ar_liheap_cooling_eligible: false + ar_liheap_cooling: 0 diff --git a/policyengine_us/variables/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling.py b/policyengine_us/variables/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling.py new file mode 100644 index 00000000000..393c9a119a0 --- /dev/null +++ b/policyengine_us/variables/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling.py @@ -0,0 +1,33 @@ +from policyengine_us.model_api import * + + +class ar_liheap_cooling(Variable): + value_type = float + entity = SPMUnit + definition_period = YEAR + unit = USD + label = "Arkansas LIHEAP summer regular cooling benefit" + defined_for = "ar_liheap_cooling_eligible" + reference = ( + # FFY 2025 manual section 6.1: the Electricity Matrix is "used for both + # Winter and Summer Regular Assistance". No FY2026 manual was found. + "https://liheapch.acf.gov/docs/2025/manuals/AR_Manual_2025.pdf#page=78", + # PDF pages 1-2 + "https://www.adeq.state.ar.us/energy/assistance/pdfs/LIHEAP_Benefit-Matix_2026_Electric.pdf#page=1", + # PDF pages 1-2 + "https://www.adeq.state.ar.us/energy/assistance/pdfs/LIHEAP_Benefit-Matix_2025_Electric.pdf#page=1", + # FY2026 state plan section 3.6: cooling benefits of $60 to $344. + "https://liheapch.acf.gov/docs/2026/state-plans/AR_Plan_2026.pdf#page=13", + ) + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.ar.ee.aeo.liheap.payment + income = spm_unit("ar_liheap_countable_income", period) / MONTHS_IN_YEAR + size = spm_unit("ar_liheap_household_size", period) + band = p.income_band.calc(max_(income, 0)).astype(int) + size_group = p.household_size_group.calc(size).astype(int) + # One fixed grant per summer from the Electricity matrix, the same for + # every known heating fuel or none, not capped at the bill. The planned + # FY2027 increase has no posted matrix, so later years carry the FY2026 + # cells forward. + return p.matrix.electricity[band][size_group] diff --git a/policyengine_us/variables/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling_eligible.py b/policyengine_us/variables/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling_eligible.py new file mode 100644 index 00000000000..98d98569c29 --- /dev/null +++ b/policyengine_us/variables/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling_eligible.py @@ -0,0 +1,38 @@ +from policyengine_us.model_api import * + + +class ar_liheap_cooling_eligible(Variable): + value_type = bool + entity = SPMUnit + definition_period = YEAR + label = "Eligible for Arkansas LIHEAP summer regular cooling assistance" + defined_for = StateCode.AR + reference = ( + # FY2026 state plan sections 3.1-3.3: the heating income chart and an + # energy burden; no asset test. + # PDF pages 12-13 + "https://liheapch.acf.gov/docs/2026/state-plans/AR_Plan_2026.pdf#page=12", + # FFY 2025 manual section 2.1: "Only electric benefits are available + # during the Summer Program." No FY2026 manual was found. + "https://liheapch.acf.gov/docs/2025/manuals/AR_Manual_2025.pdf#page=29", + ) + + def formula(spm_unit, period, parameters): + size = spm_unit("ar_liheap_household_size", period) + income = spm_unit("ar_liheap_countable_income", period) + income_limit = spm_unit("ar_liheap_income_limit", period) + # The summer benefit pays the electric bill, so the energy burden is a + # cooling cost on the household's own electric account. No input + # identifies electricity included in rent, so those households are not + # paid. + electric_bill = spm_unit("pre_subsidy_electricity_expense", period) + # No heat and every known fuel can be paid. An unspecified heating fuel + # pays $0, as in every state LIHEAP program (issue 9754). + fuel = spm_unit("heating_type", period) + return ( + (size > 0) + & (income <= income_limit) + & spm_unit("has_cooling_expense", period) + & (electric_bill > 0) + & (fuel != fuel.possible_values.UNSPECIFIED) + ) From 235166dc42e8e56878e25d8d42a6d724c6623c00 Mon Sep 17 00:00:00 2001 From: Ziming Date: Sun, 8 Nov 2026 22:05:59 -0500 Subject: [PATCH 03/11] Count Arkansas 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..41ffd86c65d 100644 --- a/policyengine_us/parameters/gov/hhs/liheap/regular_payments.yaml +++ b/policyengine_us/parameters/gov/hhs/liheap/regular_payments.yaml @@ -4,6 +4,7 @@ values: - ak_hap # Alaska Heating Assistance Program - al_liheap # Alabama LIHEAP - ar_liheap # Arkansas LIHEAP + - ar_liheap_cooling # Arkansas LIHEAP cooling - 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 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 e2ce03511887a80a676484b2ef362d169daaf875 Mon Sep 17 00:00:00 2001 From: Ziming Date: Mon, 9 Nov 2026 23:06:29 -0500 Subject: [PATCH 05/11] Pay Arkansas LIHEAP cooling to unsubsidized renters with utilities in rent Plan item 3.3 makes these renters eligible. Rewords the energy account comments, adds an in-rent renter case, and checks the national liheap total. Co-Authored-By: Claude Opus 5.5 --- policyengine_us/programs.yaml | 2 +- .../aeo/liheap/cooling/ar_liheap_cooling.yaml | 8 +++--- .../cooling/ar_liheap_cooling_eligible.yaml | 27 ++++++++++++++++++- .../cooling/ar_liheap_cooling_eligible.py | 20 +++++++++----- 4 files changed, 46 insertions(+), 11 deletions(-) diff --git a/policyengine_us/programs.yaml b/policyengine_us/programs.yaml index 6b78bd49e27..7b5a4eb5f5e 100644 --- a/policyengine_us/programs.yaml +++ b/policyengine_us/programs.yaml @@ -1020,7 +1020,7 @@ programs: variable: ar_liheap_cooling parameter_prefix: gov.states.ar.ee.aeo.liheap verified_years: "2025-2026" - notes: FY2025-FY2026 summer regular benefit from the Electricity matrix, paid once per summer on the electric bill; the planned FY2027 increase has no posted matrix. Summer crisis, equipment and electricity included in rent are out of scope. + notes: FY2025-FY2026 summer regular benefit from the Electricity matrix, paid once per summer on the electric bill or, for unsubsidized renters, through utilities included in rent; the planned FY2027 increase has no posted matrix. Summer crisis and equipment are out of scope. - state: AZ status: partial name: Arizona LIHEAP diff --git a/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling.yaml b/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling.yaml index 9582769d361..a64f3542094 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling.yaml @@ -2,8 +2,8 @@ # Electricity matrix "for both Winter and Summer Regular Assistance". Wages are full time # at the $11.00 Arkansas minimum wage: 11 x 40 x 52 = 22_880. The 20% earned disregard # gives 22_880 x 0.8 = 18_304; MCI 1_525.33 rounds to 1_525 (x12 = 18_300). The $1,800 -# electric bill only shows an electric account; it does not change the grant. TEA is -# pinned to 0, as in the heating tests. +# electric bill shows that the household pays its own electric bill, which the summer grant +# pays; the bill does not change the grant. TEA is pinned to 0, as in the heating tests. # FY2026: size three uses the 3&4 column; the size-three limit is 3_792 x 12 = 45_504. # Electric row 1510-1569 pays 114 for 3&4. @@ -106,7 +106,8 @@ # FY2025: MCI 1_525 is within the 2_251 size-one limit (x12 = 27_012). The FFY 2025 # electric row 1,510-1,569 pays 84 for 1&2 (the FY2026 row pays 77). With electric heat -# the winter grant reads the same cell, so the household receives 84 in each season. +# the winter grant reads the same cell, so the household receives 84 in each season, and +# the national LIHEAP total is 84 + 84 = 168. - name: Case 4, FY2025 summer grant for a one person household with electric heat. period: 2025 absolute_error_margin: 0.01 @@ -134,6 +135,7 @@ ar_liheap_cooling_eligible: true ar_liheap_cooling: 84 ar_liheap: 84 + liheap: 168 # The summer program pays electric benefits only (manual section 2.1, p29), so the heating # fuel does not matter: gas heat and no heat both receive the FY2026 1&2 cell for MCI diff --git a/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling_eligible.yaml index 2b6c60ce20a..3b477c716d7 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling_eligible.yaml @@ -22,7 +22,7 @@ ar_liheap_cooling_eligible: [true, false] ar_liheap_cooling: [60, 0] -# The energy burden is a cooling cost on the household's own electric account. Full-time +# The energy burden is a cooling cost on an electric bill the household pays itself. Full-time # Arkansas minimum wage (11 x 40 x 52 = 22_880; x 0.8 = 18_304; MCI 1_525) puts each # household in the FY2026 1510-1569 row, 77 for 1&2. No cooling cost, or no electric bill, # gives no summer grant. @@ -87,3 +87,28 @@ ar_liheap_countable_income: 18_300 ar_liheap_cooling_eligible: false ar_liheap_cooling: 0 + +# Plan item 3.3 makes unsubsidized renters with utilities included in the rent eligible. +# Both renters pay the FY2026 HUD Fair Market Rent for a one-bedroom unit in Pulaski +# County, $989 a month (x12 = 11_868), which includes utilities, and report no separate +# cooling cost or electric bill. Case 2's worker (MCI 1_525) gets 77 when unsubsidized; +# the rent route covers only unsubsidized households, so the subsidized renter gets 0. +- name: Case 5, an unsubsidized renter with utilities in the rent receives the summer grant. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [AR, AR] + age: [45, 45] + immigration_status: [CITIZEN, CITIZEN] + ssi: [0, 0] + ar_tea: [0, 0] + employment_income_before_lsr: [22_880, 22_880] + heating_type: [NATURAL_GAS, NATURAL_GAS] + has_cooling_expense: [false, false] + pre_subsidy_electricity_expense: [0, 0] + tenant_pays_utilities: [false, false] + rent: [11_868, 11_868] + receives_housing_assistance: [false, true] + output: + ar_liheap_cooling_eligible: [true, false] + ar_liheap_cooling: [77, 0] diff --git a/policyengine_us/variables/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling_eligible.py b/policyengine_us/variables/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling_eligible.py index 98d98569c29..0517c47eaf2 100644 --- a/policyengine_us/variables/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling_eligible.py +++ b/policyengine_us/variables/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling_eligible.py @@ -22,17 +22,25 @@ def formula(spm_unit, period, parameters): income = spm_unit("ar_liheap_countable_income", period) income_limit = spm_unit("ar_liheap_income_limit", period) # The summer benefit pays the electric bill, so the energy burden is a - # cooling cost on the household's own electric account. No input - # identifies electricity included in rent, so those households are not - # paid. - electric_bill = spm_unit("pre_subsidy_electricity_expense", period) + # cooling cost on an electric bill the household pays itself. + pays_electric_bill = spm_unit("has_cooling_expense", period) & ( + spm_unit("pre_subsidy_electricity_expense", period) > 0 + ) + # Plan item 3.3 also makes unsubsidized renters with utilities included + # in the rent eligible. + subsidized = spm_unit.household("is_in_public_housing", period) | spm_unit( + "receives_housing_assistance", period + ) + utilities_in_rent = ~spm_unit.household("tenant_pays_utilities", period) & ( + add(spm_unit, period, ["rent"]) > 0 + ) + responsible = pays_electric_bill | (utilities_in_rent & ~subsidized) # No heat and every known fuel can be paid. An unspecified heating fuel # pays $0, as in every state LIHEAP program (issue 9754). fuel = spm_unit("heating_type", period) return ( (size > 0) & (income <= income_limit) - & spm_unit("has_cooling_expense", period) - & (electric_bill > 0) + & responsible & (fuel != fuel.possible_values.UNSPECIFIED) ) 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 2f5ff91e19a6e849d438c06f49ad524c4fe32923 Mon Sep 17 00:00:00 2001 From: Ziming Date: Wed, 11 Nov 2026 22:54:07 -0500 Subject: [PATCH 08/11] Stop Arkansas LIHEAP income limits at the federal maximum Co-Authored-By: Claude Fable 5.2 --- changelog.d/ar-liheap-cooling.fixed.md | 1 + .../states/ar/ee/aeo/liheap/ar_liheap.yaml | 16 ++++---- .../aeo/liheap/cooling/ar_liheap_cooling.yaml | 9 ++-- .../liheap/income/ar_liheap_income_limit.yaml | 29 ++++++++----- .../states/ar/ee/aeo/liheap/integration.yaml | 4 +- .../liheap/income/ar_liheap_income_limit.py | 17 +++++++- source_conflicts/candidates.yaml | 41 +++++++++++++++++++ 7 files changed, 91 insertions(+), 26 deletions(-) create mode 100644 changelog.d/ar-liheap-cooling.fixed.md diff --git a/changelog.d/ar-liheap-cooling.fixed.md b/changelog.d/ar-liheap-cooling.fixed.md new file mode 100644 index 00000000000..38b64548264 --- /dev/null +++ b/changelog.d/ar-liheap-cooling.fixed.md @@ -0,0 +1 @@ +Stop Arkansas 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/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/ar_liheap.yaml b/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/ar_liheap.yaml index 6f1120caa45..e789819bebc 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/ar_liheap.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/ar_liheap.yaml @@ -32,7 +32,7 @@ output: ar_liheap_household_size: 2 ar_liheap_countable_income: 4_800 - ar_liheap_income_limit: 36_840 + ar_liheap_income_limit: 36_834 ar_liheap_matrix_amount: 227 ar_liheap: 227 @@ -144,7 +144,7 @@ output: ar_liheap_household_size: 1 ar_liheap_countable_income: 16_800 - ar_liheap_income_limit: 27_012 + ar_liheap_income_limit: 27_011 ar_liheap_eligible: true ar_liheap_matrix_amount: 91 ar_liheap: 91 @@ -235,7 +235,7 @@ output: ar_liheap_household_size: 7 ar_liheap_countable_income: 70_800 - ar_liheap_income_limit: 71_016 + ar_liheap_income_limit: 71_010 ar_liheap_eligible: true ar_liheap_matrix_amount: 60 ar_liheap: 60 @@ -265,7 +265,7 @@ output: ar_liheap_household_size: 1 ar_liheap_countable_income: 24_180 - ar_liheap_income_limit: 27_012 + ar_liheap_income_limit: 27_011 ar_liheap_eligible: true ar_liheap_matrix_amount: 73 ar_liheap: 73 @@ -484,7 +484,7 @@ # Current application section III excludes under-18 earnings whether or not the member # attends school, so countable income is 0. The FY2026 natural gas matrix awards 308 at -# zero income for size two (limit 3_070 x 12 = 36_840). +# zero income for size two (limit 36_834, HHS's 60% state median income amount). - name: Case 17, age 17 nonstudent earnings determine the final heating award. period: 2026 input: @@ -511,13 +511,13 @@ state_code: AR output: ar_liheap_countable_income: 0 - ar_liheap_income_limit: 36_840 + ar_liheap_income_limit: 36_834 ar_liheap_eligible: true ar_liheap: 308 # At 18, a member who is not in high school (manual section 4.7, p56) or college # reports work income: wages 48_000 x 0.8 = 38_400 exceed the two-person FY2026 limit -# 36_840, so no grant is paid. +# 36_834, so no grant is paid. - name: Case 18, age 18 nonstudent earnings determine the final heating award. period: 2026 input: @@ -545,7 +545,7 @@ state_code: AR output: ar_liheap_countable_income: 38_400 - ar_liheap_income_limit: 36_840 + ar_liheap_income_limit: 36_834 ar_liheap_eligible: false ar_liheap: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling.yaml b/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling.yaml index a64f3542094..81ef6beadd4 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling.yaml @@ -5,7 +5,8 @@ # electric bill shows that the household pays its own electric bill, which the summer grant # pays; the bill does not change the grant. TEA is pinned to 0, as in the heating tests. -# FY2026: size three uses the 3&4 column; the size-three limit is 3_792 x 12 = 45_504. +# FY2026: size three uses the 3&4 column; the size-three limit is 45_501, HHS's 60% state +# median income amount, which is below the chart's 3_792 x 12 = 45_504. # Electric row 1510-1569 pays 114 for 3&4. - name: Case 1, a minimum wage parent with two children receives the 3 and 4 person summer grant. period: 2026 @@ -39,7 +40,7 @@ output: ar_liheap_household_size: 3 ar_liheap_countable_income: 18_300 - ar_liheap_income_limit: 45_504 + ar_liheap_income_limit: 45_501 ar_liheap_cooling_eligible: true ar_liheap_cooling: 114 @@ -104,7 +105,7 @@ output: ar_liheap_cooling: [242, 233] -# FY2025: MCI 1_525 is within the 2_251 size-one limit (x12 = 27_012). The FFY 2025 +# FY2025: MCI 1_525 (x12 = 18_300) is within the 27_011 size-one limit. The FFY 2025 # electric row 1,510-1,569 pays 84 for 1&2 (the FY2026 row pays 77). With electric heat # the winter grant reads the same cell, so the household receives 84 in each season, and # the national LIHEAP total is 84 + 84 = 168. @@ -131,7 +132,7 @@ state_code: AR output: ar_liheap_countable_income: 18_300 - ar_liheap_income_limit: 27_012 + ar_liheap_income_limit: 27_011 ar_liheap_cooling_eligible: true ar_liheap_cooling: 84 ar_liheap: 84 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/income/ar_liheap_income_limit.yaml b/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/income/ar_liheap_income_limit.yaml index 7ea2c0302f2..615df9a4c06 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/income/ar_liheap_income_limit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/income/ar_liheap_income_limit.yaml @@ -1,7 +1,13 @@ # Independent AR component tests; no final household eligibility or total benefit is asserted. -# Official 2026 income chart sizes 1-20 plus the printed 688 monthly amount for the extra person at 21; source limits are literal, independent of the SMI/FPG formula. -- name: Case 1, all published household limits and the first additional person match. +# Official 2026 income chart sizes 1-20 plus the printed 688 monthly amount for the extra person at 21, each +# stopped at the federal maximum of 42 U.S.C. 8624(b)(2)(B): the greater of HHS's FY2026 60% state median income +# amount (28_167, 36_834, 45_501, 54_168, 62_834, 71_501 for sizes 1-6; 54_168 x 1.35 = 73_126 for size 7) and +# 150% of the 2025 poverty guideline (15_650 + 5_500 per extra person). The chart is higher at size 2 +# (3_070 x 12 = 36_840), size 3 (3_792 x 12 = 45_504), size 7 (6_094 x 12 = 73_128), every even size from 8 to 20 +# (size 8: 6_769 x 12 = 81_228 against 54_150 x 1.5 = 81_225) and size 21 (15_707 x 12 = 188_484 against +# 125_650 x 1.5 = 188_475). +- name: Case 1, published household limits and the first additional person stop at the federal maximum. period: 2026 absolute_error_margin: 0.01 input: @@ -13,9 +19,9 @@ 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200] ar_liheap_household_size: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21] output: - ar_liheap_income_limit: [28_164, 36_840, 45_504, 54_168, 62_832, 71_496, 73_128, 81_228, 89_472, - 97_728, 105_972, 114_228, 122_472, 130_728, 138_972, 147_228, 155_472, 163_728, 171_972, 180_228, - 188_484] + ar_liheap_income_limit: [28_164, 36_834, 45_501, 54_168, 62_832, 71_496, 73_126, 81_225, 89_472, + 97_725, 105_972, 114_225, 122_472, 130_725, 138_972, 147_225, 155_472, 163_725, 171_972, 180_225, + 188_475] # FY2025 chart (2024 FPG 15060 + 5380 per extra person): size 8 52720 x 1.5 / 12 = 6590; size 20 117280 x 1.5 / 12 = 14660; @@ -35,7 +41,9 @@ # SMI x 0.6 / 12 x size factor: 0.52 -> 2251.00 -> 2251; 0.68 -> 2943.62 -> 2944; 0.84 -> 3636.23 -> 3636; # 1.00 -> 4328.85 -> 4329; 1.16 -> 5021.47 -> 5021; 1.32 -> 5714.08 -> 5714. # Size 7: 47340x1.5/12=5917.50 -> 5918 (60% SMI would be 5844); size 9: 58100x1.5/12=7262.50 -> 7263. -# Annual = monthly x 12. +# Annual = monthly x 12, stopped at the federal maximum. HHS's 60% amount is floor(86_577 x 0.6) = 51_946 for +# four, times the size factor, rounded down: 27_011 (size 1), 35_323 (size 2), 51_946 (size 4). 150% of the 2024 +# guideline is 47_340 x 1.5 = 71_010 (size 7) and 58_100 x 1.5 = 87_150 (size 9). - name: Case 3, FY2025 state median income rows and the size seven poverty guideline row. period: 2025 absolute_error_margin: 0.01 @@ -46,11 +54,12 @@ gas_expense: [900, 900, 900, 900, 900, 900, 900, 900] ar_liheap_household_size: [1, 2, 3, 4, 5, 6, 7, 9] output: - ar_liheap_income_limit: [27_012, 35_328, 43_632, 51_948, 60_252, 68_568, 71_016, 87_156] + ar_liheap_income_limit: [27_011, 35_323, 43_632, 51_946, 60_252, 68_568, 71_010, 87_150] # FY2026 chart: size 20 prints $15,019 and "ADD $688 FOR EACH ADDITIONAL HOUSEHOLD -# MEMBER": size 22 = 15,019 + 2 x 688 = 16,395 a month; x 12 = 196,740. -- name: Case 4, FY2026 second additional person adds the printed amount twice. +# MEMBER": size 22 = 15,019 + 2 x 688 = 16,395 a month; x 12 = 196,740. The federal maximum is 150% of the +# 2025 guideline for 22 people: (15,650 + 21 x 5,500) x 1.5 = 196,725. +- name: Case 4, FY2026 second additional person stops at the federal maximum. period: 2026 absolute_error_margin: 0.01 input: @@ -59,4 +68,4 @@ gas_expense: 1_200 ar_liheap_household_size: 22 output: - ar_liheap_income_limit: 196_740 + ar_liheap_income_limit: 196_725 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/integration.yaml index 5d26c6e8d74..9de5581df14 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/integration.yaml @@ -102,7 +102,7 @@ # (manual 4.7.1, p59): 20,704. MCI 1,725.33 rounds to 1,725 (p58): 20,700. ar_liheap_countable_income: 20_700 # FY2026 chart, size 3: 3,792 x 12. - ar_liheap_income_limit: 45_504 + ar_liheap_income_limit: 45_501 ar_liheap_eligible: true # FY2026 natural gas row 1690-1749, column 3&4: 124. ar_liheap_matrix_amount: 124 @@ -248,7 +248,7 @@ # interest over 200 a month (p162): 3,600 - 2,400 = 1,200; total 24,360, MCI 2,030. ar_liheap_countable_income: 24_360 # FY2025 chart, size 2: 2,944 x 12. - ar_liheap_income_limit: 35_328 + ar_liheap_income_limit: 35_323 ar_liheap_eligible: true # FY2025 other (wood) row 2,010-2,129, column 1&2: 68. ar_liheap_matrix_amount: 68 diff --git a/policyengine_us/variables/gov/states/ar/ee/aeo/liheap/income/ar_liheap_income_limit.py b/policyengine_us/variables/gov/states/ar/ee/aeo/liheap/income/ar_liheap_income_limit.py index fed9787cb8f..9d3889dce38 100644 --- a/policyengine_us/variables/gov/states/ar/ee/aeo/liheap/income/ar_liheap_income_limit.py +++ b/policyengine_us/variables/gov/states/ar/ee/aeo/liheap/income/ar_liheap_income_limit.py @@ -1,5 +1,5 @@ from policyengine_us.model_api import * -from policyengine_us.variables.gov.hhs.hhs_smi import smi +from policyengine_us.variables.gov.hhs.hhs_smi import liheap_smi_limit, smi from policyengine_us.variables.gov.hhs.tax_unit_fpg import fpg @@ -11,6 +11,9 @@ class ar_liheap_income_limit(Variable): label = "Arkansas LIHEAP annualized income limit" defined_for = StateCode.AR 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://liheapch.acf.gov/docs/2026/state-plans/AR_Plan_2026.pdf#page=9", "https://www.adeq.state.ar.us/energy/assistance/pdfs/LIHEAP_Eligibility-Chart_2026.pdf", "https://liheapch.acf.gov/docs/2025/state-plans/AR_Plan_2025.pdf#page=8", @@ -36,9 +39,19 @@ def formula(spm_unit, period, parameters): # The charts add a printed amount for each person above size 20. extra_people = max_(size - p.max_table_size, 0) monthly_limit = rounded_base + extra_people * p.additional_member_amount + # 42 U.S.C. 8624(b)(2)(B) pays no household above the greater of 60% of + # state median income, in the amounts HHS publishes by household size, + # and 150% of the poverty guideline. A published monthly amount that + # rounds above that maximum stops there. + p_hhs = parameters(period).gov.hhs.liheap + smi_maximum = liheap_smi_limit(size, state, p_hhs.smi_limit, period, parameters) + fpg_maximum = p_hhs.fpg_limit * fpg( + max_(size, 1), state_group, period, parameters, year_lag=p.fpg_year_lag + ) + limit = min_(monthly_limit * MONTHS_IN_YEAR, max_(smi_maximum, fpg_maximum)) # 42 U.S.C. 8624(b)(2)(B) bars excluding a household on income below # 110% of the poverty guideline. In 2025-2027 every limit stays at # least 32% above that floor at every household size (in 2025 and 2027, # the printed per-member amount above size 20 sets the narrowest # margin), so no separate floor is applied. - return where(size > 0, monthly_limit * MONTHS_IN_YEAR, 0) + return where(size > 0, limit, 0) diff --git a/source_conflicts/candidates.yaml b/source_conflicts/candidates.yaml index 4e687a33ca4..92c12c8b63d 100644 --- a/source_conflicts/candidates.yaml +++ b/source_conflicts/candidates.yaml @@ -717,6 +717,47 @@ candidates: state: unreviewed note: 'PE-US follows Appendix F; verify whether its specific inclusions override the general exclusion of nonrecurring lump sums.' +- id: us-ar:liheap-income-limit-above-federal-ceiling + program: liheap + years: + from: 2025 + to: 2026 + type: federal-fiscal + question: Can Arkansas'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, Arkansas, page 1 + url: https://acf.gov/sites/default/files/documents/ocs/COMM_LIHEAP_IM2025-02_SMIStateTable_Att4.pdf#page=1 + - kind: guidance + citation: Arkansas FY2026 LIHEAP Income Eligibility Chart + url: https://www.adeq.state.ar.us/energy/assistance/pdfs/LIHEAP_Eligibility-Chart_2026.pdf + - kind: guidance + citation: Arkansas FY2025 LIHEAP Income Eligibility Chart + url: https://www.adeq.state.ar.us/energy/assistance/pdfs/LIHEAP_Eligibility-Chart_2025.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 each monthly limit to the nearest dollar and add a rounded amount for each member above 20, + which puts the limits above the federal maximum at several sizes. + example: In FY2026, three people with $45,504 of countable income ($3,792 a month) are within the printed limit and + would be paid, but exceed the $45,501 federal maximum and receive $0 in the PR. + pe_us: + follows: statute + code: + - policyengine_us/variables/gov/states/ar/ee/aeo/liheap/income/ar_liheap_income_limit.py + prs: + - https://github.com/PolicyEngine/policyengine-us/pull/10744 + state: ruled + note: 'Ruled under the 2026-10-19 statute-first rule. The PR stops each limit at the federal maximum. Printed and capped + annual limits: FY2026 size 2, $36,840 and $36,834; 3, $45,504 and $45,501; 7, $73,128 and $73,126; each even size + from 8 to 20, $3 lower ($81,228 and $81,225 at 8); 21, $188,484 and $188,475. FY2025, with the 60% amounts computed + from the $86,577 four-person median because HHS''s FY2025 table was not located: size 1, $27,012 and $27,011; 2, + $35,328 and $35,323; 4, $51,948 and $51,946; each odd size from 7 to 19, $6 lower ($71,016 and $71,010 at 7). Other + sizes keep the printed amount. Verify each later fiscal-year chart.' - id: us-ar:liheap-minor-and-student-earnings program: liheap years: From fdc2ab04a34145d124bf75d925e7c8ac7f5b9cd9 Mon Sep 17 00:00:00 2001 From: Ziming Date: Wed, 11 Nov 2026 23:22:28 -0500 Subject: [PATCH 09/11] Cite HHS's FY2025 median income table for the capped limits Co-Authored-By: Claude Opus 5.5 --- source_conflicts/candidates.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/source_conflicts/candidates.yaml b/source_conflicts/candidates.yaml index 92c12c8b63d..02f65e3e695 100644 --- a/source_conflicts/candidates.yaml +++ b/source_conflicts/candidates.yaml @@ -738,6 +738,9 @@ candidates: - kind: guidance citation: Arkansas FY2025 LIHEAP Income Eligibility Chart url: https://www.adeq.state.ar.us/energy/assistance/pdfs/LIHEAP_Eligibility-Chart_2025.pdf + - kind: guidance + citation: HHS LIHEAP IM 2024-02 Attachment 4, Mandatory Use in LIHEAP for FY25, Arkansas, 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. @@ -754,8 +757,8 @@ candidates: state: ruled note: 'Ruled under the 2026-10-19 statute-first rule. The PR stops each limit at the federal maximum. Printed and capped annual limits: FY2026 size 2, $36,840 and $36,834; 3, $45,504 and $45,501; 7, $73,128 and $73,126; each even size - from 8 to 20, $3 lower ($81,228 and $81,225 at 8); 21, $188,484 and $188,475. FY2025, with the 60% amounts computed - from the $86,577 four-person median because HHS''s FY2025 table was not located: size 1, $27,012 and $27,011; 2, + from 8 to 20, $3 lower ($81,228 and $81,225 at 8); 21, $188,484 and $188,475. FY2025, from HHS''s FY2025 amounts (LIHEAP IM 2024-02 + Attachment 4): size 1, $27,012 and $27,011; 2, $35,328 and $35,323; 4, $51,948 and $51,946; each odd size from 7 to 19, $6 lower ($71,016 and $71,010 at 7). Other sizes keep the printed amount. Verify each later fiscal-year chart.' - id: us-ar:liheap-minor-and-student-earnings 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 81f96098e0219b79143730566702bacaafdef884 Mon Sep 17 00:00:00 2001 From: Ziming Date: Fri, 13 Nov 2026 15:25:32 -0500 Subject: [PATCH 11/11] Fix issues from review: cap case, fuel condition, rent-route tests Addresses DTrim99's round-1 review; upstream main merged first. Co-Authored-By: Claude Opus 5.5 --- .../liheap/payment/matrix/electricity.yaml | 2 + .../states/ar/ee/aeo/liheap/ar_liheap.yaml | 6 +- .../cooling/ar_liheap_cooling_eligible.yaml | 75 +++++++++++++++++-- .../eligibility/ar_liheap_eligible.yaml | 12 ++- .../states/ar/ee/aeo/liheap/integration.yaml | 6 +- .../aeo/liheap/cooling/ar_liheap_cooling.py | 6 +- .../cooling/ar_liheap_cooling_eligible.py | 21 +++--- .../liheap/income/ar_liheap_income_limit.py | 4 + source_conflicts/candidates.yaml | 16 +++- 9 files changed, 115 insertions(+), 33 deletions(-) diff --git a/policyengine_us/parameters/gov/states/ar/ee/aeo/liheap/payment/matrix/electricity.yaml b/policyengine_us/parameters/gov/states/ar/ee/aeo/liheap/payment/matrix/electricity.yaml index f0e249395cd..a3698b84fc8 100644 --- a/policyengine_us/parameters/gov/states/ar/ee/aeo/liheap/payment/matrix/electricity.yaml +++ b/policyengine_us/parameters/gov/states/ar/ee/aeo/liheap/payment/matrix/electricity.yaml @@ -340,3 +340,5 @@ metadata: href: https://www.adeq.state.ar.us/energy/assistance/pdfs/LIHEAP_Benefit-Matix_2026_Electric.pdf#page=1 - title: "Arkansas FY2025 Benefits Matrix, Electricity #page 1-2" href: https://www.adeq.state.ar.us/energy/assistance/pdfs/LIHEAP_Benefit-Matix_2025_Electric.pdf#page=1 + - title: Arkansas FFY 2025 LIHEAP Policy and Procedures Manual, Section 6.1 + href: https://liheapch.acf.gov/docs/2025/manuals/AR_Manual_2025.pdf#page=78 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/ar_liheap.yaml b/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/ar_liheap.yaml index e789819bebc..69967ecd30f 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/ar_liheap.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/ar_liheap.yaml @@ -120,7 +120,8 @@ output: ar_liheap: [308, 0] -# FY2025: wages 21000x0.8=16800, MCI 1400; size-one limit 2251 (x12=27012). +# FY2025: wages 21000x0.8=16800, MCI 1400; size-one limit 27011 (chart 2251 x12=27012, stopped at +# HHS's FY2025 60% state median income amount). # FFY2025 electric row 1390-1449 pays 91 for 1&2 (the FY2026 row pays 88). - name: Case 6, FY2025 electric grant for a one person household. period: 2025 @@ -187,6 +188,7 @@ ar_liheap: 64 # Boundary: FY2025 size 7 uses 150% FPG, 47340x1.5/12=5917.50 -> 5918 (x12=71016); 60% SMI would be 5844. +# The federal maximum, 150% of the 2024 guideline (47340x1.5=71010), stops the limit at 71010. # Wages (50000+38500)x0.8=70800, MCI 5900, so eligible; FFY2025 electric 5140-Max pays 60 for 5&up. - name: Case 8, FY2025 seven person household between the SMI and FPG limits is paid. period: 2025 @@ -241,7 +243,7 @@ ar_liheap: 60 # FY2025 Medicare-net Social Security (plan 1.9 p6; manual 4.7.2 p59): 2025 Part B 185x12=2220; -# 26400-2220=24180, MCI 2015; size-one limit 27012; FFY2025 gas 2010-2129 pays 73 for 1&2. +# 26400-2220=24180, MCI 2015; size-one limit 27011; FFY2025 gas 2010-2129 pays 73 for 1&2. - name: Case 9, FY2025 Social Security net of the Part B premium sets the gas grant. period: 2025 input: diff --git a/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling_eligible.yaml index 3b477c716d7..5b77cf218f6 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling_eligible.yaml @@ -22,8 +22,10 @@ ar_liheap_cooling_eligible: [true, false] ar_liheap_cooling: [60, 0] -# The energy burden is a cooling cost on an electric bill the household pays itself. Full-time -# Arkansas minimum wage (11 x 40 x 52 = 22_880; x 0.8 = 18_304; MCI 1_525) puts each +# A bill payer needs its own electric bill, since Arkansas's energy burden is a utility bill the +# household receives and summer pays electric benefits only. It also needs a cooling cost: a +# PolicyEngine convention with no Arkansas source, not part of Arkansas's energy-burden rule. +# Full-time Arkansas minimum wage (11 x 40 x 52 = 22_880; x 0.8 = 18_304; MCI 1_525) puts each # household in the FY2026 1510-1569 row, 77 for 1&2. No cooling cost, or no electric bill, # gives no summer grant. - name: Case 2, a cooling cost and an electric bill are both required. @@ -60,9 +62,11 @@ ar_liheap_household_size: [1, 0] ar_liheap_cooling_eligible: [true, false] -# An unspecified heating fuel pays $0, as in every state LIHEAP program (issue 9754). Case -# 2's worker (MCI 1_525) has a cooling cost and an electric bill but no stated fuel. -- name: Case 4, an unspecified heating fuel receives no summer grant. +# The summer grant does not depend on the heating fuel, so an unspecified fuel is paid like any +# other. Case 2's worker (MCI 1_525) has a cooling cost and an electric bill but no stated fuel: +# the FY2026 1510-1569 row pays 77 for 1&2. +# Was false and 0, now true and 77: the separate cooling payment adds no heating-fuel condition. +- name: Case 4, an unspecified heating fuel still receives the summer grant. period: 2026 absolute_error_margin: 0.01 input: @@ -85,8 +89,8 @@ state_code: AR output: ar_liheap_countable_income: 18_300 - ar_liheap_cooling_eligible: false - ar_liheap_cooling: 0 + ar_liheap_cooling_eligible: true + ar_liheap_cooling: 77 # Plan item 3.3 makes unsubsidized renters with utilities included in the rent eligible. # Both renters pay the FY2026 HUD Fair Market Rent for a one-bedroom unit in Pulaski @@ -112,3 +116,60 @@ output: ar_liheap_cooling_eligible: [true, false] ar_liheap_cooling: [77, 0] + +# Each household is Case 5's unsubsidized renter (MCI 1_525, no cooling cost and no electric +# bill) with one condition of the utilities-in-rent route removed. The first pays its own +# utilities on the same $989 Pulaski County Fair Market Rent (x12 = 11_868); the second does not +# pay its own utilities but pays no rent; the third lives in public housing and pays 30% of its +# monthly adjusted income as rent (24 CFR 5.628: 22_880 x 0.3 / 12 = 572; x12 = 6_864). None +# receives the summer grant. +- name: Case 6, the utilities in rent route needs utilities in the rent, positive rent and no public housing. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [AR, AR, AR] + age: [45, 45, 45] + immigration_status: [CITIZEN, CITIZEN, CITIZEN] + ssi: [0, 0, 0] + ar_tea: [0, 0, 0] + employment_income_before_lsr: [22_880, 22_880, 22_880] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] + has_cooling_expense: [false, false, false] + pre_subsidy_electricity_expense: [0, 0, 0] + tenant_pays_utilities: [true, false, false] + rent: [11_868, 0, 6_864] + receives_housing_assistance: [false, false, false] + is_in_public_housing: [false, false, true] + output: + ar_liheap_cooling_eligible: [false, false, false] + ar_liheap_cooling: [0, 0, 0] + +# Manual section 4.8.2: a household in subsidized housing that receives a utility bill and is +# not reimbursed may be eligible. Case 2's first worker (MCI 1_525, a cooling cost and an +# electric bill) also receives housing assistance and still gets the FY2026 1510-1569 cell, 77 +# for 1&2: the subsidy exclusion applies only to the utilities-in-rent route. +- name: Case 7, a subsidized household with its own electric bill receives the summer grant. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + immigration_status: CITIZEN + ssi: 0 + employment_income_before_lsr: 22_880 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + has_cooling_expense: true + pre_subsidy_electricity_expense: 1_800 + receives_housing_assistance: true + ar_tea: 0 + households: + household: + members: [person1] + state_code: AR + output: + ar_liheap_cooling_eligible: true + ar_liheap_cooling: 77 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/eligibility/ar_liheap_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/eligibility/ar_liheap_eligible.yaml index 57238557ab7..e5e5738ea9b 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/eligibility/ar_liheap_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/eligibility/ar_liheap_eligible.yaml @@ -62,7 +62,7 @@ # An aged SSI recipient (Social Security 600/month; SSI (994-(600-20))x12=4968) lives with a # working adult son. The State's Medicaid buy-in pays Part B for an SSI recipient, so no premium is netted. -# 40000x0.8=32000 + 4968 + 7200 = 44168; MCI 3680.67 rounds to 3681; x12=44172 > 36840 two-person limit. +# 40000x0.8=32000 + 4968 + 7200 = 44168; MCI 3680.67 rounds to 3681; x12=44172 > 36834 two-person limit. - name: Case 5, SSI receipt does not waive the ordinary income ceiling. period: 2026 input: @@ -93,8 +93,12 @@ ar_liheap_eligible: false # Boundary: FY2025 plan 2.1 (p8) and chart set size 7 at 150% FPG: 47340x1.5/12=5917.50 -> 5918 -# (annual 71016). MCI 5900 (70800) and 5918 (71016) are eligible; 5919 (71028) is not. -# 60% SMI would have given 5844 and denied MCI 5900. +# (annual 71016). 60% SMI would have given 5844 and denied MCI 5900. 42 U.S.C. 8624(b)(2)(B) +# stops the limit at the greater of HHS's FY2025 60% amount for seven (70127) and 150% of the +# 2024 guideline (47340x1.5=71010), so the limit is 71010. MCI 5900 (70800) is eligible; 5918 +# (71016) and 5919 (71028) are not. +# Was [true, true, false], now [true, false, false]: the federal cap of 42 U.S.C. 8624(b)(2)(B) +# lowers the size-seven limit from the chart's 71016 to 71010. - name: Case 6, FY2025 size seven households use the poverty guideline limit. period: 2025 input: @@ -104,7 +108,7 @@ ar_liheap_household_size: [7, 7, 7] ar_liheap_countable_income: [70_800, 71_016, 71_028] output: - ar_liheap_eligible: [true, true, false] + ar_liheap_eligible: [true, false, false] # FY2026 plan item 2.3 (p10): a household has an energy burden if it pays utilities # directly; one whose utilities are reimbursed through an allowance is not eligible unless diff --git a/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/integration.yaml index 9de5581df14..75e4fcb6215 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ar/ee/aeo/liheap/integration.yaml @@ -101,7 +101,8 @@ # Wages less 20% (plan item 1.8): 22,880 x 0.8 = 18,304; child support counts in full # (manual 4.7.1, p59): 20,704. MCI 1,725.33 rounds to 1,725 (p58): 20,700. ar_liheap_countable_income: 20_700 - # FY2026 chart, size 3: 3,792 x 12. + # FY2026 chart, size 3: 3,792 x 12 = 45,504, stopped at HHS's 60% state median income + # amount, 45,501. ar_liheap_income_limit: 45_501 ar_liheap_eligible: true # FY2026 natural gas row 1690-1749, column 3&4: 124. @@ -247,7 +248,8 @@ # Social Security net of Part B (manual 4.7.2, p59): 15,780 + 7,380 = 23,160; # interest over 200 a month (p162): 3,600 - 2,400 = 1,200; total 24,360, MCI 2,030. ar_liheap_countable_income: 24_360 - # FY2025 chart, size 2: 2,944 x 12. + # FY2025 chart, size 2: 2,944 x 12 = 35,328, stopped at HHS's FY2025 60% state median + # income amount, 35,323. ar_liheap_income_limit: 35_323 ar_liheap_eligible: true # FY2025 other (wood) row 2,010-2,129, column 1&2: 68. diff --git a/policyengine_us/variables/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling.py b/policyengine_us/variables/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling.py index 393c9a119a0..325ca8692a6 100644 --- a/policyengine_us/variables/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling.py +++ b/policyengine_us/variables/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling.py @@ -27,7 +27,7 @@ def formula(spm_unit, period, parameters): band = p.income_band.calc(max_(income, 0)).astype(int) size_group = p.household_size_group.calc(size).astype(int) # One fixed grant per summer from the Electricity matrix, the same for - # every known heating fuel or none, not capped at the bill. The planned - # FY2027 increase has no posted matrix, so later years carry the FY2026 - # cells forward. + # every heating type, not capped at the bill. The planned FY2027 + # increase has no posted matrix, so later years carry the FY2026 cells + # forward. return p.matrix.electricity[band][size_group] diff --git a/policyengine_us/variables/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling_eligible.py b/policyengine_us/variables/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling_eligible.py index 0517c47eaf2..477d35a48cf 100644 --- a/policyengine_us/variables/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling_eligible.py +++ b/policyengine_us/variables/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling_eligible.py @@ -13,7 +13,9 @@ class ar_liheap_cooling_eligible(Variable): # PDF pages 12-13 "https://liheapch.acf.gov/docs/2026/state-plans/AR_Plan_2026.pdf#page=12", # FFY 2025 manual section 2.1: "Only electric benefits are available - # during the Summer Program." No FY2026 manual was found. + # during the Summer Program." Section 4.8.1: the energy burden of a + # household that receives a utility bill. No FY2026 manual was found. + # PDF pages 29, 63 "https://liheapch.acf.gov/docs/2025/manuals/AR_Manual_2025.pdf#page=29", ) @@ -21,8 +23,11 @@ def formula(spm_unit, period, parameters): size = spm_unit("ar_liheap_household_size", period) income = spm_unit("ar_liheap_countable_income", period) income_limit = spm_unit("ar_liheap_income_limit", period) - # The summer benefit pays the electric bill, so the energy burden is a - # cooling cost on an electric bill the household pays itself. + # Manual section 4.8.1 finds an energy burden in a utility bill the + # household receives, and summer pays electric benefits only, so a bill + # payer needs a positive electric bill. Requiring a cooling cost as + # well is a PolicyEngine convention with no Arkansas source; it is not + # part of Arkansas's energy-burden rule. pays_electric_bill = spm_unit("has_cooling_expense", period) & ( spm_unit("pre_subsidy_electricity_expense", period) > 0 ) @@ -35,12 +40,4 @@ def formula(spm_unit, period, parameters): add(spm_unit, period, ["rent"]) > 0 ) responsible = pays_electric_bill | (utilities_in_rent & ~subsidized) - # No heat and every known fuel can be paid. An unspecified heating fuel - # pays $0, as in every state LIHEAP program (issue 9754). - fuel = spm_unit("heating_type", period) - return ( - (size > 0) - & (income <= income_limit) - & responsible - & (fuel != fuel.possible_values.UNSPECIFIED) - ) + return (size > 0) & (income <= income_limit) & responsible diff --git a/policyengine_us/variables/gov/states/ar/ee/aeo/liheap/income/ar_liheap_income_limit.py b/policyengine_us/variables/gov/states/ar/ee/aeo/liheap/income/ar_liheap_income_limit.py index 9d3889dce38..31b42d0511c 100644 --- a/policyengine_us/variables/gov/states/ar/ee/aeo/liheap/income/ar_liheap_income_limit.py +++ b/policyengine_us/variables/gov/states/ar/ee/aeo/liheap/income/ar_liheap_income_limit.py @@ -13,7 +13,11 @@ class ar_liheap_income_limit(Variable): reference = ( "https://www.law.cornell.edu/uscode/text/42/8624#b_2_B", # FY2026 federal amounts + # PDF pages 1, 4 "https://acf.gov/sites/default/files/documents/ocs/COMM_LIHEAP_IM2025-02_SMIStateTable_Att4.pdf#page=1", + # FY2025 federal amounts + # PDF pages 1, 4 + "https://acf.gov/sites/default/files/documents/ocs/COMM_LIHEAP_IM%202024-02_Att4SMITable.pdf#page=1", "https://liheapch.acf.gov/docs/2026/state-plans/AR_Plan_2026.pdf#page=9", "https://www.adeq.state.ar.us/energy/assistance/pdfs/LIHEAP_Eligibility-Chart_2026.pdf", "https://liheapch.acf.gov/docs/2025/state-plans/AR_Plan_2025.pdf#page=8", diff --git a/source_conflicts/candidates.yaml b/source_conflicts/candidates.yaml index 2d5ab820f69..c71c7264b13 100644 --- a/source_conflicts/candidates.yaml +++ b/source_conflicts/candidates.yaml @@ -669,12 +669,18 @@ candidates: - kind: guidance citation: ACF LIHEAP Clearinghouse 2025 Arkansas matrix url: https://liheapch.acf.gov/docs/2025/benefits-matricies/AR_BenefitMatrix_2025.pdf#page=1 + - kind: plan + citation: Arkansas FY2025 LIHEAP State Plan, item 3.6, page 12 + url: https://liheapch.acf.gov/docs/2025/state-plans/AR_Plan_2025.pdf#page=12 readings: statute: No statute is supplied; AEO's FY2025 electric matrix uses $242, $288 and $344 at the lowest income band. other: The clearinghouse matrix uses $202, $240 and $287. + plan: Plan item 3.6 estimates FY2025 cooling benefits of $50 to $287; AEO's electric matrix, headed "20% OVER + BASE", pays the summer benefit at $60 to $344. example: Electric heating, household size 1-2 and income $0-$69 yields $242 under AEO's matrix versus $202 under - the clearinghouse matrix. + the clearinghouse matrix. In summer 2025, five people with no income receive $344 from AEO's matrix, above + plan item 3.6's $287 maximum. pe_us: follows: unknown code: @@ -683,11 +689,15 @@ candidates: - policyengine_us/parameters/gov/states/ar/ee/aeo/liheap/payment/matrix/natural_gas.yaml - policyengine_us/parameters/gov/states/ar/ee/aeo/liheap/payment/matrix/other.yaml - policyengine_us/parameters/gov/states/ar/ee/aeo/liheap/payment/matrix/propane.yaml + - policyengine_us/variables/gov/states/ar/ee/aeo/liheap/cooling/ar_liheap_cooling.py - policyengine_us/variables/gov/states/ar/ee/aeo/liheap/payment/ar_liheap_matrix_amount.py prs: - https://github.com/PolicyEngine/policyengine-us/pull/9973 + - https://github.com/PolicyEngine/policyengine-us/pull/10744 state: unreviewed - note: 'PE-US follows AEO''s matrix; verify the clearinghouse document''s effective year and possible mislabeling.' + note: 'PE-US follows AEO''s matrix for the winter and summer regular benefits; Wayback captures of AEO''s electric + matrix from 2024-12-06, 2025-06-15 and 2025-07-25 share one content digest, so it stayed posted through the summer + 2025 season. Verify the clearinghouse document''s effective year and possible mislabeling.' - id: us-ar:liheap-gambling-and-capital-gains program: liheap years: @@ -730,7 +740,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, Arkansas, page 1 + citation: HHS LIHEAP IM 2025-02 Attachment 4, Mandatory Use in LIHEAP for FY26, Arkansas, pages 1 and 4 url: https://acf.gov/sites/default/files/documents/ocs/COMM_LIHEAP_IM2025-02_SMIStateTable_Att4.pdf#page=1 - kind: guidance citation: Arkansas FY2026 LIHEAP Income Eligibility Chart