Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/dc-liheap-cooling.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DC LIHEAP Regular benefit now pays households with no heating cost that pay for cooling, from the Electric row capped at their electricity bill. DC LIHEAP income limits now use HHS's 60% state median income amounts, rounded down by household size, as DOEE publishes them.
1 change: 1 addition & 0 deletions policyengine_us/programs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1144,6 +1144,7 @@ programs:
full_name: DC Low Income Home Energy Assistance Program
variable: dc_liheap_payment
parameter_prefix: gov.states.dc.doee.liheap
notes: One-time Regular benefit from the DOEE matrix; it also covers cooling-only households, paying the Electric row capped at the electricity bill.
- state: DE
status: partial
name: Delaware LIHEAP
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
state_code: DC
output:
hhs_smi: 152_043
# income limit = 152_043 * 0.6 = 91,225.8
# income limit = 152_043 * 0.6 = 91,225.8, rounded down to 91,225
dc_liheap_eligible: true

- name: Case 2, household size of 4, ineligible.
Expand All @@ -28,12 +28,13 @@
# 42 U.S.C. 8624(b)(2)(B) bars excluding a household on income below 110%
# of the poverty guideline. FY2024 uses the 2023 guidelines (LIHEAP IM
# 2023-01): 14,580 + 5,140 x 35 = 194,480 for 36 people, and 110% is
# 213,928. The 60% SMI limit for 36 people is 212,669.78.
# 213,928. The 60% SMI limit for 36 people is 159,662 x 0.6 = 95,797
# (rounded down) x 2.22 = 212,669 (rounded down).
# Household 1 is below the 60% SMI limit: eligible.
# Household 2 is one cent below the floor: eligible.
# Household 3 is exactly at the floor, which is not below 110%: not eligible.
# Household 4 is above the floor: not eligible.
# Household 5 has 35 people, whose 209,795.87 limit exceeds the 208,274
# Household 5 has 35 people, whose 209,795 limit exceeds the 208,274
# floor (110% of 189,340), so the limit still applies: not eligible.
dc_liheap_eligible: [true, true, false, false, false]

Expand All @@ -56,5 +57,17 @@
irs_gross_income: [212_244.99, 212_245]
output:
# 110% x (13,590 + 4,720 x 38) = 212,245 for 39 people, above the
# 210,731.60 FY2023 60% SMI limit.
# 210,729 FY2023 60% SMI limit (91,225 x 2.31, rounded down).
dc_liheap_eligible: [true, false]

- name: Case 6, the income limit is HHS's 60% state median income amount, rounded down.
period: 2026
input:
state_code: [DC, DC]
spm_unit_size: [2, 2]
irs_gross_income: [80_869, 80_870]
output:
# FY2026: 198,211 x 0.6 = 118,926 (rounded down) x 0.68 = 80,869 (rounded
# down), DOEE's published two-person limit. The unrounded 80,870.09 would
# admit $80,870.
dc_liheap_eligible: [true, false]
Original file line number Diff line number Diff line change
Expand Up @@ -337,3 +337,272 @@
# canonical heating_type (gas bill 1_400): min(1_500, 1_400) = 1_400
dc_liheap_heating_type: OIL
dc_liheap_payment: 1_400

# Cooling-only households, FY2026 matrix (period 2026). DOEE's one-time Regular
# benefit helps with heating and cooling costs (DOEE LIHEAP page; FAQ Q3), and
# the matrix amount is a maximum benefit. A home without heating that pays its
# own electricity bill for cooling takes the Electric row, capped at that bill.
# Income levels use 5_000 steps (level 2 up to 10_000, level 3 up to 15_000).
# Wages are at the DC minimum wage of 17.95 an hour from July 1, 2025:
# 15 hours a week is 15 x 52 x 17.95 = 14_001 and 5 hours is 4_667; full time
# at 40.00 an hour is 83_200. Electricity of 1_358.76 a year is EIA's 2024 DC
# average residential bill of 113.23 a month (Table 5A,
# https://www.eia.gov/electricity/sales_revenue_price/pdf/table_5A.pdf).
- name: Cooling only, a home without heating takes the electricity row.
period: 2026
absolute_error_margin: 0.01
input:
people:
person1:
age: 40
employment_income: 14_001
spm_units:
spm_unit:
members: [person1]
heating_type: NONE
has_cooling_expense: true
pre_subsidy_electricity_expense: 1_358.76
dc_liheap_housing_type: SINGLE_FAMILY
households:
household:
members: [person1]
state_code: DC
output:
dc_liheap_heating_type: ELECTRICITY
# 10_000 < 14_001 <= 15_000
dc_liheap_income_level: 3
# SF, level 3, one person: Electric 440; min(440, 1_358.76) = 440
dc_liheap_payment: 440

- name: Cooling only, the electricity bill caps the electricity row.
period: 2026
absolute_error_margin: 0.01
input:
people:
person1:
age: 40
employment_income: 4_667
person2:
age: 38
spm_units:
spm_unit:
members: [person1, person2]
heating_type: NONE
has_cooling_expense: true
pre_subsidy_electricity_expense: 1_358.76
dc_liheap_housing_type: SINGLE_FAMILY
households:
household:
members: [person1, person2]
state_code: DC
output:
# 4_667 <= 5_000
dc_liheap_income_level: 1
# SF, level 1, two people: Electric 1_481; min(1_481, 1_358.76) = 1_358.76
dc_liheap_payment: 1_358.76

- name: No heating and no cooling costs pays nothing despite an electricity bill.
period: 2026
absolute_error_margin: 0.01
input:
people:
person1:
age: 40
employment_income: 14_001
spm_units:
spm_unit:
members: [person1]
heating_type: NONE
has_cooling_expense: false
pre_subsidy_electricity_expense: 1_358.76
dc_liheap_housing_type: SINGLE_FAMILY
households:
household:
members: [person1]
state_code: DC
output:
# NONE without cooling: heating_expense = 0; min(440, 0) = 0
dc_liheap_payment: 0

- name: Cooling only, a heated home with no heating cost takes the electricity row.
period: 2026
absolute_error_margin: 0.01
input:
people:
person1:
age: 40
employment_income: 14_001
spm_units:
spm_unit:
members: [person1]
# Wood the household gathers itself, so there is no heating bill.
heating_type: WOOD
has_cooling_expense: true
pre_subsidy_electricity_expense: 1_358.76
dc_liheap_housing_type: SINGLE_FAMILY
households:
household:
members: [person1]
state_code: DC
output:
# DOEE sets no fuel condition on the benefit, and its only energy cost is
# cooling. SF, level 3, one person: Electric 440; min(440, 1_358.76) = 440.
dc_liheap_payment: 440

- name: Cooling only, a gas-heated home with no gas bill takes the electricity amount, not its gas row.
period: 2026
absolute_error_margin: 0.01
input:
people:
person1:
age: 40
employment_income: 14_001
spm_units:
spm_unit:
members: [person1]
# Gas heat with no gas bill of its own, so there is no heating cost.
heating_type: NATURAL_GAS
has_cooling_expense: true
pre_subsidy_electricity_expense: 1_358.76
dc_liheap_housing_type: SINGLE_FAMILY
households:
household:
members: [person1]
state_code: DC
output:
dc_liheap_heating_type: GAS
# Its only energy cost is cooling, so it is paid the Electric amount, not
# the Gas row (1_193) against the electricity bill. SF, level 3, one
# person: Electric 440; min(440, 1_358.76) = 440.
dc_liheap_payment: 440

- name: A heated home with a heating bill and a cooling cost keeps its heating row.
period: 2026
absolute_error_margin: 0.01
input:
people:
person1:
age: 40
employment_income: 14_001
spm_units:
spm_unit:
members: [person1]
heating_type: NATURAL_GAS
has_cooling_expense: true
gas_expense: 300
pre_subsidy_electricity_expense: 1_358.76
dc_liheap_housing_type: SINGLE_FAMILY
households:
household:
members: [person1]
state_code: DC
output:
# SF, level 3, one person: Gas 1_193, capped at the 300 heating bill. The
# cooling cost does not move it to the electricity bill.
dc_liheap_payment: 300

- name: A heat-in-rent tenant with a cooling cost keeps the flat heat-in-rent amount.
period: 2026
absolute_error_margin: 0.01
input:
people:
person1:
age: 40
employment_income: 14_001
spm_units:
spm_unit:
members: [person1]
heating_type: NATURAL_GAS
heat_expense_included_in_rent: true
has_cooling_expense: true
pre_subsidy_electricity_expense: 1_358.76
dc_liheap_housing_type: SINGLE_FAMILY
households:
household:
members: [person1]
state_code: DC
output:
dc_liheap_heating_type: HEAT_IN_RENT
# Heat in the rent keeps the household off the cooling route: it gets the
# flat heat-in-rent amount of 200, with no expense cap, not the Electric
# amount of 440.
dc_liheap_payment: 200

- name: Cooling only, income above the limit pays nothing.
period: 2026
absolute_error_margin: 0.01
input:
people:
person1:
age: 40
employment_income: 83_200
spm_units:
spm_unit:
members: [person1]
heating_type: NONE
has_cooling_expense: true
pre_subsidy_electricity_expense: 1_358.76
dc_liheap_housing_type: SINGLE_FAMILY
households:
household:
members: [person1]
state_code: DC
output:
# 83_200 exceeds 60% of the one-person state median income
dc_liheap_eligible: false
dc_liheap_payment: 0

# The cooling route needs a known heating type. An unspecified fuel keeps the
# legacy adapter even with a cooling cost: the person-level heating total of
# 300 (as in the legacy precedence case above) caps the Electric row, not the
# electricity bill.
- name: Cooling cost with an unspecified fuel keeps the legacy adapter.
period: 2026
absolute_error_margin: 0.01
input:
people:
person1:
age: 40
employment_income: 14_001
heating_expense_person: 300
spm_units:
spm_unit:
members: [person1]
heating_type: UNSPECIFIED
has_cooling_expense: true
pre_subsidy_electricity_expense: 1_358.76
dc_liheap_housing_type: SINGLE_FAMILY
households:
household:
members: [person1]
state_code: DC
output:
dc_liheap_heating_type: ELECTRICITY
# SF, level 3, one person: Electric 440; legacy expense 300; min(440, 300) = 300
dc_liheap_payment: 300

- name: Cooling cost with an unspecified fuel on the deprecated gas row and no gas bill pays nothing.
period: 2026
absolute_error_margin: 0.01
input:
people:
person1:
age: 40
employment_income: 14_001
spm_units:
spm_unit:
members: [person1]
heating_type: UNSPECIFIED
dc_liheap_heating_type: GAS
has_cooling_expense: true
pre_subsidy_electricity_expense: 1_358.76
dc_liheap_housing_type: SINGLE_FAMILY
households:
household:
members: [person1]
state_code: DC
output:
# An unspecified fuel stays off the cooling route. The legacy adapter takes
# the gas bill for the direct GAS row, which is 0: min(1_193, 0) = 0, as on
# main.
dc_liheap_payment: 0
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from policyengine_us.model_api import *
from policyengine_us.variables.gov.hhs.hhs_smi import liheap_smi_limit
from policyengine_us.variables.gov.hhs.tax_unit_fpg import fpg


Expand All @@ -10,20 +11,24 @@ class dc_liheap_eligible(Variable):
defined_for = StateCode.DC
reference = (
"https://doee.dc.gov/liheap",
# FY2026 federal amounts
"https://acf.gov/sites/default/files/documents/ocs/COMM_LIHEAP_IM2025-02_SMIStateTable_Att4.pdf#page=1",
"https://www.law.cornell.edu/uscode/text/42/8624",
)

def formula(spm_unit, period, parameters):
p = parameters(period).gov.hhs.liheap
p_dc = parameters(period).gov.states.dc.doee.liheap
state_median_income = spm_unit("hhs_smi", period)
# The income concept is not clearly defined, assuming IRS gross income
income = add(spm_unit, period, ["irs_gross_income"])
smi_limit = state_median_income * p.smi_limit
size = spm_unit("spm_unit_size", period)
state = spm_unit.household("state_code_str", period)
# HHS's 60% state median income amounts, rounded down by household
# size, match DOEE's published limits (FY2026: $80,869 for two people).
smi_limit = liheap_smi_limit(size, state, p.smi_limit, period, parameters)
# 42 U.S.C. 8624(b)(2)(B) bars excluding a household on income below 110%
# of the poverty guideline. The 60% SMI limit falls below that floor from
# household size 34 to 39 in 2022 to 2024.
size = spm_unit("spm_unit_size", period)
state_group = spm_unit.household("state_group_str", period)
guideline = fpg(
size, state_group, period, parameters, year_lag=p_dc.fpg_year_lag
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class dc_liheap_heating_type(Variable):
default_value = DCLIHEAPHeatingType.ELECTRICITY
definition_period = YEAR
label = "Household heating types for DC LIHEAP"
documentation = "Derived from heat_expense_included_in_rent and the canonical heating_type input. Fuel oil, kerosene and propane take the oil row; wood, coal, other and solar heating have no DC matrix row and take the electricity row, as do UNSPECIFIED and NONE. Setting this directly is deprecated during the vocabulary migration and changes only the matrix row: the expense cap still follows the canonical heating_type."
documentation = "Derived from heat_expense_included_in_rent and the canonical heating_type input. Fuel oil, kerosene and propane take the oil row; wood, coal, other and solar heating have no DC matrix row and take the electricity row, as do UNSPECIFIED and NONE. Setting this directly is deprecated during the vocabulary migration and changes only the matrix row: the expense cap still follows the canonical heating_type, and a cooling-only household (a known heating_type, no heat in rent, no heating cost and a cooling cost) takes the electricity amount whatever this row is."
defined_for = StateCode.DC

def formula(spm_unit, period, parameters):
Expand Down
Loading
Loading