diff --git a/changelog.d/ky-liheap-cooling.added.md b/changelog.d/ky-liheap-cooling.added.md new file mode 100644 index 00000000000..c487179f20f --- /dev/null +++ b/changelog.d/ky-liheap-cooling.added.md @@ -0,0 +1 @@ +Add Kentucky LIHEAP regular cooling assistance (the one-time electric cooling subsidy). diff --git a/policyengine_us/parameters/gov/hhs/liheap/regular_payments.yaml b/policyengine_us/parameters/gov/hhs/liheap/regular_payments.yaml index e811bea6fbd..2b49bac82f3 100644 --- a/policyengine_us/parameters/gov/hhs/liheap/regular_payments.yaml +++ b/policyengine_us/parameters/gov/hhs/liheap/regular_payments.yaml @@ -22,6 +22,7 @@ values: - in_eap # Indiana EAP - ks_liheap # Kansas LIEAP - ky_liheap # Kentucky LIHEAP + - ky_liheap_cooling # Kentucky LIHEAP cooling - la_liheap # Louisiana LIHEAP - ma_liheap # Massachusetts LIHEAP - md_meap # Maryland MEAP diff --git a/policyengine_us/parameters/gov/states/ky/dcbs/liheap/cooling/fpg_year_lag.yaml b/policyengine_us/parameters/gov/states/ky/dcbs/liheap/cooling/fpg_year_lag.yaml new file mode 100644 index 00000000000..09a66f8c1a1 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ky/dcbs/liheap/cooling/fpg_year_lag.yaml @@ -0,0 +1,15 @@ +description: Kentucky uses the federal poverty guidelines from this many years before the federal fiscal year's ending year for the cooling subsidy income limit under the Low Income Home Energy Assistance Program. +# The summer fact sheet's limits are 150% of the 2026 guidelines: +# 15,960 * 1.5 / 12 = 1,995 for one person, and 5,680 * 1.5 / 12 = 710 for +# each additional member. Heating in the same federal fiscal year uses the +# 2025 guidelines (fpg_year_lag), and so do the cooling income bands, whose +# FY2026 workbook ranges match the heating ranges. +values: + 2025-10-01: 0 +metadata: + unit: year + period: year + label: Kentucky LIHEAP cooling federal poverty guideline year lag + reference: + - title: Kentucky LIHEAP Summer Cooling Subsidy Component FFY 2026 fact sheet, eligibility + href: https://www.capky.org/wp-content/uploads/2026/07/FY26-LIHEAP-Summer-Cooling-Subsidy-Fact-Sheet.pdf diff --git a/policyengine_us/parameters/gov/states/ky/dcbs/liheap/payment/dollars_per_point.yaml b/policyengine_us/parameters/gov/states/ky/dcbs/liheap/payment/dollars_per_point.yaml index 59e6fc81c09..e8a06698106 100644 --- a/policyengine_us/parameters/gov/states/ky/dcbs/liheap/payment/dollars_per_point.yaml +++ b/policyengine_us/parameters/gov/states/ky/dcbs/liheap/payment/dollars_per_point.yaml @@ -1,4 +1,4 @@ -description: Kentucky provides this amount per heating payment point, rounded up to whole dollars, under the Low Income Home Energy Assistance Program. +description: Kentucky provides this amount per heating or cooling payment point, rounded up to whole dollars, under the Low Income Home Energy Assistance Program. values: 2024-10-01: 16.66 metadata: diff --git a/policyengine_us/parameters/gov/states/ky/dcbs/liheap/payment/fuel_points.yaml b/policyengine_us/parameters/gov/states/ky/dcbs/liheap/payment/fuel_points.yaml index fcb790ed2e6..606c7c62144 100644 --- a/policyengine_us/parameters/gov/states/ky/dcbs/liheap/payment/fuel_points.yaml +++ b/policyengine_us/parameters/gov/states/ky/dcbs/liheap/payment/fuel_points.yaml @@ -1,4 +1,4 @@ -description: Kentucky sets these heating payment points by primary heating fuel under the Low Income Home Energy Assistance Program. +description: Kentucky sets these heating and cooling payment points by fuel under the Low Income Home Energy Assistance Program. UNSPECIFIED: 2024-10-01: 0 ELECTRICITY: @@ -24,7 +24,7 @@ NONE: metadata: unit: /1 period: year - label: Kentucky LIHEAP heating fuel points + label: Kentucky LIHEAP fuel points breakdown: - heating_type reference: diff --git a/policyengine_us/parameters/gov/states/ky/dcbs/liheap/payment/income_bands.yaml b/policyengine_us/parameters/gov/states/ky/dcbs/liheap/payment/income_bands.yaml index bae35f2a9b0..d9cf501ec65 100644 --- a/policyengine_us/parameters/gov/states/ky/dcbs/liheap/payment/income_bands.yaml +++ b/policyengine_us/parameters/gov/states/ky/dcbs/liheap/payment/income_bands.yaml @@ -1,4 +1,4 @@ -description: Kentucky uses these federal poverty guideline shares as the upper boundaries of its first three heating payment income bands under the Low Income Home Energy Assistance Program. +description: Kentucky uses these federal poverty guideline shares as the upper boundaries of its first three heating and cooling payment income bands under the Low Income Home Energy Assistance Program. values: 2024-10-01: [0.75, 1, 1.25] metadata: diff --git a/policyengine_us/parameters/gov/states/ky/dcbs/liheap/payment/subsidized_housing_rate.yaml b/policyengine_us/parameters/gov/states/ky/dcbs/liheap/payment/subsidized_housing_rate.yaml index 5743af5f2c7..0dbbc93555f 100644 --- a/policyengine_us/parameters/gov/states/ky/dcbs/liheap/payment/subsidized_housing_rate.yaml +++ b/policyengine_us/parameters/gov/states/ky/dcbs/liheap/payment/subsidized_housing_rate.yaml @@ -1,4 +1,4 @@ -description: Kentucky provides this share of the point-based heating payment to households in subsidized housing under the Low Income Home Energy Assistance Program. +description: Kentucky provides this share of the point-based heating or cooling payment to households in subsidized housing under the Low Income Home Energy Assistance Program. values: 2024-10-01: 0.3 metadata: diff --git a/policyengine_us/programs.yaml b/policyengine_us/programs.yaml index 6f704d520fb..bc3af5dd0eb 100644 --- a/policyengine_us/programs.yaml +++ b/policyengine_us/programs.yaml @@ -1214,7 +1214,15 @@ programs: variable: ky_liheap parameter_prefix: gov.states.ky.dcbs.liheap verified_years: "2025-2026" - notes: FY2025-FY2026 regular heating only, from the published benefit workbooks. Crisis and cooling assistance are out of scope. + notes: FY2025-FY2026 regular heating only, from the published benefit workbooks; the cooling subsidy is the separate Kentucky LIHEAP cooling entry. Crisis assistance is out of scope. + - state: KY + status: partial + name: Kentucky LIHEAP cooling + full_name: Kentucky Low Income Home Energy Assistance Program cooling subsidy + variable: ky_liheap_cooling + parameter_prefix: gov.states.ky.dcbs.liheap + verified_years: "2026" + notes: FY2026 one-time electric cooling subsidy from the heating point schedule with electric fuel points, including electricity paid through rent, under a current-year poverty guideline income limit, paid once a year although FY2026 ran spring and summer rounds. Air conditioners and crisis assistance are out of scope. - state: LA status: partial name: Louisiana LIHEAP diff --git a/policyengine_us/tests/policy/baseline/gov/states/ky/dcbs/liheap/cooling/ky_liheap_cooling.yaml b/policyengine_us/tests/policy/baseline/gov/states/ky/dcbs/liheap/cooling/ky_liheap_cooling.yaml new file mode 100644 index 00000000000..041c9375c13 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ky/dcbs/liheap/cooling/ky_liheap_cooling.yaml @@ -0,0 +1,232 @@ +# Sources: Kentucky FY2026 state plan Sections 3.1-3.6, the FY2026 heat-cool +# benefit workbook (cooling column equals the electricity heating column), and +# the FFY 2026 Summer Cooling Subsidy fact sheet (150% of the 2026 guidelines). +# The cooling limit uses the 2026 guidelines: one person 15,960 a year, each added +# person 5,680. The income bands use the 2025 guidelines, as heating and the +# workbook's income ranges do: one person 15,650 a year (1,304.17 a month), each +# added person 5,500. +# Points: size (capped at 5) + income points + 6 electric points; amount = +# ceil(points * 16.66); subsidized housing = ceil(amount * 0.3). +# Wages are hourly pay times hours; the 7.25 rate is the federal minimum wage +# that Kentucky applies. Electric bills of 1,560 are 130 a month. + +- name: Case 1, a gas-heated home receives the electric cooling amount. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + # 7.25 an hour, 20 hours a week: 7.25 * 1_040 = 7_540. + employment_income_before_lsr: 7_540 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 720 + pre_subsidy_electricity_expense: 1_560 + has_cooling_expense: true + receives_housing_assistance: false + households: + household: + members: [person1] + state_code: KY + is_in_public_housing: false + output: + # 7_540 / 12 = 628.33 a month. + ky_liheap_cooling_eligible: true + # Both seasons: 628.33 <= ceil(15_650 / 12 * 0.75 = 978.13) = 979, the + # 0-75% band (4 points). + # Cooling uses the electric points: 1 + 4 + 6 = 11 points; + # ceil(11 * 16.66 = 183.26) = 184. + ky_liheap_cooling: 184 + # Heating keeps the natural gas points: 1 + 4 + 3 = 8 points; + # ceil(8 * 16.66 = 133.28) = 134. + ky_liheap: 134 + # The national LIHEAP total adds both: 134 + 184. + liheap: 318 + +- name: Case 2, a public housing tenant receives the subsidized electric amount. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + employment_income_before_lsr: 7_540 + spm_units: + spm_unit: + members: [person1] + heating_type: ELECTRICITY + pre_subsidy_electricity_expense: 1_560 + has_cooling_expense: true + receives_housing_assistance: false + households: + household: + members: [person1] + state_code: KY + is_in_public_housing: true + output: + # 0-75% band, subsidized income points 1: 1 + 1 + 6 = 8 points; + # ceil(8 * 16.66 = 133.28) = 134; ceil(134 * 0.3 = 40.2) = 41, the plan's + # $41 minimum. + ky_liheap_cooling: 41 + +- name: Case 3, a family of five in the lowest band receives the 250 maximum. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 38 + # 11.00 an hour, 40 hours a week: 11 * 2_080 = 22_880. + employment_income_before_lsr: 22_880 + person2: + age: 36 + person3: + age: 12 + person4: + age: 9 + person5: + age: 6 + spm_units: + spm_unit: + members: [person1, person2, person3, person4, person5] + heating_type: ELECTRICITY + pre_subsidy_electricity_expense: 1_560 + has_cooling_expense: true + receives_housing_assistance: false + # The household does not receive K-TAP. + tanf: 0 + households: + household: + members: [person1, person2, person3, person4, person5] + state_code: KY + is_in_public_housing: false + output: + # 2026 guideline for five: 15_960 + 4 * 5_680 = 38_680. + # Limit: 38_680 * 1.5 / 12 = 4_835, the fact sheet's amount for five. + ky_liheap_cooling_eligible: true + # 2025 guideline for five: 15_650 + 4 * 5_500 = 37_650, or 3_137.50 a month. + # 22_880 / 12 = 1_906.67 <= ceil(3_137.50 * 0.75 = 2_353.13) = 2_354, so + # 0-75%. 5 + 4 + 6 = 15 points; ceil(15 * 16.66 = 249.9) = 250, the plan's + # $250 maximum. + ky_liheap_cooling: 250 + +- name: Case 4, the lowest band ends at 75 percent of the 2025 guideline. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + # 979.00 a month. + employment_income_before_lsr: 11_748 + person2: + age: 40 + # 979.01 a month. + employment_income_before_lsr: 11_748.12 + spm_units: + spm_unit1: + members: [person1] + heating_type: ELECTRICITY + pre_subsidy_electricity_expense: 1_560 + has_cooling_expense: true + receives_housing_assistance: false + spm_unit2: + members: [person2] + heating_type: ELECTRICITY + pre_subsidy_electricity_expense: 1_560 + has_cooling_expense: true + receives_housing_assistance: false + households: + household1: + members: [person1] + state_code: KY + is_in_public_housing: false + household2: + members: [person2] + state_code: KY + is_in_public_housing: false + output: + # The 0-75% band ends at ceil(15_650 / 12 * 0.75 = 978.13) = 979, the + # workbook's printed range end for one person. + # Household 1: 979.00 is in it: 1 + 4 + 6 = 11 points = 184. + # Household 2: 979.01 is in 75.01-100% (3 points): 1 + 3 + 6 = 10 points; + # ceil(10 * 16.66 = 166.6) = 167. + ky_liheap_cooling: [184, 167] + +- name: Case 5, income above the 2025 heating limit but within the 2026 cooling limit. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + # 11.40 an hour, 40 hours a week: 11.40 * 2_080 = 23_712. + employment_income_before_lsr: 23_712 + spm_units: + spm_unit: + members: [person1] + heating_type: ELECTRICITY + pre_subsidy_electricity_expense: 1_560 + has_cooling_expense: true + receives_housing_assistance: false + households: + household: + members: [person1] + state_code: KY + is_in_public_housing: false + output: + # 23_712 / 12 = 1_976 a month. + # Heating limit: ceil(15_650 * 1.5 / 12 = 1_956.25) = 1_957, so 1_976 is over. + ky_liheap_income_eligible: false + ky_liheap: 0 + # Cooling limit: 15_960 * 1.5 / 12 = 1_995, so 1_976 qualifies. + ky_liheap_cooling_eligible: true + # 1_976 > ceil(1_304.17 * 1.25 = 1_630.21) = 1_631, the top band + # (1 point). 1 + 1 + 6 = 8 points; ceil(8 * 16.66 = 133.28) = 134. + ky_liheap_cooling: 134 + +- name: Case 6, a home without heating and a home with an unspecified fuel both receive cooling. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + employment_income_before_lsr: 7_540 + person2: + age: 40 + employment_income_before_lsr: 7_540 + spm_units: + spm_unit1: + members: [person1] + heating_type: NONE + pre_subsidy_electricity_expense: 1_560 + has_cooling_expense: true + receives_housing_assistance: false + spm_unit2: + members: [person2] + heating_type: UNSPECIFIED + pre_subsidy_electricity_expense: 1_560 + has_cooling_expense: true + receives_housing_assistance: false + households: + household1: + members: [person1] + state_code: KY + is_in_public_housing: false + household2: + members: [person2] + state_code: KY + is_in_public_housing: false + output: + # The cooling subsidy uses the electric points whatever the home heats + # with: 1 + 4 + 6 = 11 points = 184. + # Was [true, false], now [true, true]: cooling has no heating-fuel condition. + ky_liheap_cooling_eligible: [true, true] + # Was [184, 0], now [184, 184]: cooling has no heating-fuel condition. + ky_liheap_cooling: [184, 184] + # Neither home has a heating benefit. + ky_liheap: [0, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/ky/dcbs/liheap/cooling/ky_liheap_cooling_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ky/dcbs/liheap/cooling/ky_liheap_cooling_eligible.yaml new file mode 100644 index 00000000000..313f7707cea --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ky/dcbs/liheap/cooling/ky_liheap_cooling_eligible.yaml @@ -0,0 +1,214 @@ +# Sources: Kentucky FFY 2026 Summer Cooling Subsidy fact sheet (responsibility +# for home utility costs; monthly income at or below 1,995 for one person, +# 150% of the 2026 guideline), FY2026 state plan Section 3.4 (electric +# benefits only), and 921 KAR 4:116 Section 2(1)(d) (a Social Security number +# or permanent residency card for each member). Electric bills of 1,560 are +# 130 a month. + +- name: Case 1, income at the 2026 cooling limit qualifies and one cent more does not. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + # 1_995.00 a month. + employment_income_before_lsr: 23_940 + person2: + age: 40 + # 1_995.01 a month. + employment_income_before_lsr: 23_940.12 + spm_units: + spm_unit1: + members: [person1] + heating_type: ELECTRICITY + pre_subsidy_electricity_expense: 1_560 + has_cooling_expense: true + spm_unit2: + members: [person2] + heating_type: ELECTRICITY + pre_subsidy_electricity_expense: 1_560 + has_cooling_expense: true + households: + household1: + members: [person1] + state_code: KY + household2: + members: [person2] + state_code: KY + output: + # Limit: 15_960 * 1.5 / 12 = 1_995. + ky_liheap_cooling_eligible: [true, false] + +- name: Case 2, the household must have cooling costs and its own electric bill, in Kentucky. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + employment_income_before_lsr: 7_540 + person2: + age: 40 + employment_income_before_lsr: 7_540 + person3: + age: 40 + employment_income_before_lsr: 7_540 + spm_units: + spm_unit1: + members: [person1] + heating_type: ELECTRICITY + pre_subsidy_electricity_expense: 1_560 + has_cooling_expense: false + spm_unit2: + members: [person2] + heating_type: NATURAL_GAS + gas_expense: 720 + pre_subsidy_electricity_expense: 0 + has_cooling_expense: true + spm_unit3: + members: [person3] + heating_type: ELECTRICITY + pre_subsidy_electricity_expense: 1_560 + has_cooling_expense: true + households: + household1: + members: [person1] + state_code: KY + household2: + members: [person2] + state_code: KY + household3: + members: [person3] + state_code: IN + output: + # Household 1 has no cooling costs, household 2 has no electric bill, and + # household 3 lives in Indiana. + ky_liheap_cooling_eligible: [false, false, false] + ky_liheap_cooling: [0, 0, 0] + +- name: Case 3, a member without a Social Security number or green card makes the household ineligible. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + employment_income_before_lsr: 7_540 + person2: + age: 38 + immigration_status: UNDOCUMENTED + ssn_card_type: NONE + spm_units: + spm_unit: + members: [person1, person2] + heating_type: ELECTRICITY + pre_subsidy_electricity_expense: 1_560 + has_cooling_expense: true + households: + household: + members: [person1, person2] + state_code: KY + output: + ky_liheap_cooling_eligible: false + ky_liheap_cooling: 0 + +- name: Case 4, renters who pay electricity through the rent qualify whether or not subsidized. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + employment_income_before_lsr: 7_540 + # The FY2026 HUD Fair Market Rent for a one-bedroom unit in Jefferson + # County, 1,047 a month (x12 = 12,564), which includes utilities. + rent: 12_564 + person2: + age: 40 + employment_income_before_lsr: 7_540 + rent: 12_564 + spm_units: + spm_unit1: + members: [person1] + heating_type: ELECTRICITY + pre_subsidy_electricity_expense: 0 + has_cooling_expense: false + receives_housing_assistance: false + spm_unit2: + members: [person2] + heating_type: ELECTRICITY + pre_subsidy_electricity_expense: 0 + has_cooling_expense: false + receives_housing_assistance: true + households: + household1: + members: [person1] + state_code: KY + tenant_pays_utilities: false + household2: + members: [person2] + state_code: KY + tenant_pays_utilities: false + output: + # The fact sheet covers utility costs paid "as an undesignated portion of + # rent", and 921 KAR 4:116 Section 4(3)(a) pays the landlord. + ky_liheap_cooling_eligible: [true, true] + # 628.33 a month is in the 0-75% band. Unsubsidized: 1 + 4 + 6 = 11 + # points = 184. Subsidized: 1 + 1 + 6 = 8 points = 134; + # ceil(134 * 0.3 = 40.2) = 41. + ky_liheap_cooling: [184, 41] + +- name: Case 5, a renter who pays utilities apart from the rent needs an electric bill. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + employment_income_before_lsr: 7_540 + # The Jefferson County one-bedroom Fair Market Rent, as in Case 4. + rent: 12_564 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 720 + pre_subsidy_electricity_expense: 0 + has_cooling_expense: false + households: + household: + members: [person1] + state_code: KY + tenant_pays_utilities: true + output: + # The household pays rent, but its utilities are not in the rent, and it + # has no electric bill, so neither route applies. + ky_liheap_cooling_eligible: false + ky_liheap_cooling: 0 + +- name: Case 6, a rent-free household whose utilities someone else pays is not eligible. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + employment_income_before_lsr: 7_540 + rent: 0 + spm_units: + spm_unit: + members: [person1] + heating_type: ELECTRICITY + pre_subsidy_electricity_expense: 0 + has_cooling_expense: false + households: + household: + members: [person1] + state_code: KY + tenant_pays_utilities: false + output: + # The household does not pay its utilities, and with no rent it pays no + # utility costs through rent either. + ky_liheap_cooling_eligible: false + ky_liheap_cooling: 0 diff --git a/policyengine_us/variables/gov/states/ky/dcbs/liheap/cooling/ky_liheap_cooling.py b/policyengine_us/variables/gov/states/ky/dcbs/liheap/cooling/ky_liheap_cooling.py new file mode 100644 index 00000000000..59e684c95c2 --- /dev/null +++ b/policyengine_us/variables/gov/states/ky/dcbs/liheap/cooling/ky_liheap_cooling.py @@ -0,0 +1,49 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.ky.dcbs.liheap.ky_liheap import ( + ky_liheap_points_amount, +) + + +class ky_liheap_cooling(Variable): + value_type = float + entity = SPMUnit + definition_period = YEAR + unit = USD + label = "Kentucky LIHEAP regular cooling assistance" + documentation = ( + "One-time electric cooling subsidy per federal fiscal year. Verified " + "for FY2026; other years use model parameter backfilling and are " + "unverified estimates." + ) + defined_for = "ky_liheap_cooling_eligible" + reference = ( + "https://liheapch.acf.gov/docs/2026/benefits-matricies/KY_BenefitMatrix_Heat-Cool_2026.xlsx", + # PDF pages 4,10-11: Sections 1.1, 3.4-3.6. + "https://liheapch.acf.gov/docs/2026/state-plans/KY_Plan_2026.pdf#page=4", + "https://www.capky.org/wp-content/uploads/2026/07/FY26-LIHEAP-Summer-Cooling-Subsidy-Fact-Sheet.pdf", + # Section 4(3). + "https://apps.legislature.ky.gov/law/kar/titles/921/004/116/", + ) + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.ky.dcbs.liheap + # Cooling assistance provides electric benefits only (plan Section + # 3.4). The FY2026 workbook's cooling column equals its electricity + # heating column in every row, so the electric fuel points apply + # whatever fuel heats the home. The workbook prints the same income + # ranges for both columns, on the guideline year heating uses; the + # cooling income limit alone uses the summer fact sheet's guideline, + # so incomes above the printed ranges but within that limit take the + # top band. Subsidized households get heating's subsidized income + # points (candidate us-ky:liheap-subsidized-income-points). + # The model pays one subsidy a year. Section 4(3)(a) allows one per + # component period, and plan Section 1.1 offers cooling and summer + # components only "if additional money is received", so a second + # round in a year depends on funding. + return ky_liheap_points_amount( + spm_unit, + period, + parameters, + p.payment.fuel_points.ELECTRICITY, + spm_unit("ky_liheap_fpg", period), + ) diff --git a/policyengine_us/variables/gov/states/ky/dcbs/liheap/cooling/ky_liheap_cooling_eligible.py b/policyengine_us/variables/gov/states/ky/dcbs/liheap/cooling/ky_liheap_cooling_eligible.py new file mode 100644 index 00000000000..bf49e382bf2 --- /dev/null +++ b/policyengine_us/variables/gov/states/ky/dcbs/liheap/cooling/ky_liheap_cooling_eligible.py @@ -0,0 +1,44 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.ky.dcbs.liheap.ky_liheap_eligible import ( + ky_liheap_members_documented, +) +from policyengine_us.variables.gov.states.ky.dcbs.liheap.ky_liheap_income_eligible import ( + ky_liheap_meets_income_limit, +) + + +class ky_liheap_cooling_eligible(Variable): + value_type = bool + entity = SPMUnit + definition_period = YEAR + label = "Kentucky LIHEAP cooling subsidy eligibility" + defined_for = StateCode.KY + reference = ( + "https://www.capky.org/wp-content/uploads/2026/07/FY26-LIHEAP-Summer-Cooling-Subsidy-Fact-Sheet.pdf", + # Sections 3.1 and 3.4. + "https://liheapch.acf.gov/docs/2026/state-plans/KY_Plan_2026.pdf#page=10", + # Sections 2(1)(d) and 4(3)(a). + "https://apps.legislature.ky.gov/law/kar/titles/921/004/116/", + ) + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.ky.dcbs.liheap.cooling + income_eligible = ky_liheap_meets_income_limit( + spm_unit, period, parameters, p.fpg_year_lag + ) + # The household must be responsible for its home electric costs, with + # the subsidy paid to its electric vendor, or pay them as an + # undesignated portion of the rent, with the subsidy paid to the + # landlord (Section 4(3)(a)). The fact sheet asks for the current + # electric bill and sets no air-conditioning test; requiring + # has_cooling_expense with the bill is a PolicyEngine convention for + # cooling costs, not a Kentucky condition. + pays_electric_bill = spm_unit("has_cooling_expense", period) & ( + spm_unit("pre_subsidy_electricity_expense", period) > 0 + ) + utilities_in_rent = ~spm_unit.household("tenant_pays_utilities", period) & ( + add(spm_unit, period, ["rent"]) > 0 + ) + responsible = pays_electric_bill | utilities_in_rent + documented = ky_liheap_members_documented(spm_unit, period) + return income_eligible & responsible & documented diff --git a/policyengine_us/variables/gov/states/ky/dcbs/liheap/ky_liheap.py b/policyengine_us/variables/gov/states/ky/dcbs/liheap/ky_liheap.py index 8a98037829f..ca999098255 100644 --- a/policyengine_us/variables/gov/states/ky/dcbs/liheap/ky_liheap.py +++ b/policyengine_us/variables/gov/states/ky/dcbs/liheap/ky_liheap.py @@ -1,6 +1,36 @@ from policyengine_us.model_api import * +def ky_liheap_points_amount(spm_unit, period, parameters, fuel_points, fpg): + """Return the point-based subsidy for the supplied fuel points. + + Heating and the electric cooling subsidy share the household-size, + income-band and housing points. fpg is the annual guideline that sets the + income bands. + """ + p = parameters(period).gov.states.ky.dcbs.liheap.payment + income = spm_unit("ky_liheap_income", period) / MONTHS_IN_YEAR + monthly_fpg = fpg / MONTHS_IN_YEAR + size = spm_unit("spm_unit_size", period) + # 921 KAR 4:116 Section 4(1)(e) lowers benefits for federally assisted + # housing or receipt of a utility allowance. A household receiving a + # utility allowance is already an assisted household. + subsidized = spm_unit("receives_housing_assistance", period) | spm_unit.household( + "is_in_public_housing", period + ) + band = 0 + for rate in p.income_bands: + band = band + (income > np.ceil(monthly_fpg * rate)) + income_points = where( + subsidized, + p.income_points.subsidized.calc(band), + p.income_points.unsubsidized.calc(band), + ) + points = min_(size, p.maximum_household_points) + income_points + fuel_points + amount = np.ceil(points * p.dollars_per_point) + return where(subsidized, np.ceil(amount * p.subsidized_housing_rate), amount) + + class ky_liheap(Variable): value_type = float entity = SPMUnit @@ -23,33 +53,11 @@ class ky_liheap(Variable): def formula(spm_unit, period, parameters): p = parameters(period).gov.states.ky.dcbs.liheap.payment - income = spm_unit("ky_liheap_income", period) / MONTHS_IN_YEAR - fpg = spm_unit("ky_liheap_fpg", period) / MONTHS_IN_YEAR size = spm_unit("spm_unit_size", period) fuel = spm_unit("heating_type", period) types = fuel.possible_values - # 921 KAR 4:116 Section 4(1)(e) lowers benefits for federally assisted - # housing or receipt of a utility allowance. A household receiving a - # utility allowance is already an assisted household. - subsidized = spm_unit( - "receives_housing_assistance", period - ) | spm_unit.household("is_in_public_housing", period) - band = 0 - for rate in p.income_bands: - band = band + (income > np.ceil(fpg * rate)) - income_points = where( - subsidized, - p.income_points.subsidized.calc(band), - p.income_points.unsubsidized.calc(band), - ) fuel_points = p.fuel_points[fuel] coal_adjustment = (fuel == types.COAL) * p.coal_adjustment.calc(size) - points = ( - min_(size, p.maximum_household_points) - + income_points - + fuel_points - + coal_adjustment - ) # Reproduces every heating cell of the FY2026 "rev benefit income # range" and FY2025 "subsidy benefits" worksheets, including the # two-person coal exception and the reversed subsidized income points @@ -57,8 +65,13 @@ def formula(spm_unit, period, parameters): # income_points/subsidized). The FY2026 sheet uses 2025 guidelines # despite its stale title and matches the plan's $15-$250 range; the # conditional "if no funding FFY26" sheet is not used. - amount = np.ceil(points * p.dollars_per_point) - amount = where(subsidized, np.ceil(amount * p.subsidized_housing_rate), amount) + amount = ky_liheap_points_amount( + spm_unit, + period, + parameters, + fuel_points + coal_adjustment, + spm_unit("ky_liheap_fpg", period), + ) # Fuels without a published column (OTHER, SOLAR, UNSPECIFIED, NONE) # carry zero points, which also marks them as having no schedule. The # full seasonal subsidy may credit the account, so it is not capped at diff --git a/policyengine_us/variables/gov/states/ky/dcbs/liheap/ky_liheap_eligible.py b/policyengine_us/variables/gov/states/ky/dcbs/liheap/ky_liheap_eligible.py index 83a2b1cc83d..f46db814325 100644 --- a/policyengine_us/variables/gov/states/ky/dcbs/liheap/ky_liheap_eligible.py +++ b/policyengine_us/variables/gov/states/ky/dcbs/liheap/ky_liheap_eligible.py @@ -1,6 +1,36 @@ from policyengine_us.model_api import * +def ky_liheap_members_documented(spm_unit, period): + """Return whether the household meets the LIHEAP documentation rules. + + Heating and the cooling subsidy share these rules. + """ + # LIHEAP is a federal public benefit, so at least one member must be a + # citizen or qualified noncitizen; plan Section 17.3 verifies that + # status. + has_qualified_member = add(spm_unit, period, ["is_citizen_or_legal_immigrant"]) > 0 + # 921 KAR 4:116 Section 2(1)(d) requires "a Social Security number, or + # a permanent residency card, for each household member". Section 2(2) + # treats the application as incomplete until it is received and + # Section 8(3) denies it after five working days, so a household with + # a member who has neither is ineligible. The plan's Section 17.2 + # checklist requires the card from adults and requests it from other + # members; the regulation's per-member rule applies here. Section 1(10) + # defines the household with no immigration carve-out, so every member + # counts for size and income. + person = spm_unit.members + ssn_card_type = person("ssn_card_type", period) + immigration_status = person("immigration_status", period) + has_ssn = ssn_card_type != ssn_card_type.possible_values.NONE + is_permanent_resident = ( + immigration_status + == immigration_status.possible_values.LEGAL_PERMANENT_RESIDENT + ) + all_documented = spm_unit.all(has_ssn | is_permanent_resident) + return has_qualified_member & all_documented + + class ky_liheap_eligible(Variable): value_type = bool entity = SPMUnit @@ -21,28 +51,5 @@ def formula(spm_unit, period, parameters): responsible = (spm_unit("heating_expense", period) > 0) | spm_unit( "heat_expense_included_in_rent", period ) - # LIHEAP is a federal public benefit, so at least one member must be a - # citizen or qualified noncitizen; plan Section 17.3 verifies that - # status. - has_qualified_member = ( - add(spm_unit, period, ["is_citizen_or_legal_immigrant"]) > 0 - ) - # 921 KAR 4:116 Section 2(1)(d) requires "a Social Security number, or - # a permanent residency card, for each household member". Section 2(2) - # treats the application as incomplete until it is received and - # Section 8(3) denies it after five working days, so a household with - # a member who has neither is ineligible. The plan's Section 17.2 - # checklist requires the card from adults and requests it from other - # members; the regulation's per-member rule applies here. Section 1(10) - # defines the household with no immigration carve-out, so every member - # counts for size and income. - person = spm_unit.members - ssn_card_type = person("ssn_card_type", period) - immigration_status = person("immigration_status", period) - has_ssn = ssn_card_type != ssn_card_type.possible_values.NONE - is_permanent_resident = ( - immigration_status - == immigration_status.possible_values.LEGAL_PERMANENT_RESIDENT - ) - all_documented = spm_unit.all(has_ssn | is_permanent_resident) - return income_eligible & responsible & has_qualified_member & all_documented + documented = ky_liheap_members_documented(spm_unit, period) + return income_eligible & responsible & documented diff --git a/policyengine_us/variables/gov/states/ky/dcbs/liheap/ky_liheap_income_eligible.py b/policyengine_us/variables/gov/states/ky/dcbs/liheap/ky_liheap_income_eligible.py index 15e74332cb8..bb18055a3cc 100644 --- a/policyengine_us/variables/gov/states/ky/dcbs/liheap/ky_liheap_income_eligible.py +++ b/policyengine_us/variables/gov/states/ky/dcbs/liheap/ky_liheap_income_eligible.py @@ -2,6 +2,42 @@ from policyengine_us.variables.gov.hhs.tax_unit_fpg import fpg +def ky_liheap_meets_income_limit(spm_unit, period, parameters, year_lag): + """Compare monthly income with the limit from guidelines lagged year_lag years. + + Heating and the cooling subsidy share the limit rate and rounding but can + use different guideline years. + """ + p = parameters(period).gov.states.ky.dcbs.liheap + size = spm_unit("spm_unit_size", period) + state_group = spm_unit.household("state_group_str", period) + table_size = clip(size, 1, p.maximum_table_size) + additional_people = max_(size - p.maximum_table_size, 0) + guideline = fpg(table_size, state_group, period, parameters, year_lag=year_lag) + additional_guideline = ( + fpg( + table_size + 1, + state_group, + period, + parameters, + year_lag=year_lag, + ) + - guideline + ) + # The heating fact sheet states "Add $688 for each additional family + # member" without a derivation. Rounding the additional-person guideline + # up at the income limit rate reproduces $688 for FY2026 and all eight + # published limits. The workbook's own table for sizes 9-14 rounds each + # whole limit instead and differs by up to $3 (for example $8,144 + # against $8,145 at size 10); the fact sheet is followed. The summer + # cooling fact sheet's $710 for each added member is exact + # (5,680 * 1.5 / 12 = 710), so either rounding gives it. + monthly_limit = np.ceil(guideline * p.income_limit / MONTHS_IN_YEAR) + monthly_increment = np.ceil(additional_guideline * p.income_limit / MONTHS_IN_YEAR) + income = spm_unit("ky_liheap_income", period) / MONTHS_IN_YEAR + return income <= monthly_limit + additional_people * monthly_increment + + class ky_liheap_income_eligible(Variable): value_type = bool entity = SPMUnit @@ -18,32 +54,6 @@ class ky_liheap_income_eligible(Variable): def formula(spm_unit, period, parameters): p = parameters(period).gov.states.ky.dcbs.liheap - size = spm_unit("spm_unit_size", period) - state_group = spm_unit.household("state_group_str", period) - table_size = clip(size, 1, p.maximum_table_size) - additional_people = max_(size - p.maximum_table_size, 0) - guideline = fpg( - table_size, state_group, period, parameters, year_lag=p.fpg_year_lag - ) - additional_guideline = ( - fpg( - table_size + 1, - state_group, - period, - parameters, - year_lag=p.fpg_year_lag, - ) - - guideline - ) - # The fact sheet states "Add $688 for each additional family member" - # without a derivation. Rounding the additional-person guideline up at - # the income limit rate reproduces $688 for FY2026 and all eight - # published limits. The workbook's own table for sizes 9-14 rounds each - # whole limit instead and differs by up to $3 (for example $8,144 - # against $8,145 at size 10); the fact sheet is followed. - monthly_limit = np.ceil(guideline * p.income_limit / MONTHS_IN_YEAR) - monthly_increment = np.ceil( - additional_guideline * p.income_limit / MONTHS_IN_YEAR + return ky_liheap_meets_income_limit( + spm_unit, period, parameters, p.fpg_year_lag ) - income = spm_unit("ky_liheap_income", period) / MONTHS_IN_YEAR - return income <= monthly_limit + additional_people * monthly_increment diff --git a/source_conflicts/candidates.yaml b/source_conflicts/candidates.yaml index 352bbc3a578..ab2f7d5dfcc 100644 --- a/source_conflicts/candidates.yaml +++ b/source_conflicts/candidates.yaml @@ -4521,24 +4521,36 @@ candidates: the workbooks' reversed income points? sources: - kind: regulation - citation: 921 KAR 4:116 Section 4(1)(c) + citation: 921 KAR 4:116 Sections 4(1)(c) and 4(3)(c)2 url: https://apps.legislature.ky.gov/law/kar/titles/921/004/116/ + - kind: plan + citation: Kentucky FY2026 LIHEAP State Plan, item 3.4 + url: https://liheapch.acf.gov/docs/2026/state-plans/KY_Plan_2026.pdf#page=10 - kind: calculator - citation: Kentucky FY2026 benefit workbook, rev benefit income range worksheet, cells E52-E55; FY2025 subsidy - benefits worksheet, cells E45-E48 + citation: Kentucky FY2026 benefit workbook, rev benefit income range worksheet, cells E52-E55 (heating) and + K52-K55 (cooling); FY2025 subsidy benefits worksheet, cells E45-E48 url: https://liheapch.acf.gov/docs/2026/benefits-matricies/KY_BenefitMatrix_Heat-Cool_2026.xlsx readings: - statute: The lowest-income household receives the highest benefit. - other: Subsidized income points rise as 1, 2, 3 and 4 with income. + statute: The lowest-income household receives the highest benefit (Section 4(1)(c)), and for cooling plan item + 3.4 says "The highest benefits are awarded to the lowest income household size". Section 4(3)(c)2's cooling + ceilings rise with income for subsidized housing ($100 up to 100% of poverty, $200 from 101% to 150%) and fall + for nonsubsidized housing ($400, then $300). + other: Subsidized income points rise as 1, 2, 3 and 4 with income, for heating and for cooling (cooling of $41, + $45, $51 and $56 for one person). + example: A one-person subsidized household in the lowest income band gets $41 of cooling under the workbook + (points 1 plus 1 plus 6 = 8, ceil(134 x 0.3)); under the regulation's ordering, with 4 income points, it would + get ceil(184 x 0.3) = $56. pe_us: follows: unknown code: - policyengine_us/parameters/gov/states/ky/dcbs/liheap/payment/income_points/subsidized.yaml - policyengine_us/variables/gov/states/ky/dcbs/liheap/ky_liheap.py - prs: [] + - policyengine_us/variables/gov/states/ky/dcbs/liheap/cooling/ky_liheap_cooling.py + prs: + - https://github.com/PolicyEngine/policyengine-us/pull/10752 state: unreviewed - note: PE-US follows the workbooks; verify actual agency payments and reconcile FY2025-FY2026 ordering with the - regulation. + note: PE-US follows the workbooks for heating and cooling; verify actual agency payments and reconcile the + FY2025-FY2026 ordering with the regulation. - id: us-la:blind-deaf-disabled-additional-exemption program: income-tax years: