diff --git a/changelog.d/liheap-fpg-floor-in-md-riv.fixed.md b/changelog.d/liheap-fpg-floor-in-md-riv.fixed.md new file mode 100644 index 00000000000..841a6b6e883 --- /dev/null +++ b/changelog.d/liheap-fpg-floor-in-md-riv.fixed.md @@ -0,0 +1 @@ +Apply the federal 110% poverty guideline floor to Indiana EAP, Maryland MEAP and Riverside County LIHEAP income eligibility. diff --git a/policyengine_us/parameters/gov/local/ca/riv/cap/liheap/fpg_year_lag.yaml b/policyengine_us/parameters/gov/local/ca/riv/cap/liheap/fpg_year_lag.yaml new file mode 100644 index 00000000000..a79f8303f37 --- /dev/null +++ b/policyengine_us/parameters/gov/local/ca/riv/cap/liheap/fpg_year_lag.yaml @@ -0,0 +1,20 @@ +description: Riverside County uses the poverty guidelines from this many years before the model year for the federal income floor under the Low Income Home Energy Assistance Program. +# HHS makes each January's guidelines mandatory for LIHEAP from the next +# October 1, so model period 2025 (FY2025) uses the 2024 guidelines and 2026 +# (FY2026) the 2025 guidelines. The county's own limit uses state median +# income. +values: + 2021-10-01: 1 +metadata: + unit: year + period: year + label: Riverside County LIHEAP poverty guideline year lag + reference: + - title: LIHEAP Information Memoranda, annual poverty guideline memoranda + href: https://acf.gov/ocs/policy-guidance/liheap-information-memoranda + - title: LIHEAP IM 2024-02, Attachment 2, poverty guidelines for mandatory use in FY25 + href: https://acf.gov/sites/default/files/documents/ocs/COMM_LIHEAP_IM%202024-02_Att2FPGTables_States.pdf#page=1 + - title: LIHEAP IM 2025-02, Attachment 2, poverty guidelines for mandatory use in FY26 + href: https://acf.gov/sites/default/files/documents/ocs/COMM_LIHEAP_IM2025-02_FPGSte-Table_Att2.pdf#page=1 + - title: LIHEAP IM 2026-01, poverty guidelines for optional use in FY26 and mandatory use in FY27 + href: https://acf.gov/ocs/policy-guidance/liheap-im2026-01-federal-poverty-guidelines-fpg-and-state-median-income-smi diff --git a/policyengine_us/parameters/gov/states/in/ihcda/eap/eligibility/fpg_year_lag.yaml b/policyengine_us/parameters/gov/states/in/ihcda/eap/eligibility/fpg_year_lag.yaml new file mode 100644 index 00000000000..bb567b174a3 --- /dev/null +++ b/policyengine_us/parameters/gov/states/in/ihcda/eap/eligibility/fpg_year_lag.yaml @@ -0,0 +1,17 @@ +description: Indiana uses the poverty guidelines from this many years before the model year for the federal income floor under the Low Income Home Energy Assistance Program. +# HHS makes each January's guidelines mandatory for LIHEAP from the next +# October 1, so model period 2025 (PY2025) uses the 2024 guidelines and 2026 +# (PY2026) the 2025 guidelines. Indiana's own limit uses state median income. +values: + 2024-10-01: 1 +metadata: + unit: year + period: year + label: Indiana EAP poverty guideline year lag + reference: + - title: LIHEAP IM 2024-02, Attachment 2, poverty guidelines for mandatory use in FY25 + href: https://acf.gov/sites/default/files/documents/ocs/COMM_LIHEAP_IM%202024-02_Att2FPGTables_States.pdf#page=1 + - title: LIHEAP IM 2025-02, Attachment 2, poverty guidelines for mandatory use in FY26 + href: https://acf.gov/sites/default/files/documents/ocs/COMM_LIHEAP_IM2025-02_FPGSte-Table_Att2.pdf#page=1 + - title: LIHEAP IM 2026-01, poverty guidelines for optional use in FY26 and mandatory use in FY27 + href: https://acf.gov/ocs/policy-guidance/liheap-im2026-01-federal-poverty-guidelines-fpg-and-state-median-income-smi diff --git a/policyengine_us/tests/policy/baseline/gov/local/ca/riv/cap/liheap/ca_riv_liheap_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/local/ca/riv/cap/liheap/ca_riv_liheap_eligible.yaml index 03d6a6cf399..b352237ffef 100644 --- a/policyengine_us/tests/policy/baseline/gov/local/ca/riv/cap/liheap/ca_riv_liheap_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/local/ca/riv/cap/liheap/ca_riv_liheap_eligible.yaml @@ -19,3 +19,50 @@ output: hhs_smi: 103_856 ca_riv_liheap_eligible: false + +- name: Case 3, federal 110% poverty floor above the 60% state median income limit at size 21. + period: 2026 + input: + state_code: [CA, CA, CA, CA, CA] + in_riv: [true, true, true, true, true] + spm_unit_size: [21, 21, 21, 21, 20] + ca_riv_liheap_countable_income: [136_000, 138_214.99, 138_215, 138_216, 133_800] + output: + # 42 U.S.C. 8624(b)(2)(B) bars excluding a household on income below 110% + # of the poverty guideline. FY2026 uses the 2025 guidelines (LIHEAP IM + # 2025-02): 15,650 + 5,500 x 20 = 125,650 for 21 people, and 110% is + # 138,215. The 60% SMI limit for 21 people is 136,088.93. + # 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 20 people, whose 133,782.34 limit exceeds the 132,165 + # floor (110% of 120,150), so the limit still applies: not eligible. + ca_riv_liheap_eligible: [true, true, false, false, false] + +- name: Case 4, the floor uses the poverty guidelines from the year before the model year. + period: 2026 + input: + state_code: CA + in_riv: true + spm_unit_size: 21 + ca_riv_liheap_countable_income: 140_000 + output: + # FY2026 uses the 2025 guidelines, so the floor is 138,215. The 2026 + # guidelines would give 110% x (15,960 + 5,680 x 20) = 142,516. + ca_riv_liheap_eligible: false + +- name: Case 5, FY2025 uses the 2024 poverty guidelines for the floor. + period: 2025 + input: + state_code: [CA, CA] + in_riv: [true, true] + spm_unit_size: [20, 20] + ca_riv_liheap_countable_income: [129_007.99, 129_008] + output: + # LIHEAP IM 2024-02 makes the 2024 guidelines mandatory in FY2025: + # 110% x (15,060 + 5,380 x 19) = 129,008 for 20 people, above the + # 127,290.74 FY2025 60% SMI limit. The 2025 CSD table (Final Plan PDF page + # 180) prints a monthly top of 10,607.47 for 20 people, also below the + # 10,750.67 monthly floor. + ca_riv_liheap_eligible: [true, false] diff --git a/policyengine_us/tests/policy/baseline/gov/states/in/ihcda/eap/in_eap.yaml b/policyengine_us/tests/policy/baseline/gov/states/in/ihcda/eap/in_eap.yaml index 4ac6644c22d..d59a6e468ec 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/in/ihcda/eap/in_eap.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/in/ihcda/eap/in_eap.yaml @@ -341,3 +341,27 @@ output: in_eap_income: [15_080, 15_080, 15_080] in_eap: [125, 125, 175] +- name: Case 17, a household that qualifies only through the federal 110% poverty floor receives the highest-income award. + period: 2026 + input: + state_code: [IN, IN] + age: [40, 40] + ssi: [0, 0] + social_security: [0, 0] + receives_ssi: [false, false] + receives_medicaid: [false, false] + rent: [0, 0] + in_eap_household_size: [16, 16] + in_eap_income: [106_000, 107_965] + heating_type: [NATURAL_GAS, NATURAL_GAS] + gas_expense: [1_000, 1_000] + pre_subsidy_electricity_expense: [1_000, 1_000] + output: + # Both incomes exceed the 104,543 size-16 limit. Household 1 is below the + # 107,965 floor (110% of the 2025 guideline of 98,150) and qualifies under + # 42 U.S.C. 8624(b)(2)(B). Household 2 is at the floor and does not qualify. + in_eap_eligible: [true, false] + # Household 1 is above the middle income band: 2 income points + 3 + # single-family points + 1 natural gas point = 6 x $25 = $150, plus the + # $50 higher-income electric allowance. + in_eap: [200, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/in/ihcda/eap/in_eap_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/in/ihcda/eap/in_eap_eligible.yaml index 981055ff692..9bb6081d328 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/in/ihcda/eap/in_eap_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/in/ihcda/eap/in_eap_eligible.yaml @@ -92,3 +92,45 @@ in_eap_income: 74_640 in_eap_eligible: false in_eap: 0 +- name: Case 6, federal 110% poverty floor above the 60% state median income limit at size 16. + period: 2026 + input: + state_code: [IN, IN, IN, IN, IN] + age: [40, 40, 40, 40, 40] + in_eap_household_size: [16, 16, 16, 16, 15] + in_eap_income: [104_543, 107_964.99, 107_965, 107_966, 102_608] + output: + # 42 U.S.C. 8624(b)(2)(B) bars excluding a household on income below 110% + # of the poverty guideline. PY2026 uses the 2025 guidelines (LIHEAP IM + # 2025-02): 15,650 + 5,500 x 15 = 98,150 for 16 people, and 110% is + # 107,965. The 60% SMI limit for 16 people is 104,543. + # Household 1 is at 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 15 people, whose 102,607 limit exceeds the 101,915 floor + # (110% of 92,650), so the limit still applies: not eligible. + in_eap_eligible: [true, true, false, false, false] +- name: Case 7, the floor uses the poverty guidelines from the year before the model year. + period: 2026 + input: + state_code: IN + age: 40 + in_eap_household_size: 16 + in_eap_income: 110_000 + output: + # PY2026 uses the 2025 guidelines, so the floor is 107,965. The 2026 + # guidelines would give 110% x (15,960 + 5,680 x 15) = 111,276. + in_eap_eligible: false +- name: Case 8, PY2025 uses the 2024 poverty guidelines for the floor. + period: 2025 + input: + state_code: [IN, IN] + age: [40, 40] + in_eap_household_size: [15, 15] + in_eap_income: [99_417.99, 99_418] + output: + # LIHEAP IM 2024-02 makes the 2024 guidelines mandatory in FY2025: + # 110% x (15,060 + 5,380 x 14) = 99,418 for 15 people, above the 98,571 + # PY2025 60% SMI limit. + in_eap_eligible: [true, false] diff --git a/policyengine_us/tests/policy/baseline/gov/states/md/dhs/meap/eligibility/md_meap_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/md/dhs/meap/eligibility/md_meap_eligible.yaml index fce98b98b2d..011482a852d 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/md/dhs/meap/eligibility/md_meap_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/md/dhs/meap/eligibility/md_meap_eligible.yaml @@ -122,3 +122,53 @@ # row ACF LIHEAP IM 2025-02 Attachment 4 (page 5) prints. $135,931 is over the # limit although it is below 200% of the eleven-person guideline ($141,300). md_meap_eligible: [true, false] + +- name: Case 8, federal 110% poverty floor above the 60% state median income limit at size 29. + period: 2026 + input: + state_code: [MD, MD, MD, MD, MD] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] + gas_expense: [1_150, 1_150, 1_150, 1_150, 1_150] + md_meap_household_size: [29, 29, 29, 29, 28] + md_meap_countable_income: [185_864, 186_614.99, 186_615, 186_616, 183_091] + md_meap_categorically_eligible: [false, false, false, false, false] + output: + # 42 U.S.C. 8624(b)(2)(B) bars excluding a household on income below 110% + # of the poverty guideline. FY26 uses the 2025 guidelines (md_meap_fpg): + # 15,650 + 5,500 x 28 = 169,650 for 29 people, and 110% is 186,615. The + # 60% SMI limit for 29 people is 185,864. + # Household 1 is at 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 28 people, whose 183,090 limit exceeds the 180,565 floor + # (110% of 164,150), so the limit still applies: not eligible. + md_meap_eligible: [true, true, false, false, false] + +- name: Case 9, the floor uses the poverty guidelines from the year before the model year. + period: 2026 + input: + state_code: MD + heating_type: NATURAL_GAS + gas_expense: 1_150 + md_meap_household_size: 29 + md_meap_countable_income: 190_000 + md_meap_categorically_eligible: false + output: + # FY26 uses the 2025 guidelines, so the floor is 186,615. The 2026 + # guidelines would give 110% x (15,960 + 5,680 x 28) = 192,500. + md_meap_eligible: false + +- name: Case 10, FY25 uses the 2024 poverty guidelines for the floor. + period: 2025 + input: + state_code: [MD, MD] + heating_type: [NATURAL_GAS, NATURAL_GAS] + gas_expense: [1_150, 1_150] + md_meap_household_size: [29, 29] + md_meap_countable_income: [182_269.99, 182_270] + md_meap_categorically_eligible: [false, false] + output: + # 110% x (15,060 + 5,380 x 28) = 182,270 for 29 people, above the 179,993 + # FY25 60% SMI limit. + md_meap_eligible: [true, false] diff --git a/policyengine_us/tests/policy/baseline/gov/states/md/dhs/meap/md_meap.yaml b/policyengine_us/tests/policy/baseline/gov/states/md/dhs/meap/md_meap.yaml index 5ed59cf4e52..55634af0952 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/md/dhs/meap/md_meap.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/md/dhs/meap/md_meap.yaml @@ -254,3 +254,21 @@ # the plan's multiplier. # Oil, Level 7: the $25 nominal grant, printed $25. md_meap: [110, 732, 391, 1_078, 25] + +- name: Case 15, a household that qualifies only through the federal 110% poverty floor is paid at its poverty level. + period: 2026 + input: + state_code: [MD, MD] + heating_type: [NATURAL_GAS, NATURAL_GAS] + gas_expense: [1_150, 1_150] + md_meap_household_size: [29, 29] + md_meap_countable_income: [186_000, 186_615] + md_meap_categorically_eligible: [false, false] + output: + # Both incomes exceed the 185,864 size-29 limit. Household 1 is below the + # 186,615 floor (110% of the 2025 guideline of 169,650) and qualifies under + # 42 U.S.C. 8624(b)(2)(B). Household 2 is at the floor and does not qualify. + md_meap_eligible: [true, false] + # 186,000 / 169,650 = 109.6% of the guideline, Level 4 (over 100% to 150%). + # The matrix pays $360 for gas at Level 4. + md_meap: [360, 0] diff --git a/policyengine_us/variables/gov/local/ca/riv/liheap/ca_riv_liheap_eligible.py b/policyengine_us/variables/gov/local/ca/riv/liheap/ca_riv_liheap_eligible.py index 852c34cbe96..88d19af9002 100644 --- a/policyengine_us/variables/gov/local/ca/riv/liheap/ca_riv_liheap_eligible.py +++ b/policyengine_us/variables/gov/local/ca/riv/liheap/ca_riv_liheap_eligible.py @@ -1,4 +1,5 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.hhs.tax_unit_fpg import fpg class ca_riv_liheap_eligible(Variable): @@ -7,12 +8,31 @@ class ca_riv_liheap_eligible(Variable): label = "Eligible for the California Riverside County LIHEAP" definition_period = YEAR defined_for = "in_riv" - reference = "https://capriverside.org/utility-assistance-program" + reference = ( + "https://capriverside.org/utility-assistance-program", + # 2025 HEAP and Fast Track payment table (PDF page 180). + "https://www.csd.ca.gov/Shared%20Documents/2025-Final-LIHEAP-State-Plan.pdf#page=180", + # 2026 HEAP and Fast Track payment table (PDF page 184). + "https://www.csd.ca.gov/Shared%20Documents/2026-Final-LIHEAP-State-Plan.pdf#page=184", + "https://www.law.cornell.edu/uscode/text/42/8624", + ) def formula(spm_unit, period, parameters): p = parameters(period).gov.hhs.liheap + p_riv = parameters(period).gov.local.ca.riv.cap.liheap state_median_income = spm_unit("hhs_smi", period) # The income concept is not clearly defined, assuming IRS gross income countable_income = spm_unit("ca_riv_liheap_countable_income", period) smi_limit = state_median_income * p.smi_limit - return countable_income <= smi_limit + # 42 U.S.C. 8624(b)(2)(B) bars excluding a household on income below 110% + # of the poverty guideline. California's printed CSD tables fall below + # that floor at sizes 20 to 25 in 2025 and 21 to 25 in 2026, and the + # modeled 60% SMI limit does so from size 20 in 2025 and size 21 in + # 2026 and 2027. + 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_riv.fpg_year_lag + ) + floor = np.round(guideline * p.fpg_floor, 2) + return (countable_income <= smi_limit) | (countable_income < floor) diff --git a/policyengine_us/variables/gov/states/in/ihcda/eap/in_eap_eligible.py b/policyengine_us/variables/gov/states/in/ihcda/eap/in_eap_eligible.py index 208fe21f869..f9d73a6ebea 100644 --- a/policyengine_us/variables/gov/states/in/ihcda/eap/in_eap_eligible.py +++ b/policyengine_us/variables/gov/states/in/ihcda/eap/in_eap_eligible.py @@ -1,4 +1,5 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.hhs.tax_unit_fpg import fpg class in_eap_eligible(Variable): @@ -11,12 +12,23 @@ class in_eap_eligible(Variable): # Section 3.1 (page 23), Section 4.4 (page 29), Section 8.3 (pages 69-70) and # Section 8.8 (pages 73-75). "https://www.in.gov/ihcda/files/Indiana-LIHEAP-Intake-and-Operations-Program-Manual-PY2026.pdf#page=23", + "https://www.law.cornell.edu/uscode/text/42/8624", ) def formula(spm_unit, period, parameters): p = parameters(period).gov.states["in"].ihcda.eap income = spm_unit("in_eap_income", period) limit = np.floor(spm_unit("in_eap_smi", period) * p.eligibility.income_limit) + # 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 15 in PY2025 and size 16 in PY2026 and PY2027. + p_hhs = parameters(period).gov.hhs.liheap + size = spm_unit("in_eap_household_size", period) + state_group = spm_unit.household("state_group_str", period) + guideline = fpg( + size, state_group, period, parameters, year_lag=p.eligibility.fpg_year_lag + ) + floor = np.round(guideline * p_hhs.fpg_floor, 2) # Section 4.4 permits an ineligible adult to apply for eligible household members. # Emancipated minors, residency duration, eviction writs, SSNs, and sanctions # cannot be determined here. Section 8.8 separates household eligibility from @@ -24,5 +36,5 @@ def formula(spm_unit, period, parameters): return ( (add(spm_unit, period, ["is_citizen_or_legal_immigrant"]) > 0) & spm_unit.any(spm_unit.members("age", period) >= p.eligibility.adult_age) - & (income <= limit) + & ((income <= limit) | (income < floor)) ) diff --git a/policyengine_us/variables/gov/states/md/dhs/meap/eligibility/md_meap_eligible.py b/policyengine_us/variables/gov/states/md/dhs/meap/eligibility/md_meap_eligible.py index 57967995fef..1f55c315d91 100644 --- a/policyengine_us/variables/gov/states/md/dhs/meap/eligibility/md_meap_eligible.py +++ b/policyengine_us/variables/gov/states/md/dhs/meap/eligibility/md_meap_eligible.py @@ -12,6 +12,7 @@ class md_meap_eligible(Variable): "https://regs.maryland.gov/us/md/exec/comar/07.03.21.06", # PDF pages 11, 12, 16, 38, 39, 108. "https://dhs.maryland.gov/documents/OHEP/OHEP-Operations-Manual.pdf#page=11", + "https://www.law.cornell.edu/uscode/text/42/8624", ) documentation = ( "Regular heating has no resource test. Application documentation, emancipation " @@ -28,11 +29,19 @@ def formula(spm_unit, period, parameters): subsidized = spm_unit( "receives_housing_assistance", period ) | spm_unit.household("is_in_public_housing", period) + income = spm_unit("md_meap_countable_income", period) + # 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 29 in FY2025 to FY2027. md_meap_fpg already uses the + # guideline of the calendar year before the model year. + p_hhs = parameters(period).gov.hhs.liheap + floor = np.round(spm_unit("md_meap_fpg", period) * p_hhs.fpg_floor, 2) # Manual page 38: categorical eligibility waives the income test but # not the other denials. - income_eligible = spm_unit("md_meap_categorically_eligible", period) | ( - spm_unit("md_meap_countable_income", period) - <= spm_unit("md_meap_income_limit", period) + income_eligible = ( + spm_unit("md_meap_categorically_eligible", period) + | (income <= spm_unit("md_meap_income_limit", period)) + | (income < floor) ) # Manual page 108: the household must have at least one eligible # (citizen or qualified alien) member.