From d3b03217a6b17e204791b78f9ecbd62befe27162 Mon Sep 17 00:00:00 2001 From: Ziming Date: Sat, 3 Oct 2026 19:49:21 -0400 Subject: [PATCH 1/9] Add SD regular heating LIHEAP components --- changelog.d/sd-liheap.added.md | 1 + .../liheap/income/earned_income_min_age.yaml | 10 + .../sd/dss/liheap/income/fpg_year_lag.yaml | 10 + .../liheap/income/limits/fpg_lower_rate.yaml | 12 + .../liheap/income/limits/fpg_upper_rate.yaml | 12 + .../sd/dss/liheap/income/limits/smi_rate.yaml | 12 + .../income/secondary_school_age_limit.yaml | 10 + .../sd/dss/liheap/income/sources/earned.yaml | 19 + .../dss/liheap/income/sources/unearned.yaml | 27 ++ .../states/sd/dss/liheap/payment/direct.yaml | 120 ++++++ .../liheap/payment/heat_in_rent/amount.yaml | 19 + .../liheap/payment/heat_in_rent/months.yaml | 10 + .../payment/heat_in_rent/rent_share.yaml | 10 + .../liheap/payment/income_threshold_rate.yaml | 12 + .../dss/liheap/payment/regions/region_1.yaml | 30 ++ .../dss/liheap/payment/regions/region_2.yaml | 29 ++ .../dss/liheap/payment/regions/region_3.yaml | 25 ++ .../dss/liheap/payment/regions/region_4.yaml | 23 ++ policyengine_us/programs.yaml | 6 + .../eligibility/sd_liheap_income_limit.yaml | 48 +++ .../income/sd_liheap_countable_income.yaml | 352 ++++++++++++++++++ .../payment/sd_liheap_base_payment.yaml | 117 ++++++ .../sd_liheap_heat_in_rent_payment.yaml | 211 +++++++++++ .../payment/sd_liheap_income_threshold.yaml | 36 ++ .../dss/liheap/payment/sd_liheap_region.yaml | 50 +++ .../eligibility/sd_liheap_income_limit.py | 40 ++ .../income/sd_liheap_countable_income.py | 42 +++ .../liheap/payment/sd_liheap_base_payment.py | 52 +++ .../payment/sd_liheap_heat_in_rent_payment.py | 40 ++ .../payment/sd_liheap_income_threshold.py | 36 ++ .../sd/dss/liheap/payment/sd_liheap_region.py | 29 ++ 31 files changed, 1450 insertions(+) create mode 100644 changelog.d/sd-liheap.added.md create mode 100644 policyengine_us/parameters/gov/states/sd/dss/liheap/income/earned_income_min_age.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/liheap/income/fpg_year_lag.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/fpg_lower_rate.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/fpg_upper_rate.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/smi_rate.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/liheap/income/secondary_school_age_limit.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/earned.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/unearned.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/liheap/payment/direct.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/liheap/payment/heat_in_rent/amount.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/liheap/payment/heat_in_rent/months.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/liheap/payment/heat_in_rent/rent_share.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/liheap/payment/income_threshold_rate.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_1.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_2.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_3.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_4.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/income/sd_liheap_countable_income.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_base_payment.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_heat_in_rent_payment.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_income_threshold.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_region.yaml create mode 100644 policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.py create mode 100644 policyengine_us/variables/gov/states/sd/dss/liheap/income/sd_liheap_countable_income.py create mode 100644 policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_base_payment.py create mode 100644 policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_heat_in_rent_payment.py create mode 100644 policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_income_threshold.py create mode 100644 policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_region.py diff --git a/changelog.d/sd-liheap.added.md b/changelog.d/sd-liheap.added.md new file mode 100644 index 00000000000..49a3f5a770d --- /dev/null +++ b/changelog.d/sd-liheap.added.md @@ -0,0 +1 @@ +Add South Dakota LIHEAP income, income thresholds, county regions, and direct-heating and heat-in-rent payment components. diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/earned_income_min_age.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/earned_income_min_age.yaml new file mode 100644 index 00000000000..d6a840b78a6 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/earned_income_min_age.yaml @@ -0,0 +1,10 @@ +description: South Dakota excludes earned income below this age under the Low Income Energy Assistance Program. +values: + 2025-10-01: 18 +metadata: + unit: year + period: year + label: South Dakota LIEAP earned income minimum age + reference: + - title: South Dakota Administrative Rule 67:15:01:18(1), Income exclusions + href: https://sdlegislature.gov/Rules/Administrative/67:15:01:18 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/fpg_year_lag.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/fpg_year_lag.yaml new file mode 100644 index 00000000000..3c8e65f9a3f --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/fpg_year_lag.yaml @@ -0,0 +1,10 @@ +description: South Dakota uses federal poverty guidelines from this many years before the modeled heating year for the Low Income Energy Assistance Program. The date records the FY2024 schedule in the annual model, not policy commencement; earlier backfilled years are unverified. +values: + 2024-01-01: 1 +metadata: + unit: year + period: year + label: South Dakota LIEAP poverty guideline year lag + reference: + - title: South Dakota FY2024 LIHEAP Benefit Matrix, pages 1-5, income thresholds reconciled to 2023 guidelines + href: https://liheapch.acf.gov/docs/2024/benefits-matricies/SD_BenefitMatrix_2024.pdf#page=1 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/fpg_lower_rate.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/fpg_lower_rate.yaml new file mode 100644 index 00000000000..ff09ce13ffe --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/fpg_lower_rate.yaml @@ -0,0 +1,12 @@ +description: South Dakota uses this federal poverty guideline rate as the lower bound of its income limit under the Low Income Energy Assistance Program. This numerical rule reproduces the FY2024 schedule and carries forward to FY2026; the date is an annual-model observation, not policy commencement. +values: + 2024-01-01: 1.5 +metadata: + unit: /1 + period: year + label: South Dakota LIEAP income limit lower poverty rate + reference: + - title: South Dakota FY2024 LIHEAP Benefit Matrix, pages 1-5, maximum income + href: https://liheapch.acf.gov/docs/2024/benefits-matricies/SD_BenefitMatrix_2024.pdf#page=1 + - title: South Dakota DSS, FY2026 three-month income limits + href: https://dss.sd.gov/economicassistance/energy_weatherization_assistance.aspx diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/fpg_upper_rate.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/fpg_upper_rate.yaml new file mode 100644 index 00000000000..426ac0a1537 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/fpg_upper_rate.yaml @@ -0,0 +1,12 @@ +description: South Dakota caps the income limit at this federal poverty guideline rate under the Low Income Energy Assistance Program. This numerical rule reproduces the FY2024 schedule and carries forward to FY2026; the date is an annual-model observation, not policy commencement. +values: + 2024-01-01: 2 +metadata: + unit: /1 + period: year + label: South Dakota LIEAP income limit upper poverty rate + reference: + - title: South Dakota FY2024 LIHEAP Benefit Matrix, pages 1-5, maximum income + href: https://liheapch.acf.gov/docs/2024/benefits-matricies/SD_BenefitMatrix_2024.pdf#page=1 + - title: South Dakota DSS, FY2026 three-month income limits + href: https://dss.sd.gov/economicassistance/energy_weatherization_assistance.aspx diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/smi_rate.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/smi_rate.yaml new file mode 100644 index 00000000000..3ac9ac69ecf --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/smi_rate.yaml @@ -0,0 +1,12 @@ +description: South Dakota applies this rate to four-person state median income before household-size adjustment, subject to poverty guideline bounds, under the Low Income Energy Assistance Program. Flooring before size adjustment and quarterly flooring reproduce the FY2024 and FY2026 tables; this is a reconciled numerical pattern. The date is an FY2024 annual-model observation, not policy commencement. +values: + 2024-01-01: 0.6 +metadata: + unit: /1 + period: year + label: South Dakota LIEAP income limit state median income rate + reference: + - title: South Dakota FY2024 LIHEAP Benefit Matrix, pages 1-5, maximum income + href: https://liheapch.acf.gov/docs/2024/benefits-matricies/SD_BenefitMatrix_2024.pdf#page=1 + - title: South Dakota DSS, FY2026 three-month income limits + href: https://dss.sd.gov/economicassistance/energy_weatherization_assistance.aspx diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/secondary_school_age_limit.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/secondary_school_age_limit.yaml new file mode 100644 index 00000000000..bbf9743a6b7 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/secondary_school_age_limit.yaml @@ -0,0 +1,10 @@ +description: South Dakota excludes earned income of secondary school students below this age under the Low Income Energy Assistance Program. +values: + 2025-10-01: 19 +metadata: + unit: year + period: year + label: South Dakota LIEAP secondary school earned income age limit + reference: + - title: South Dakota Administrative Rule 67:15:01:18(1), Income exclusions + href: https://sdlegislature.gov/Rules/Administrative/67:15:01:18 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/earned.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/earned.yaml new file mode 100644 index 00000000000..ff066b813eb --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/earned.yaml @@ -0,0 +1,19 @@ +description: South Dakota counts these earned income sources subject to its age and school exclusions under the Low Income Energy Assistance Program. +values: + 2025-10-01: + - employment_income + - self_employment_income + - sstb_self_employment_income + - farm_operations_income + - partnership_s_corp_income +# Use existing net business inputs and floor source losses without an additional +# business-expense deduction or reconstructing gross receipts. +metadata: + unit: list + period: year + label: South Dakota LIEAP earned income sources + reference: + - title: South Dakota Administrative Rule 67:15:01:18.01, Self-employed households + href: https://sdlegislature.gov/Rules/Administrative/67:15:01:18.01 + - title: South Dakota FY2026 LIHEAP State Plan, section 1.9, pages 6-7 + href: https://liheapch.acf.gov/docs/2026/state-plans/SD_Plan_2026.pdf#page=6 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/unearned.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/unearned.yaml new file mode 100644 index 00000000000..f906077ccad --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/unearned.yaml @@ -0,0 +1,27 @@ +description: South Dakota counts these unearned income sources for members of all ages under the Low Income Energy Assistance Program. +values: + 2025-10-01: + - social_security + - ssi + - pension_income + - survivor_benefits + - unemployment_compensation + - veterans_benefits + - child_support_received + - alimony_income + - rental_income + - farm_rent_income + - strike_benefits + - general_assistance + - sd_tanf +# State TANF is counted once at SPM level. Pension/SSA subcomponents are not +# added again. Interest, dividends, capital gains, and cash gifts are excluded. +metadata: + unit: list + period: year + label: South Dakota LIEAP unearned income sources + reference: + - title: South Dakota FY2026 LIHEAP State Plan, section 1.9, pages 6-7 (counted sources) + href: https://liheapch.acf.gov/docs/2026/state-plans/SD_Plan_2026.pdf#page=6 + - title: South Dakota Administrative Rule 67:15:01:18, Income exclusions + href: https://sdlegislature.gov/Rules/Administrative/67:15:01:18 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/direct.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/direct.yaml new file mode 100644 index 00000000000..8e69165d626 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/direct.yaml @@ -0,0 +1,120 @@ +description: South Dakota provides these direct-heating base payments by fuel, heating region, and income tier under the Low Income Energy Assistance Program. These are table ceilings before unpaid-charge limits and final eligibility. + +coal_wood: + 1: + 1: + 2025-10-01: 742 + 2: + 2025-10-01: 668 + 2: + 1: + 2025-10-01: 770 + 2: + 2025-10-01: 694 + 3: + 1: + 2025-10-01: 812 + 2: + 2025-10-01: 730 + 4: + 1: + 2025-10-01: 854 + 2: + 2025-10-01: 768 +electricity: + 1: + 1: + 2025-10-01: 1331 + 2: + 2025-10-01: 1198 + 2: + 1: + 2025-10-01: 1410 + 2: + 2025-10-01: 1270 + 3: + 1: + 2025-10-01: 1516 + 2: + 2025-10-01: 1369 + 4: + 1: + 2025-10-01: 1626 + 2: + 2025-10-01: 1461 +fuel_oil: + 1: + 1: + 2025-10-01: 2858 + 2: + 2025-10-01: 2573 + 2: + 1: + 2025-10-01: 3018 + 2: + 2025-10-01: 2715 + 3: + 1: + 2025-10-01: 3236 + 2: + 2025-10-01: 2910 + 4: + 1: + 2025-10-01: 3437 + 2: + 2025-10-01: 3093 +natural_gas: + 1: + 1: + 2025-10-01: 880 + 2: + 2025-10-01: 792 + 2: + 1: + 2025-10-01: 938 + 2: + 2025-10-01: 844 + 3: + 1: + 2025-10-01: 1007 + 2: + 2025-10-01: 907 + 4: + 1: + 2025-10-01: 1058 + 2: + 2025-10-01: 953 +propane: + 1: + 1: + 2025-10-01: 2499 + 2: + 2025-10-01: 2249 + 2: + 1: + 2025-10-01: 2658 + 2: + 2025-10-01: 2394 + 3: + 1: + 2025-10-01: 2834 + 2: + 2025-10-01: 2552 + 4: + 1: + 2025-10-01: 3066 + 2: + 2025-10-01: 2762 + +metadata: + unit: currency-USD + period: year + label: South Dakota LIEAP direct-heating base payment + breakdown: + - ['coal_wood', 'electricity', 'fuel_oil', 'natural_gas', 'propane'] + - range(1, 5) + - range(1, 3) + propagate_metadata_to_children: true + reference: + - title: South Dakota FY2026 LIHEAP Benefit Matrix, Own and Rent Cases + href: https://liheapch.acf.gov/docs/2026/benefits-matricies/SD_BenefitMatrix_2026.pdf#page=1 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/heat_in_rent/amount.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/heat_in_rent/amount.yaml new file mode 100644 index 00000000000..000011b1b47 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/heat_in_rent/amount.yaml @@ -0,0 +1,19 @@ +description: South Dakota provides no more than this heat-in-rent payment by heating region under the Low Income Energy Assistance Program. +1: + 2025-10-01: 800 +2: + 2025-10-01: 800 +3: + 2025-10-01: 850 +4: + 2025-10-01: 850 +metadata: + unit: currency-USD + period: year + label: South Dakota LIEAP heat-in-rent maximum by region + breakdown: + - range(1, 5) + propagate_metadata_to_children: true + reference: + - title: South Dakota FY2026 LIHEAP Benefit Matrix, Heat in Rent Cases + href: https://liheapch.acf.gov/docs/2026/benefits-matricies/SD_BenefitMatrix_2026.pdf diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/heat_in_rent/months.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/heat_in_rent/months.yaml new file mode 100644 index 00000000000..6ecbd7df353 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/heat_in_rent/months.yaml @@ -0,0 +1,10 @@ +description: South Dakota covers this many months of tenant-paid heat in rent, from October through April, under the Low Income Energy Assistance Program. +values: + 2025-10-01: 7 +metadata: + unit: month + period: year + label: South Dakota LIEAP heat-in-rent covered months + reference: + - title: South Dakota LIEAP Policy and Procedures Manual, 2018, physical pages 31-32 (October through April) + href: https://liheapch.acf.gov/sites/default/files/webfiles/docs/SD_Policy-and-Procedures-Manual2018.pdf#page=31 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/heat_in_rent/rent_share.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/heat_in_rent/rent_share.yaml new file mode 100644 index 00000000000..413c8145940 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/heat_in_rent/rent_share.yaml @@ -0,0 +1,10 @@ +description: South Dakota limits heat-in-rent payments to this fraction of monthly tenant-paid rent under the Low Income Energy Assistance Program. +values: + 2025-10-01: 0.3 +metadata: + unit: /1 + period: month + label: South Dakota LIEAP heat-in-rent payment share + reference: + - title: South Dakota Administrative Rule 67:15:01:41.02, Amount of payment if rent includes cost of heat + href: https://sdlegislature.gov/Rules/Administrative/67:15:01:41.02 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/income_threshold_rate.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/income_threshold_rate.yaml new file mode 100644 index 00000000000..8f05ff0a275 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/income_threshold_rate.yaml @@ -0,0 +1,12 @@ +description: South Dakota uses this federal poverty guideline rate for the first income tier under the Low Income Energy Assistance Program. Rounding its three-month equivalent half up reproduces all published FY2022 and FY2024 first-tier thresholds; this inferred rule carries forward to the FY2026 Mid tier. The date is the latest explicit FY2024 annual-model observation, not policy commencement. +values: + 2024-01-01: 1.25 +metadata: + unit: /1 + period: year + label: South Dakota LIEAP first-tier poverty rate + reference: + - title: South Dakota FY2024 LIHEAP Benefit Matrix, pages 1-5, first-tier maximum income + href: https://liheapch.acf.gov/docs/2024/benefits-matricies/SD_BenefitMatrix_2024.pdf#page=1 + - title: South Dakota FY2026 LIHEAP Benefit Matrix, Income Tier 1, 0-Mid + href: https://liheapch.acf.gov/docs/2026/benefits-matricies/SD_BenefitMatrix_2026.pdf#page=1 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_1.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_1.yaml new file mode 100644 index 00000000000..9f12ffa210c --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_1.yaml @@ -0,0 +1,30 @@ +description: South Dakota assigns these counties to heating region 1 under the Low Income Energy Assistance Program. +values: + 2025-10-01: + - BENNETT_COUNTY_SD + - BON_HOMME_COUNTY_SD + - CHARLES_MIX_COUNTY_SD + - CLAY_COUNTY_SD + - CUSTER_COUNTY_SD + - DAVISON_COUNTY_SD + - DOUGLAS_COUNTY_SD + - FALL_RIVER_COUNTY_SD + - GREGORY_COUNTY_SD + - HUTCHINSON_COUNTY_SD + - JACKSON_COUNTY_SD + - JONES_COUNTY_SD + - LINCOLN_COUNTY_SD + - MELLETTE_COUNTY_SD + - OGLALA_LAKOTA_COUNTY_SD + - TODD_COUNTY_SD + - TRIPP_COUNTY_SD + - TURNER_COUNTY_SD + - UNION_COUNTY_SD +# Oglala Lakota is the current name of Shannon County in the rule. +metadata: + unit: list + period: year + label: South Dakota LIEAP region 1 counties + reference: + - title: South Dakota Administrative Rule 67:15:01:35, Designation of regions + href: https://sdlegislature.gov/Rules/Administrative/67:15:01:35 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_2.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_2.yaml new file mode 100644 index 00000000000..c5db4ca8116 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_2.yaml @@ -0,0 +1,29 @@ +description: South Dakota assigns these counties to heating region 2 under the Low Income Energy Assistance Program. +values: + 2025-10-01: + - AURORA_COUNTY_SD + - BRULE_COUNTY_SD + - BUFFALO_COUNTY_SD + - BUTTE_COUNTY_SD + - HAAKON_COUNTY_SD + - HAND_COUNTY_SD + - HANSON_COUNTY_SD + - HUGHES_COUNTY_SD + - JERAULD_COUNTY_SD + - LAWRENCE_COUNTY_SD + - LYMAN_COUNTY_SD + - MCCOOK_COUNTY_SD + - MEADE_COUNTY_SD + - MINER_COUNTY_SD + - PENNINGTON_COUNTY_SD + - SANBORN_COUNTY_SD + - STANLEY_COUNTY_SD + - SULLY_COUNTY_SD + - YANKTON_COUNTY_SD +metadata: + unit: list + period: year + label: South Dakota LIEAP region 2 counties + reference: + - title: South Dakota Administrative Rule 67:15:01:35, Designation of regions + href: https://sdlegislature.gov/Rules/Administrative/67:15:01:35 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_3.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_3.yaml new file mode 100644 index 00000000000..7fc71dd6667 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_3.yaml @@ -0,0 +1,25 @@ +description: South Dakota assigns these counties to heating region 3 under the Low Income Energy Assistance Program. +values: + 2025-10-01: + - BEADLE_COUNTY_SD + - DEWEY_COUNTY_SD + - EDMUNDS_COUNTY_SD + - FAULK_COUNTY_SD + - GRANT_COUNTY_SD + - HYDE_COUNTY_SD + - KINGSBURY_COUNTY_SD + - LAKE_COUNTY_SD + - MINNEHAHA_COUNTY_SD + - MOODY_COUNTY_SD + - POTTER_COUNTY_SD + - ROBERTS_COUNTY_SD + - SPINK_COUNTY_SD + - WALWORTH_COUNTY_SD + - ZIEBACH_COUNTY_SD +metadata: + unit: list + period: year + label: South Dakota LIEAP region 3 counties + reference: + - title: South Dakota Administrative Rule 67:15:01:35, Designation of regions + href: https://sdlegislature.gov/Rules/Administrative/67:15:01:35 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_4.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_4.yaml new file mode 100644 index 00000000000..d48740ac5e1 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_4.yaml @@ -0,0 +1,23 @@ +description: South Dakota assigns these counties to heating region 4 under the Low Income Energy Assistance Program. +values: + 2025-10-01: + - BROOKINGS_COUNTY_SD + - BROWN_COUNTY_SD + - CAMPBELL_COUNTY_SD + - CLARK_COUNTY_SD + - CODINGTON_COUNTY_SD + - CORSON_COUNTY_SD + - DAY_COUNTY_SD + - DEUEL_COUNTY_SD + - HAMLIN_COUNTY_SD + - HARDING_COUNTY_SD + - MARSHALL_COUNTY_SD + - MCPHERSON_COUNTY_SD + - PERKINS_COUNTY_SD +metadata: + unit: list + period: year + label: South Dakota LIEAP region 4 counties + reference: + - title: South Dakota Administrative Rule 67:15:01:35, Designation of regions + href: https://sdlegislature.gov/Rules/Administrative/67:15:01:35 diff --git a/policyengine_us/programs.yaml b/policyengine_us/programs.yaml index 756dc2c4988..e904593fe79 100644 --- a/policyengine_us/programs.yaml +++ b/policyengine_us/programs.yaml @@ -804,6 +804,12 @@ programs: status: partial coverage: OR, DC, Riverside County, MA, IL, KS, MS, TX state_implementations: + - state: SD + status: in_progress + name: South Dakota LIHEAP + full_name: South Dakota Low Income Home Energy Assistance Program + parameter_prefix: gov.states.sd.dss.liheap + notes: Income, county, heat-in-rent and direct-payment components use the latest supported threshold rules and FY2026 matrix. Mixed-status income arithmetic remains unresolved; existing expense inputs do not identify unpaid balances or eligible billing dates. Regular heating only; historical backfills do not establish verified coverage. - state: OR status: in_progress name: LIHEAP diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.yaml new file mode 100644 index 00000000000..2c95a56e6a4 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.yaml @@ -0,0 +1,48 @@ +# South Dakota independent ordinary-household components; no final award is asserted. + +# Current DSS page explicitly labels2025-2026season and prints quarterly7825,10575,13325,16075,18825,21575,22514,23014,23514,24431. Annual model returns each multipliedby4; size9 tests the material inner SMI floor. +- name: Case 1, all ten published FY2026 three month maxima are annualized once. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [SD, SD, SD, SD, SD, SD, SD, SD, SD, SD] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, + NATURAL_GAS, NATURAL_GAS] + spm_unit_size: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + output: + sd_liheap_income_limit: [31300, 42300, 53300, 64300, 75300, 86300, 90056, 92056, 94056, 97724] + +# FY2024matrix physicalpp1-5 publishes quarterly maxima for sizes1-15. Annualize each literal quarter amount by4. Size5 and7 distinguish inner SMI floor; odd large-sizequarters distinguish latest floor from older FY2022 rounding. This does not claim a common FY2022 maximum convention. +- name: Case 2, all fifteen FY2024 published quarterly maxima retain their rounded amounts. + period: 2024 + absolute_error_margin: 0.01 + input: + state_code: [SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, + NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] + spm_unit_size: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] + output: + sd_liheap_income_limit: [29160, 39308, 48560, 57808, 67056, 76308, 78040, 79776, 83548, 91260, 98968, 106680, + 114388, 122100, 129808] + +# Latest reconciled rule carried forward, not printed FY2026cells:2025FPG for11-15 is70650,76150,81650,87150,92650. At150percent, floor quarter gives26493,28556,30618,32681,34743; annualizeby4. No size10 clamp. +- name: Case 3, derived FY2026 large household limits continue beyond the printed ten person table. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [SD, SD, SD, SD, SD] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] + spm_unit_size: [11, 12, 13, 14, 15] + output: + sd_liheap_income_limit: [105972, 114224, 122472, 130724, 138972] + +# Source coverage is SouthDakota fully qualified households; size0 guard supplies no one-person limit, while size1 uses7825quarter*4. This is not a mixed-status eligibility test. +- name: Case 4, zero household size and a different state have no South Dakota income limit. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [SD, ND, SD] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] + spm_unit_size: [0, 1, 1] + output: + sd_liheap_income_limit: [0, 0, 31300] diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/income/sd_liheap_countable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/income/sd_liheap_countable_income.yaml new file mode 100644 index 00000000000..94133b326ca --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/income/sd_liheap_countable_income.yaml @@ -0,0 +1,352 @@ +# South Dakota supported income region and heat-in-rent components only; no final award is asserted. + +# Rule67:15:01:18.01 and approved net-input approximation:12000+600+600+1200+600=15000; no extra business expense deduction. +- name: Case 1, adult wages and existing net business sources count once. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + employment_income: 12_000 + self_employment_income: 600 + sstb_self_employment_income: 600 + farm_operations_income: 1_200 + partnership_s_corp_income: 600 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 1_200 + sd_tanf: 0 + households: + household: + members: [person1] + state_code: SD + county: AURORA_COUNTY_SD + output: + sd_liheap_countable_income: 15_000 + +# Rule67:15:01:18.01 explicitly treats self-employment loss aszero:12000 wages+600 positivebusiness=12600. +- name: Case 2, business losses are zero before adding other income. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + employment_income: 12_000 + self_employment_income: -6_000 + farm_operations_income: -1_200 + partnership_s_corp_income: 600 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 1_200 + sd_tanf: 0 + households: + household: + members: [person1] + state_code: SD + county: AURORA_COUNTY_SD + output: + sd_liheap_countable_income: 12_600 + +# Rule67:15:01:18(1) exempts earnings under18 or under19 while in highschool. UnearnedSSA1200 always counts; when earnings count add6000+1200 to parent12000+SSA1200. College does not satisfy secondary-school rule. +- name: Case 3, age 17 nonstudent follows the precise earned income exception. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + employment_income: 12_000 + person2: + age: 17 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + is_full_time_student: false + employment_income: 6_000 + self_employment_income: 1_200 + social_security: 1_200 + spm_units: + spm_unit: + members: [person1, person2] + heating_type: NATURAL_GAS + gas_expense: 1_200 + sd_tanf: 0 + households: + household: + members: [person1, person2] + state_code: SD + county: AURORA_COUNTY_SD + output: + sd_liheap_countable_income: 13_200 + +# Rule67:15:01:18(1) exempts earnings under18 or under19 while in highschool. UnearnedSSA1200 always counts; when earnings count add6000+1200 to parent12000+SSA1200. College does not satisfy secondary-school rule. +- name: Case 4, age 18 secondary student follows the precise earned income exception. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + employment_income: 12_000 + person2: + age: 18 + immigration_status: CITIZEN + is_in_secondary_school: true + ssi: 0 + is_full_time_student: true + employment_income: 6_000 + self_employment_income: 1_200 + social_security: 1_200 + spm_units: + spm_unit: + members: [person1, person2] + heating_type: NATURAL_GAS + gas_expense: 1_200 + sd_tanf: 0 + households: + household: + members: [person1, person2] + state_code: SD + county: AURORA_COUNTY_SD + output: + sd_liheap_countable_income: 13_200 + +# Rule67:15:01:18(1) exempts earnings under18 or under19 while in highschool. UnearnedSSA1200 always counts; when earnings count add6000+1200 to parent12000+SSA1200. College does not satisfy secondary-school rule. +- name: Case 5, age 18 college student follows the precise earned income exception. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + employment_income: 12_000 + person2: + age: 18 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + is_full_time_student: true + employment_income: 6_000 + self_employment_income: 1_200 + social_security: 1_200 + spm_units: + spm_unit: + members: [person1, person2] + heating_type: NATURAL_GAS + gas_expense: 1_200 + sd_tanf: 0 + households: + household: + members: [person1, person2] + state_code: SD + county: AURORA_COUNTY_SD + output: + sd_liheap_countable_income: 20_400 + +# Rule67:15:01:18(1) exempts earnings under18 or under19 while in highschool. UnearnedSSA1200 always counts; when earnings count add6000+1200 to parent12000+SSA1200. College does not satisfy secondary-school rule. +- name: Case 6, age 19 secondary student follows the precise earned income exception. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + employment_income: 12_000 + person2: + age: 19 + immigration_status: CITIZEN + is_in_secondary_school: true + ssi: 0 + is_full_time_student: true + employment_income: 6_000 + self_employment_income: 1_200 + social_security: 1_200 + spm_units: + spm_unit: + members: [person1, person2] + heating_type: NATURAL_GAS + gas_expense: 1_200 + sd_tanf: 0 + households: + household: + members: [person1, person2] + state_code: SD + county: AURORA_COUNTY_SD + output: + sd_liheap_countable_income: 20_400 + +# Current planpp6-7 income categories:1200+2400+600+3600+1200+1200+600+1200+600+300+300=13200. Independent farm rent/survivor inputs are included. +- name: Case 7, disjoint rental survivor and ordinary unearned sources count at every age. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + rental_income: 1_200 + farm_rent_income: 2_400 + survivor_benefits: 600 + social_security: 3_600 + pension_income: 1_200 + unemployment_compensation: 1_200 + veterans_benefits: 600 + child_support_received: 1_200 + alimony_income: 600 + strike_benefits: 300 + general_assistance: 300 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 1_200 + sd_tanf: 0 + households: + household: + members: [person1] + state_code: SD + county: AURORA_COUNTY_SD + output: + sd_liheap_countable_income: 13_200 + +# Current planpp6-7 excludes investmentinterest/dividends, gifts andcapital gains; rule67:15:01:18(14) confirms gains.12000 wages remain12000;SNAP also excluded. +- name: Case 8, interest dividends capital gains and gifts do not enter income. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + employment_income: 12_000 + interest_income: 600 + ordinary_dividend_income: 1_200 + capital_gains: 6_000 + financial_assistance: 2_400 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 1_200 + sd_tanf: 0 + snap: 6_000 + households: + household: + members: [person1] + state_code: SD + county: AURORA_COUNTY_SD + output: + sd_liheap_countable_income: 12_000 + +# Current plan countsSSI/TANF for allages:6000 adultwages+1200 adultSSI+2400 childSSI+3600 familyTANF=13200. Monthly inputs supplied asannual totals are aggregated once. +- name: Case 9, adult and child SSI and the state TANF grant are annualized once. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 1_200 + employment_income: 6_000 + person2: + age: 8 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 2_400 + spm_units: + spm_unit: + members: [person1, person2] + heating_type: NATURAL_GAS + gas_expense: 1_200 + sd_tanf: 3_600 + households: + household: + members: [person1, person2] + state_code: SD + county: AURORA_COUNTY_SD + output: + sd_liheap_countable_income: 13_200 + +# Rule67:15:01:18(20):24000-3600 childsupportpaid=20400. +- name: Case 10, income used for legally mandated child support is deducted. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + employment_income: 24_000 + child_support_expense: 3_600 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 1_200 + sd_tanf: 0 + households: + household: + members: [person1] + state_code: SD + county: AURORA_COUNTY_SD + output: + sd_liheap_countable_income: 20_400 + +# Rule67:15:01:18(20) exclusion with householdfloor:max(1200-2400,0)=0. +- name: Case 11, child support deductions cannot produce negative countable income. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + employment_income: 1_200 + child_support_expense: 2_400 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 1_200 + sd_tanf: 0 + households: + household: + members: [person1] + state_code: SD + county: AURORA_COUNTY_SD + output: + sd_liheap_countable_income: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_base_payment.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_base_payment.yaml new file mode 100644 index 00000000000..bb59e6e0a4b --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_base_payment.yaml @@ -0,0 +1,117 @@ +# South Dakota independent ordinary-household components; no final award is asserted. + +# FY2026matrix physicalp1:5fuelgroups x2income tiers x4regions=40literalcells. Size1 Mid19564annual;0is tier1 and20000is tier2 below current31300max. Lower payments are literal, not uniformly90percent. This is a table component; unpaid-balance and date-of-charge eligibility are not established. +- name: Case 1, all forty published direct heating cells retain their literal amounts. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, + SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD] + heating_type: [WOOD, WOOD, WOOD, WOOD, WOOD, WOOD, WOOD, WOOD, ELECTRICITY, ELECTRICITY, ELECTRICITY, ELECTRICITY, + ELECTRICITY, ELECTRICITY, ELECTRICITY, ELECTRICITY, FUEL_OIL, FUEL_OIL, FUEL_OIL, FUEL_OIL, FUEL_OIL, FUEL_OIL, + FUEL_OIL, FUEL_OIL, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, + NATURAL_GAS, PROPANE, PROPANE, PROPANE, PROPANE, PROPANE, PROPANE, PROPANE, PROPANE] + sd_liheap_region: [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, + 4, 1, 2, 3, 4, 1, 2, 3, 4] + spm_unit_size: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1] + sd_liheap_countable_income: [0, 0, 0, 0, 20000, 20000, 20000, 20000, 0, 0, 0, 0, 20000, 20000, 20000, 20000, 0, + 0, 0, 0, 20000, 20000, 20000, 20000, 0, 0, 0, 0, 20000, 20000, 20000, 20000, 0, 0, 0, 0, 20000, 20000, 20000, + 20000] + output: + sd_liheap_base_payment: [742, 770, 812, 854, 668, 694, 730, 768, 1331, 1410, 1516, 1626, 1198, 1270, 1369, 1461, + 2858, 3018, 3236, 3437, 2573, 2715, 2910, 3093, 880, 938, 1007, 1058, 792, 844, 907, 953, 2499, 2658, 2834, + 3066, 2249, 2394, 2552, 2762] + +# FY2026matrixp1 expressly groupsCoal&Wood. Currentapplication EA297 physicalp4 lists one FuelOil/Kerosene category; use the oil schedule for both. This maps price only, not older kerosene payment timing. +- name: Case 2, wood coal and the application oil kerosene category use matching source rows. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [SD, SD, SD, SD, SD, SD, SD, SD] + heating_type: [WOOD, COAL, FUEL_OIL, KEROSENE, WOOD, COAL, FUEL_OIL, KEROSENE] + sd_liheap_region: [1, 1, 2, 2, 3, 3, 4, 4] + spm_unit_size: [1, 1, 1, 1, 1, 1, 1, 1] + sd_liheap_countable_income: [0, 0, 0, 0, 20000, 20000, 20000, 20000] + output: + sd_liheap_base_payment: [742, 742, 3018, 3018, 730, 730, 3093, 3093] + +# FY2024matrixp1: size1Midquarter4556=>18224annual (not18225); size2quarter6163=>24652 (not24650). Firsttier includes exactquarter threshold, centsabove move to secondtier. Region1naturalgas880/792. +- name: Case 3, the rounded quarterly threshold controls exact and cent boundaries rather than the annual column. + period: 2024 + absolute_error_margin: 0.01 + input: + state_code: [SD, SD, SD, SD, SD] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] + sd_liheap_region: [1, 1, 1, 1, 1] + spm_unit_size: [1, 1, 2, 2, 2] + sd_liheap_countable_income: [18224, 18224.01, 24650, 24652, 24652.01] + output: + sd_liheap_income_threshold: [18224, 18224, 24652, 24652, 24652] + sd_liheap_base_payment: [880, 792, 880, 880, 792] + +# FY2026reconciled Midquarter4891=>19564annual. Region3electricity tier1=1516 andtier2=1369; exactthreshold remains firsttier. No normal90percent replacement. +- name: Case 4, the current threshold includes its exact boundary and switches one cent above. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [SD, SD, SD] + heating_type: [ELECTRICITY, ELECTRICITY, ELECTRICITY] + sd_liheap_region: [3, 3, 3] + spm_unit_size: [1, 1, 1] + sd_liheap_countable_income: [19563.99, 19564, 19564.01] + output: + sd_liheap_base_payment: [1516, 1516, 1369] + +# No supported directprice forunknownregion orSolar/Other/Unspecified. These zeros are component placeholders, not legal denials. NONE,stateandzero-size guards also return0. +- name: Case 5, unknown region unpriced fuels zero size and other states return component placeholders. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [SD, SD, SD, SD, SD, SD, SD, ND] + heating_type: [NATURAL_GAS, NATURAL_GAS, NONE, SOLAR, OTHER, UNSPECIFIED, NATURAL_GAS, NATURAL_GAS] + sd_liheap_region: [0, 5, 1, 1, 1, 1, 1, 1] + spm_unit_size: [1, 1, 1, 1, 1, 1, 0, 1] + sd_liheap_countable_income: [0, 0, 0, 0, 0, 0, 0, 0] + output: + sd_liheap_base_payment: [0, 0, 0, 0, 0, 0, 0, 0] + +# Wholly qualified households: size1Mid19564 puts20000in tier2; size2Mid26436 puts identicalincome in tier1. Region1gas retains792/880; size does not independently multiply the payment. +- name: Case 6, actual household membership changes the tier while leaving the regional fuel schedule shared. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + adult1: + age: 40 + immigration_status: CITIZEN + ssi: 0 + adult2: + age: 40 + immigration_status: CITIZEN + ssi: 0 + child: + age: 10 + immigration_status: CITIZEN + ssi: 0 + spm_units: + unit1: + members: [adult1] + heating_type: NATURAL_GAS + sd_liheap_region: 1 + sd_liheap_countable_income: 20000 + unit2: + members: [adult2, child] + heating_type: NATURAL_GAS + sd_liheap_region: 1 + sd_liheap_countable_income: 20000 + households: + household1: + members: [adult1] + state_code: SD + household2: + members: [adult2, child] + state_code: SD + output: + sd_liheap_income_threshold: [19564, 26436] + sd_liheap_base_payment: [792, 880] diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_heat_in_rent_payment.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_heat_in_rent_payment.yaml new file mode 100644 index 00000000000..1fae90eabd5 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_heat_in_rent_payment.yaml @@ -0,0 +1,211 @@ +# South Dakota supported income region and heat-in-rent components only; no final award is asserted. + +# FY2026 matrix HIR cells800/800/850/850; annualrent6000 gives500monthly*30%*7=1050, so regionalmax binds. No separate heating bill required. +- name: Case 1, all four regions use their source heat in rent maximum. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [SD, SD, SD, SD] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] + gas_expense: [0, 0, 0, 0] + county: [CUSTER_COUNTY_SD, AURORA_COUNTY_SD, ZIEBACH_COUNTY_SD, HARDING_COUNTY_SD] + heat_expense_included_in_rent: [true, true, true, true] + rent: [6_000, 6_000, 6_000, 6_000] + output: + sd_liheap_region: [1, 2, 3, 4] + sd_liheap_heat_in_rent_payment: [800, 800, 850, 850] + +# Rule67:15:01:41.02 andmanual31-32:1200annual/12*30%*7=210, belowregion2 maximum800. +- name: Case 2, one hundred dollars monthly tenant rent caps seven month assistance at two hundred ten. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + rent: 1_200 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 0 + sd_tanf: 0 + heat_expense_included_in_rent: true + households: + household: + members: [person1] + state_code: SD + county: AURORA_COUNTY_SD + output: + sd_liheap_heat_in_rent_payment: 210 + +# Two adults each pay600annual rent; child0. Household1200/12*30%*7=210, not a separate regional benefit perperson. +- name: Case 3, person rent shares are summed once for the household cap. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + rent: 600 + person2: + age: 39 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + rent: 600 + person3: + age: 8 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + rent: 0 + spm_units: + spm_unit: + members: [person1, person2, person3] + heating_type: NATURAL_GAS + gas_expense: 0 + sd_tanf: 0 + heat_expense_included_in_rent: true + households: + household: + members: [person1, person2, person3] + state_code: SD + county: AURORA_COUNTY_SD + output: + sd_liheap_heat_in_rent_payment: 210 + +# Manualp32 allows positive subsidizedtenantshare.12000grossannualrent-9600reportedhousingassistance=2400tenantannualrent; equaladultshares1200each.2400/12*30%*7=420 belowregion3 maximum850. +- name: Case 4, subsidized rent uses the actual positive tenant share across multiple people. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + pre_subsidy_rent: 6_000 + person2: + age: 39 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + pre_subsidy_rent: 6_000 + person3: + age: 8 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + pre_subsidy_rent: 0 + spm_units: + spm_unit: + members: [person1, person2, person3] + heating_type: NATURAL_GAS + gas_expense: 0 + sd_tanf: 0 + heat_expense_included_in_rent: true + housing_assistance: 9_600 + receives_housing_assistance: true + households: + household: + members: [person1, person2, person3] + state_code: SD + county: ZIEBACH_COUNTY_SD + is_in_public_housing: true + output: + rent: [1_200, 1_200, 0] + sd_liheap_heat_in_rent_payment: 420 + +# Manualp32 zeroTenantPayment has no heat cost.12000grossrent-12000reportedassistance=0netrent, so cap0. +- name: Case 5, zero tenant share after housing assistance produces zero component. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + pre_subsidy_rent: 12_000 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 0 + sd_tanf: 0 + heat_expense_included_in_rent: true + housing_assistance: 12_000 + receives_housing_assistance: true + households: + household: + members: [person1] + state_code: SD + county: AURORA_COUNTY_SD + output: + rent: 0 + sd_liheap_heat_in_rent_payment: 0 + +# This component is limited to heat included in rent. A directbill belongs to the unresolved separate directvendor award path. +- name: Case 6, a direct heating bill without heat in rent does not receive this component. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + rent: 6_000 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 1_200 + sd_tanf: 0 + heat_expense_included_in_rent: false + households: + household: + members: [person1] + state_code: SD + county: AURORA_COUNTY_SD + output: + sd_liheap_heat_in_rent_payment: 0 + +# Current matrixHIR isfuel-neutral; withverifiedHIR, unknownfuel uses1200/12*.3*7=210. ExplicitNONE means noheat and gives0. +- name: Case 7, fuel neutral heat in rent permits unknown fuel but rejects explicit no heating. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [SD, SD] + heating_type: [UNSPECIFIED, NONE] + gas_expense: [0, 0] + county: [AURORA_COUNTY_SD, AURORA_COUNTY_SD] + heat_expense_included_in_rent: [true, true] + rent: [1_200, 1_200] + output: + sd_liheap_heat_in_rent_payment: [210, 0] + +# Explicitunknowngeography cannot choose a verifiedregionalamount; region0 is safely handled. OutsideSD component also0; neither is a final legal eligibility adjudication. +- name: Case 8, unknown region returns a safe unsupported estimate without inventing a county. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [SD, CA] + heating_type: [NATURAL_GAS, NATURAL_GAS] + gas_expense: [0, 0] + county: [UNKNOWN, LOS_ANGELES_COUNTY_CA] + heat_expense_included_in_rent: [true, true] + rent: [6_000, 6_000] + output: + sd_liheap_region: [0, 0] + sd_liheap_heat_in_rent_payment: [0, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_income_threshold.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_income_threshold.yaml new file mode 100644 index 00000000000..c744a7e8bf1 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_income_threshold.yaml @@ -0,0 +1,36 @@ +# South Dakota independent ordinary-household components; no final award is asserted. + +# FY2024matrix pp1-5 Midquarter values4556,6163,7769,9375,10981,12588,14194,15800,17406,19013,20619,22225,23831,25438,27044. Return4times these, not the sometimes-different printedannual column. E.g. size2quarter6163 means24652 rather than24650. +- name: Case 1, all fifteen FY2024 tier thresholds use the printed three month amounts. + period: 2024 + absolute_error_margin: 0.01 + input: + state_code: [SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, + NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] + spm_unit_size: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] + output: + sd_liheap_income_threshold: [18224, 24652, 31076, 37500, 43924, 50352, 56776, 63200, 69624, 76052, 82476, 88900, + 95324, 101752, 108176] + +# FY2024reconciled125percent lag1FPG rule carried forward: FY2025FPG15650,21150,59650,65150,92650. Quarter raw4890.625,6609.375,18640.625,20359.375,28953.125 rounds halfup then times4. These are formula-derived FY2026tier thresholds, not printed current numericalMidcells. +- name: Case 2, FY2026 threshold carry forward preserves half up quarter rounding. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [SD, SD, SD, SD, SD] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] + spm_unit_size: [1, 2, 9, 10, 15] + output: + sd_liheap_income_threshold: [19564, 26436, 74564, 81436, 115812] + +# Fully qualified ordinary-household scope and state/size guards; size1 annualizedMid4891*4=19564. +- name: Case 3, zero size and another state do not receive a South Dakota tier threshold. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [SD, ND, SD] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] + spm_unit_size: [0, 1, 1] + output: + sd_liheap_income_threshold: [0, 0, 19564] diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_region.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_region.yaml new file mode 100644 index 00000000000..feb5e015975 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_region.yaml @@ -0,0 +1,50 @@ +# South Dakota supported income region and heat-in-rent components only; no final award is asserted. + +# Officialrule67:15:01:35 table, independently extracted in sources/sd_county_regions.json and visually corroborated manualp52. HistoricShannon maps tocurrentOglalaLakota region1. Expected regions come fromoriginalsource, notimplementationparameters. +- name: Case 1, all sixty six official county assignments match including renamed Shannon County. + period: 2026 + input: + state_code: [SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, + SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, + SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, + NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, + NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, + NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, + NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, + NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, + NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, + NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, + NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] + gas_expense: [1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, + 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, + 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, + 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, + 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200, 1_200] + county: [CUSTER_COUNTY_SD, BUTTE_COUNTY_SD, ZIEBACH_COUNTY_SD, HARDING_COUNTY_SD, FALL_RIVER_COUNTY_SD, + MEADE_COUNTY_SD, DEWEY_COUNTY_SD, PERKINS_COUNTY_SD, OGLALA_LAKOTA_COUNTY_SD, LAWRENCE_COUNTY_SD, + WALWORTH_COUNTY_SD, CORSON_COUNTY_SD, JACKSON_COUNTY_SD, PENNINGTON_COUNTY_SD, EDMUNDS_COUNTY_SD, + CAMPBELL_COUNTY_SD, BENNETT_COUNTY_SD, HAAKON_COUNTY_SD, POTTER_COUNTY_SD, MCPHERSON_COUNTY_SD, + JONES_COUNTY_SD, STANLEY_COUNTY_SD, FAULK_COUNTY_SD, BROWN_COUNTY_SD, MELLETTE_COUNTY_SD, SULLY_COUNTY_SD, + SPINK_COUNTY_SD, MARSHALL_COUNTY_SD, TODD_COUNTY_SD, HUGHES_COUNTY_SD, ROBERTS_COUNTY_SD, DAY_COUNTY_SD, + TRIPP_COUNTY_SD, LYMAN_COUNTY_SD, GRANT_COUNTY_SD, CLARK_COUNTY_SD, GREGORY_COUNTY_SD, HAND_COUNTY_SD, + BEADLE_COUNTY_SD, CODINGTON_COUNTY_SD, CHARLES_MIX_COUNTY_SD, BUFFALO_COUNTY_SD, KINGSBURY_COUNTY_SD, + HAMLIN_COUNTY_SD, DOUGLAS_COUNTY_SD, JERAULD_COUNTY_SD, HYDE_COUNTY_SD, BROOKINGS_COUNTY_SD, DAVISON_COUNTY_SD, + SANBORN_COUNTY_SD, LAKE_COUNTY_SD, DEUEL_COUNTY_SD, HUTCHINSON_COUNTY_SD, BRULE_COUNTY_SD, MOODY_COUNTY_SD, + TURNER_COUNTY_SD, AURORA_COUNTY_SD, MINNEHAHA_COUNTY_SD, LINCOLN_COUNTY_SD, HANSON_COUNTY_SD, BON_HOMME_COUNTY_SD, + MCCOOK_COUNTY_SD, CLAY_COUNTY_SD, YANKTON_COUNTY_SD, UNION_COUNTY_SD, MINER_COUNTY_SD] + output: + sd_liheap_region: [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, + 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 1, 2, 3, 1, + 2, 1, 2, 1, 2, 1, 2] + +# ExplicitUNKNOWN/foreigncounty returnsunsupported marker0 safely; missingcounty is a separate sharedmodelAurora fallback and is deliberately not treated asverifiedlocation. +- name: Case 2, explicit unknown and foreign counties never default to a verified region. + period: 2026 + input: + state_code: [SD, SD, CA] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] + gas_expense: [1_200, 1_200, 1_200] + county: [UNKNOWN, LOS_ANGELES_COUNTY_CA, LOS_ANGELES_COUNTY_CA] + output: + sd_liheap_region: [0, 0, 0] diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.py b/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.py new file mode 100644 index 00000000000..e6771dbca36 --- /dev/null +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.py @@ -0,0 +1,40 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.hhs.hhs_smi import smi +from policyengine_us.variables.gov.hhs.tax_unit_fpg import fpg + + +class sd_liheap_income_limit(Variable): + value_type = float + entity = SPMUnit + definition_period = YEAR + unit = USD + label = "South Dakota LIEAP annualized income limit" + defined_for = StateCode.SD + reference = ( + # Pages 1-5 publish three-month limits for sizes 1-15. + "https://liheapch.acf.gov/docs/2024/benefits-matricies/SD_BenefitMatrix_2024.pdf#page=1", + "https://dss.sd.gov/economicassistance/energy_weatherization_assistance.aspx", + ) + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.sd.dss.liheap.income + size = spm_unit("spm_unit_size", period) + # This component supports wholly qualified households. The citizenship + # calculator's mixed-status size and income allocation remain unresolved. + state_group = spm_unit.household("state_group_str", period) + guideline = fpg( + max_(size, 1), state_group, period, parameters, year_lag=p.fpg_year_lag + ) + state = spm_unit.household("state_code_str", period) + four_person_smi = parameters(period).gov.hhs.smi.amount[state] + size_share = smi(size, state, period, parameters) / four_person_smi + smi_limit = np.floor(four_person_smi * p.limits.smi_rate) * size_share + annual_limit = min_( + guideline * p.limits.fpg_upper_rate, + max_(guideline * p.limits.fpg_lower_rate, smi_limit), + ) + # This inferred ordering matches every FY2024 and published FY2026 + # three-month maximum. Annualize the rounded quarter, not its unrounded + # annual equivalent. Sizes 11+ carry the FY2024 rule forward; earlier + # backfilled years are unverified and FY2022 used different rounding. + return where(size > 0, 4 * np.floor(annual_limit / 4), 0) 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 new file mode 100644 index 00000000000..74a589d4905 --- /dev/null +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/income/sd_liheap_countable_income.py @@ -0,0 +1,42 @@ +from policyengine_us.model_api import * + + +class sd_liheap_countable_income(Variable): + value_type = float + entity = SPMUnit + definition_period = YEAR + unit = USD + label = "South Dakota LIEAP annual countable household income" + defined_for = StateCode.SD + reference = ( + "https://sdlegislature.gov/Rules/Administrative/67:15:01:18", + "https://sdlegislature.gov/Rules/Administrative/67:15:01:18.01", + "https://liheapch.acf.gov/docs/2026/state-plans/SD_Plan_2026.pdf#page=6", + ) + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.sd.dss.liheap.income + person = spm_unit.members + age = person("age", period) + excluded_earnings = (age < p.earned_income_min_age) | ( + (age < p.secondary_school_age_limit) + & person("is_in_secondary_school", period) + ) + earned = 0 + for source in p.sources.earned: + # Rule 67:15:01:18.01 counts self-employment losses as zero. + # Use existing net sources without another business deduction. + earned = earned + max_(person(source, period), 0) + unearned = add(spm_unit, period, p.sources.unearned, options=[ADD]) + child_support_paid = add(spm_unit, period, ["child_support_expense"]) + # Rule 67:15:01:18(1) exempts earnings, not minor unearned income; + # subsection (20) excludes income used to pay child support. State TANF + # is counted once at SPM level; SSI and TANF are explicitly annualized. + income = spm_unit.sum(earned * ~excluded_earnings) + unearned + # This component supports fully qualified households. The manual refers + # to an unavailable citizenship worksheet for allocating nonqualified + # members' income; that mixed-status calculation is not implemented. + # Annual inputs approximate stable income over the prior three months. + # Medicare deductions are deferred. Existing inputs cannot isolate all + # jury/foster receipts, program overpayment withholding, or expense refunds. + return max_(income - child_support_paid, 0) diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_base_payment.py b/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_base_payment.py new file mode 100644 index 00000000000..b44989c9026 --- /dev/null +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_base_payment.py @@ -0,0 +1,52 @@ +from policyengine_us.model_api import * + + +class sd_liheap_base_payment(Variable): + value_type = float + entity = SPMUnit + definition_period = YEAR + unit = USD + label = "South Dakota LIEAP direct-heating base payment component" + defined_for = StateCode.SD + reference = ( + "https://liheapch.acf.gov/docs/2026/benefits-matricies/SD_BenefitMatrix_2026.pdf#page=1", + # Pages 2 and 4: unpaid heating charges and fuel-oil/kerosene grouping. + "https://dss.sd.gov/formsandpubs/docs/ENERGY/energyassistanceapplication.pdf#page=2", + ) + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.sd.dss.liheap.payment.direct + region = spm_unit("sd_liheap_region", period) + supported_region = (region >= 1) & (region <= 4) + safe_region = clip(region, 1, 4) + income = spm_unit("sd_liheap_countable_income", period) + threshold = spm_unit("sd_liheap_income_threshold", period) + tier = where(income <= threshold, 1, 2) + heating_type = spm_unit("heating_type", period) + fuels = heating_type.possible_values + amount = select( + [ + (heating_type == fuels.COAL) | (heating_type == fuels.WOOD), + heating_type == fuels.ELECTRICITY, + (heating_type == fuels.FUEL_OIL) | (heating_type == fuels.KEROSENE), + heating_type == fuels.NATURAL_GAS, + heating_type == fuels.PROPANE, + ], + [ + p.coal_wood[safe_region][tier], + p.electricity[safe_region][tier], + p.fuel_oil[safe_region][tier], + p.natural_gas[safe_region][tier], + p.propane[safe_region][tier], + ], + default=0, + ) + # This is an ungated table ceiling, separate from heat-in-rent payments. + # Final income/immigration eligibility and actual unpaid charges are not + # applied here. Gas/electric meter dates must fall in October 1-May 15; + # oil/propane fills in July 1-April 30. Paid charges are not reimbursable. + # Annual heating_expense cannot identify balances or charge dates. + # Unknown region or unpriced fuel returns an unsupported-component zero, + # not a finding that the household is legally ineligible. + size = spm_unit("spm_unit_size", period) + return where(supported_region & (size > 0), amount, 0) diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_heat_in_rent_payment.py b/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_heat_in_rent_payment.py new file mode 100644 index 00000000000..3372fd7f902 --- /dev/null +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_heat_in_rent_payment.py @@ -0,0 +1,40 @@ +from policyengine_us.model_api import * + + +class sd_liheap_heat_in_rent_payment(Variable): + value_type = float + entity = SPMUnit + definition_period = YEAR + unit = USD + label = "South Dakota LIEAP heat-in-rent payment component" + defined_for = StateCode.SD + reference = ( + "https://sdlegislature.gov/Rules/Administrative/67:15:01:41.02", + "https://liheapch.acf.gov/docs/2026/benefits-matricies/SD_BenefitMatrix_2026.pdf", + # Physical pages 31-32: October-April payments and tenant rent share. + "https://liheapch.acf.gov/sites/default/files/webfiles/docs/SD_Policy-and-Procedures-Manual2018.pdf#page=31", + ) + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.sd.dss.liheap.payment.heat_in_rent + region = spm_unit("sd_liheap_region", period) + supported_region = (region >= 1) & (region <= 4) + maximum = p.amount[clip(region, 1, 4)] + # Existing Person rent is the net tenant share, apportioned after modeled + # HUD assistance. Report observed rent directly, or override the existing + # housing_assistance amount, when that default is not actual receipt. + annual_rent = max_(add(spm_unit, period, ["rent"]), 0) + cap = annual_rent / MONTHS_IN_YEAR * p.months * p.rent_share + heat_in_rent = spm_unit("heat_expense_included_in_rent", period) + heating_type = spm_unit("heating_type", period) + has_heat = heating_type != heating_type.possible_values.NONE + # The fuel-neutral heat-in-rent schedule permits unknown primary fuel; + # no separate heating bill is required. Subsidy does not disqualify a + # positive tenant share, while zero share yields zero through the cap. + # This is not final eligibility or a full award: income, immigration, + # tribal routing, landlord verification, and prior payments remain needed. + return where( + supported_region & heat_in_rent & has_heat, + min_(maximum, cap), + 0, + ) diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_income_threshold.py b/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_income_threshold.py new file mode 100644 index 00000000000..7c9cf002fe2 --- /dev/null +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_income_threshold.py @@ -0,0 +1,36 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.hhs.tax_unit_fpg import fpg + + +class sd_liheap_income_threshold(Variable): + value_type = float + entity = SPMUnit + definition_period = YEAR + unit = USD + label = "South Dakota LIEAP annualized first-tier income threshold" + defined_for = StateCode.SD + reference = ( + # Pages 1-5 publish both income tiers for sizes 1-15. + "https://liheapch.acf.gov/docs/2024/benefits-matricies/SD_BenefitMatrix_2024.pdf#page=1", + "https://liheapch.acf.gov/docs/2026/benefits-matricies/SD_BenefitMatrix_2026.pdf#page=1", + ) + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.sd.dss.liheap + size = spm_unit("spm_unit_size", period) + # Use total size for wholly qualified households; the mixed-status + # citizenship calculator's allocation is not established by the sources. + state_group = spm_unit.household("state_group_str", period) + guideline = fpg( + max_(size, 1), + state_group, + period, + parameters, + year_lag=p.income.fpg_year_lag, + ) + # Half-up rounding reproduces all FY2022 and FY2024 quarterly Mid + # thresholds. Carry this latest explicit rule into the FY2026 schedule, + # which names Mid without printing it. This is a numerical inference, + # not an express rounding instruction; earlier backfill is unverified. + quarter_limit = np.floor(guideline * p.payment.income_threshold_rate / 4 + 0.5) + return where(size > 0, 4 * quarter_limit, 0) diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_region.py b/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_region.py new file mode 100644 index 00000000000..fceaa903923 --- /dev/null +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_region.py @@ -0,0 +1,29 @@ +from policyengine_us.model_api import * + + +class sd_liheap_region(Variable): + value_type = int + entity = SPMUnit + definition_period = YEAR + label = "South Dakota LIEAP heating region" + defined_for = StateCode.SD + reference = "https://sdlegislature.gov/Rules/Administrative/67:15:01:35" + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.sd.dss.liheap.payment.regions + county = spm_unit.household("county_str", period) + # All 66 counties are assigned by rule; historic Shannon is represented + # by the existing Oglala Lakota county identifier. Unknown/foreign county + # is an unsupported location (0), never silently region 1. + # The shared county formula may infer the first county if no geography + # is supplied; an observed county is needed for a verified regional amount. + return select( + [ + np.isin(county, p.region_1), + np.isin(county, p.region_2), + np.isin(county, p.region_3), + np.isin(county, p.region_4), + ], + [1, 2, 3, 4], + default=0, + ) From 0fe833a1f592b219990b04261f78a170cf5cbba6 Mon Sep 17 00:00:00 2001 From: Ziming Date: Sat, 3 Oct 2026 23:26:29 -0400 Subject: [PATCH 2/9] Count railroad retirement in South Dakota LIHEAP income --- .../dss/liheap/income/sources/unearned.yaml | 5 +-- .../income/sd_liheap_countable_income.yaml | 35 +++++++++++++++++-- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/unearned.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/unearned.yaml index f906077ccad..b55fc81b7d5 100644 --- a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/unearned.yaml +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/unearned.yaml @@ -4,6 +4,7 @@ values: - social_security - ssi - pension_income + - railroad_benefits - survivor_benefits - unemployment_compensation - veterans_benefits @@ -21,7 +22,7 @@ metadata: period: year label: South Dakota LIEAP unearned income sources reference: - - title: South Dakota FY2026 LIHEAP State Plan, section 1.9, pages 6-7 (counted sources) - href: https://liheapch.acf.gov/docs/2026/state-plans/SD_Plan_2026.pdf#page=6 + - title: South Dakota FY2026 LIHEAP State Plan, sections 1.5 and 1.9 and fixed-income processing, pages 5-7, 10 + href: https://liheapch.acf.gov/docs/2026/state-plans/SD_Plan_2026.pdf#page=5 - title: South Dakota Administrative Rule 67:15:01:18, Income exclusions href: https://sdlegislature.gov/Rules/Administrative/67:15:01:18 diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/income/sd_liheap_countable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/income/sd_liheap_countable_income.yaml index 94133b326ca..1f9acb46d4c 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/income/sd_liheap_countable_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/income/sd_liheap_countable_income.yaml @@ -199,7 +199,7 @@ output: sd_liheap_countable_income: 20_400 -# Current planpp6-7 income categories:1200+2400+600+3600+1200+1200+600+1200+600+300+300=13200. Independent farm rent/survivor inputs are included. +# Current plan pp5-7,10:1200+2400+600+3600+1200+600+1200+600+1200+600+300+300=13800. Railroad, farm rent and survivor inputs are disjoint. - name: Case 7, disjoint rental survivor and ordinary unearned sources count at every age. period: 2026 absolute_error_margin: 0.01 @@ -215,6 +215,7 @@ survivor_benefits: 600 social_security: 3_600 pension_income: 1_200 + railroad_benefits: 600 unemployment_compensation: 1_200 veterans_benefits: 600 child_support_received: 1_200 @@ -233,7 +234,7 @@ state_code: SD county: AURORA_COUNTY_SD output: - sd_liheap_countable_income: 13_200 + sd_liheap_countable_income: 13_800 # Current planpp6-7 excludes investmentinterest/dividends, gifts andcapital gains; rule67:15:01:18(14) confirms gains.12000 wages remain12000;SNAP also excluded. - name: Case 8, interest dividends capital gains and gifts do not enter income. @@ -350,3 +351,33 @@ county: AURORA_COUNTY_SD output: sd_liheap_countable_income: 0 + +# FY2026 plan pp5,10 counts railroad retirement. DSS publishes a one-person +# three-month maximum of $7,825, annualized to $31,300; no final award is asserted. +- name: Case 12, railroad retirement counts without pension or Social Security receipts. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 67 + immigration_status: CITIZEN + is_in_secondary_school: false + railroad_benefits: 40_000 + pension_income: 0 + social_security: 0 + ssi: 0 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 1_200 + sd_tanf: 0 + households: + household: + members: [person1] + state_code: SD + county: CUSTER_COUNTY_SD + output: + sd_liheap_countable_income: 40_000 + sd_liheap_income_limit: 31_300 From b287c8fc4dde3424e3c048037841a91593543f73 Mon Sep 17 00:00:00 2001 From: Ziming Date: Tue, 6 Oct 2026 15:49:24 -0400 Subject: [PATCH 3/9] Add supported South Dakota LIHEAP eligibility and heating awards --- changelog.d/sd-liheap.added.md | 2 +- policyengine_us/programs.yaml | 3 +- .../eligibility/sd_liheap_eligible.yaml | 151 ++++++++++++++++++ .../gov/states/sd/dss/liheap/sd_liheap.yaml | 113 +++++++++++++ .../liheap/eligibility/sd_liheap_eligible.py | 46 ++++++ .../liheap/payment/sd_liheap_base_payment.py | 6 +- .../payment/sd_liheap_heat_in_rent_payment.py | 5 +- .../gov/states/sd/dss/liheap/sd_liheap.py | 42 +++++ 8 files changed, 360 insertions(+), 8 deletions(-) create mode 100644 policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/eligibility/sd_liheap_eligible.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/sd_liheap.yaml create mode 100644 policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_eligible.py create mode 100644 policyengine_us/variables/gov/states/sd/dss/liheap/sd_liheap.py diff --git a/changelog.d/sd-liheap.added.md b/changelog.d/sd-liheap.added.md index 49a3f5a770d..bceeff695d6 100644 --- a/changelog.d/sd-liheap.added.md +++ b/changelog.d/sd-liheap.added.md @@ -1 +1 @@ -Add South Dakota LIHEAP income, income thresholds, county regions, and direct-heating and heat-in-rent payment components. +Add South Dakota LIHEAP income, eligibility, county regions, and regular heating estimates for supported wholly qualified households, with seasonal expense assumptions and documented coverage limitations. diff --git a/policyengine_us/programs.yaml b/policyengine_us/programs.yaml index e904593fe79..5af0788c673 100644 --- a/policyengine_us/programs.yaml +++ b/policyengine_us/programs.yaml @@ -808,8 +808,9 @@ programs: status: in_progress name: South Dakota LIHEAP full_name: South Dakota Low Income Home Energy Assistance Program + variable: sd_liheap parameter_prefix: gov.states.sd.dss.liheap - notes: Income, county, heat-in-rent and direct-payment components use the latest supported threshold rules and FY2026 matrix. Mixed-status income arithmetic remains unresolved; existing expense inputs do not identify unpaid balances or eligible billing dates. Regular heating only; historical backfills do not establish verified coverage. + notes: Supported wholly qualified households receive an FY2026 regular heating estimate. Vendor payments assume heating_expense reports qualifying unpaid seasonal charges; wood, coal and kerosene use the historical manual's full-payment method. Heat-in-rent assumes seven eligible months of steady tenant rent. Mixed-status arithmetic and unpriced direct fuels remain unsupported, and tribal routing and administrative verification are assumed. Current Mid thresholds and household sizes above ten rely on documented historical patterns. Historical backfills do not establish verified coverage. - state: OR status: in_progress name: LIHEAP diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/eligibility/sd_liheap_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/eligibility/sd_liheap_eligible.yaml new file mode 100644 index 00000000000..77caf778534 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/eligibility/sd_liheap_eligible.yaml @@ -0,0 +1,151 @@ +# ARSD 67:15:01:01 and :09; FY2026 plan sections 1.4a and 17.3. +# Ordinary size-one annual income limit is 4 x published quarterly $7,825. +- name: Case 1, ordinary income includes the published limit and excludes one cent above. + period: 2026 + absolute_error_margin: 0.001 + input: + state_code: [SD, SD, SD] + age: [40, 40, 40] + immigration_status: [CITIZEN, CITIZEN, CITIZEN] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] + heating_expense: [2000, 2000, 2000] + employment_income: [0, 31300, 31300.01] + ssi: [0, 0, 0] + sd_tanf: [0, 0, 0] + snap: + 2026-01: [0, 0, 0] + output: + sd_liheap_countable_income: [0, 31300, 31300.01] + sd_liheap_income_limit: [31300, 31300, 31300] + sd_liheap_eligible: [true, true, false] +- name: Case 2, heating responsibility permits positive tenant rent without a separate bill. + period: 2026 + absolute_error_margin: 0.001 + input: + state_code: [SD, SD, SD, SD, SD] + age: [40, 40, 40, 40, 40] + immigration_status: [CITIZEN, CITIZEN, CITIZEN, CITIZEN, CITIZEN] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NONE] + heating_expense: [100, 0, 0, 0, 1000] + employment_income: [0, 0, 0, 0, 0] + ssi: [0, 0, 0, 0, 0] + sd_tanf: [0, 0, 0, 0, 0] + snap: + 2026-01: [0, 0, 0, 0, 0] + heat_expense_included_in_rent: [false, false, true, true, false] + rent: [0, 0, 1200, 0, 0] + output: + sd_liheap_eligible: [true, false, true, false, false] +- name: Case 3, ordinary LIEAP uses federal qualification without SNAP-specific immigrant bars. + period: 2026 + absolute_error_margin: 0.001 + input: + state_code: [SD, SD, SD, SD] + age: [40, 40, 40, 40] + immigration_status: [CITIZEN, LEGAL_PERMANENT_RESIDENT, REFUGEE, UNDOCUMENTED] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] + heating_expense: [2000, 2000, 2000, 2000] + employment_income: [0, 0, 0, 0] + ssi: [0, 0, 0, 0] + sd_tanf: [0, 0, 0, 0] + snap: + 2026-01: [0, 0, 0, 0] + output: + sd_liheap_eligible: [true, true, true, false] +- name: Case 4, categorical income eligibility requires every household member to receive SNAP. + period: 2026 + absolute_error_margin: 0.001 + input: + people: + adult1: + age: 40 + immigration_status: CITIZEN + employment_income: 100000 + ssi: 0 + child1: + age: 10 + immigration_status: CITIZEN + ssi: 0 + adult2: + age: 40 + immigration_status: CITIZEN + employment_income: 100000 + ssi: 0 + child2: + age: 10 + immigration_status: CITIZEN + ssi: 0 + spm_units: + unit1: + members: &id001 [adult1, child1] + heating_type: NATURAL_GAS + heating_expense: 2000 + sd_tanf: 0 + snap: + 2026-01: 100 + snap_unit_size: + 2026-01: 2 + unit2: + members: &id002 [adult2, child2] + heating_type: NATURAL_GAS + heating_expense: 2000 + sd_tanf: 0 + snap: + 2026-01: 100 + snap_unit_size: + 2026-01: 1 + households: + household1: + members: *id001 + state_code: SD + household2: + members: *id002 + state_code: SD + output: + sd_liheap_countable_income: [100000, 100000] + sd_liheap_eligible: [true, false] +- name: Case 5, mixed-status households return unsupported coverage rather than asserting legal denial. + period: 2026 + absolute_error_margin: 0.001 + input: + people: + adult: + age: 40 + immigration_status: UNDOCUMENTED + ssi: 0 + child: + age: 10 + immigration_status: CITIZEN + ssi: 0 + spm_units: + unit: + members: [adult, child] + heating_type: NATURAL_GAS + heating_expense: 2000 + sd_tanf: 0 + snap: + 2026-01: 0 + households: + household: + members: [adult, child] + state_code: SD + output: + sd_liheap_eligible: false + sd_liheap: 0 +- name: Case 6, another state does not receive South Dakota eligibility. + period: 2026 + absolute_error_margin: 0.001 + input: + state_code: [ND] + age: [40] + immigration_status: [CITIZEN] + heating_type: [NATURAL_GAS] + heating_expense: [2000] + employment_income: [0] + ssi: [0] + sd_tanf: [0] + snap: + 2026-01: [0] + output: + sd_liheap_eligible: [false] + sd_liheap: [0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/sd_liheap.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/sd_liheap.yaml new file mode 100644 index 00000000000..afd76f75590 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/sd_liheap.yaml @@ -0,0 +1,113 @@ +# FY2026 matrix page 1; current application page 2; 2018 manual physical pages 30-32. +# Vendor cases use the approved eligible-unpaid-expense assumption. Cash-fuel +# payments carry forward the manual method; heat-in-rent assumes seven eligible months. +- name: Case 1, vendor fuels pay the smaller of the schedule and qualifying unpaid seasonal charges. + period: 2026 + absolute_error_margin: 0.001 + input: + state_code: [SD, SD, SD, SD, SD, SD, SD, SD] + age: [40, 40, 40, 40, 40, 40, 40, 40] + immigration_status: [CITIZEN, CITIZEN, CITIZEN, CITIZEN, CITIZEN, CITIZEN, CITIZEN, CITIZEN] + heating_type: [NATURAL_GAS, NATURAL_GAS, ELECTRICITY, ELECTRICITY, PROPANE, PROPANE, FUEL_OIL, FUEL_OIL] + heating_expense: [500, 2000, 500, 2000, 500, 3000, 500, 4000] + employment_income: [0, 0, 0, 0, 0, 0, 0, 0] + ssi: [0, 0, 0, 0, 0, 0, 0, 0] + sd_tanf: [0, 0, 0, 0, 0, 0, 0, 0] + snap: + 2026-01: [0, 0, 0, 0, 0, 0, 0, 0] + county_str: [CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, + CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, CUSTER_COUNTY_SD] + output: + sd_liheap_eligible: [true, true, true, true, true, true, true, true] + sd_liheap: [500, 880, 500, 1331, 500, 2499, 500, 2858] +- name: Case 2, wood coal and kerosene pay the full schedule directly to the household. + period: 2026 + absolute_error_margin: 0.001 + input: + state_code: [SD, SD, SD] + age: [40, 40, 40] + immigration_status: [CITIZEN, CITIZEN, CITIZEN] + heating_type: [WOOD, COAL, KEROSENE] + heating_expense: [1, 1, 1] + employment_income: [0, 0, 0] + ssi: [0, 0, 0] + sd_tanf: [0, 0, 0] + snap: + 2026-01: [0, 0, 0] + county_str: [CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, CUSTER_COUNTY_SD] + output: + sd_liheap: [742, 742, 2858] +- name: Case 3, income changes the payment tier and final eligibility gates the award. + period: 2026 + absolute_error_margin: 0.001 + input: + state_code: [SD, SD, SD, SD] + age: [40, 40, 40, 40] + immigration_status: [CITIZEN, CITIZEN, CITIZEN, CITIZEN] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] + heating_expense: [2000, 2000, 2000, 0] + employment_income: [20000, 31300, 31300.01, 0] + ssi: [0, 0, 0, 0] + sd_tanf: [0, 0, 0, 0] + snap: + 2026-01: [0, 0, 0, 0] + county_str: [CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, CUSTER_COUNTY_SD] + output: + sd_liheap: [792, 792, 0, 0] +- name: Case 4, heat-in-rent applies the regional ceiling and seven months of thirty percent tenant rent. + period: 2026 + absolute_error_margin: 0.001 + input: + state_code: [SD, SD, SD, SD, SD] + age: [40, 40, 40, 40, 40] + immigration_status: [CITIZEN, CITIZEN, CITIZEN, CITIZEN, CITIZEN] + heating_type: [UNSPECIFIED, NATURAL_GAS, ELECTRICITY, NATURAL_GAS, NONE] + heating_expense: [0, 0, 0, 0, 0] + employment_income: [0, 0, 0, 0, 0] + ssi: [0, 0, 0, 0, 0] + sd_tanf: [0, 0, 0, 0, 0] + snap: + 2026-01: [0, 0, 0, 0, 0] + heat_expense_included_in_rent: [true, true, true, true, true] + rent: [1200, 6000, 6000, 0, 1200] + county_str: [CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, HARDING_COUNTY_SD, CUSTER_COUNTY_SD, CUSTER_COUNTY_SD] + output: + sd_liheap: [210, 800, 850, 0, 0] +- name: Case 5, unpriced direct fuels retain unsupported zero payments despite modeled eligibility. + period: 2026 + absolute_error_margin: 0.001 + input: + state_code: [SD, SD, SD] + age: [40, 40, 40] + immigration_status: [CITIZEN, CITIZEN, CITIZEN] + heating_type: [SOLAR, OTHER, UNSPECIFIED] + heating_expense: [2000, 2000, 2000] + employment_income: [0, 0, 0] + ssi: [0, 0, 0] + sd_tanf: [0, 0, 0] + snap: + 2026-01: [0, 0, 0] + has_heating_expense: [true, true, true] + county_str: [CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, CUSTER_COUNTY_SD] + output: + sd_liheap_eligible: [true, true, true] + sd_liheap: [0, 0, 0] +- name: Case 6, vendor balances floor at zero and unknown regions remain unsupported. + period: 2026 + absolute_error_margin: 0.001 + input: + state_code: [SD, SD, SD] + age: [40, 40, 40] + immigration_status: [CITIZEN, CITIZEN, CITIZEN] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] + heating_expense: [-100, 0, 100] + employment_income: [0, 0, 0] + ssi: [0, 0, 0] + sd_tanf: [0, 0, 0] + snap: + 2026-01: [0, 0, 0] + has_heating_expense: [true, true, true] + county_str: [CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, UNKNOWN] + output: + sd_liheap_eligible: [true, true, true] + sd_liheap: [0, 0, 0] diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_eligible.py b/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_eligible.py new file mode 100644 index 00000000000..9c2e4c1ccea --- /dev/null +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_eligible.py @@ -0,0 +1,46 @@ +from policyengine_us.model_api import * + + +class sd_liheap_eligible(Variable): + value_type = bool + entity = SPMUnit + definition_period = YEAR + label = "Eligible for South Dakota LIEAP within the modeled household scope" + defined_for = StateCode.SD + reference = ( + "https://sdlegislature.gov/Rules/Administrative/67:15:01:01", + "https://sdlegislature.gov/Rules/Administrative/67:15:01:09", + # Sections 1.4a and 17.3: SNAP recipients and qualified noncitizens. + "https://liheapch.acf.gov/docs/2026/state-plans/SD_Plan_2026.pdf", + ) + + def formula(spm_unit, period, parameters): + size = spm_unit("spm_unit_size", period) + qualified_members = add(spm_unit, period, ["is_citizen_or_legal_immigrant"]) + # The mixed-status income worksheet is unavailable. False for a mixed + # household marks unsupported coverage, not a legal denial of LIEAP. + supported_household = (size > 0) & (qualified_members == size) + income = spm_unit("sd_liheap_countable_income", period) + limit = spm_unit("sd_liheap_income_limit", period) + # All application members must receive SNAP for categorical income + # eligibility. January receipt and SNAP unit size approximate a stable + # application-month caseload; a benefit for only some members is not + # enough. SNAP-specific member exclusions do not gate ordinary LIEAP. + receives_snap = spm_unit("snap", period.first_month) > 0 + all_receive_snap = ( + spm_unit("snap_unit_size", period.first_month) == size + ) & receives_snap + income_eligible = (income <= limit) | all_receive_snap + heating_type = spm_unit("heating_type", period) + has_heat = heating_type != heating_type.possible_values.NONE + heat_in_rent = spm_unit("heat_expense_included_in_rent", period) + responsible_for_heat = where( + heat_in_rent, + add(spm_unit, period, ["rent"]) > 0, + spm_unit("has_heating_expense", period), + ) + # Assume an independent, noninstitutional household served by the state, + # with required heating/landlord verification and no duplicate tribal + # award. Existing inputs cannot establish tribal service-area routing, + # institutional residence, or completed administrative verification. + return supported_household & income_eligible & has_heat & responsible_for_heat diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_base_payment.py b/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_base_payment.py index b44989c9026..d52923dc705 100644 --- a/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_base_payment.py +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_base_payment.py @@ -42,10 +42,8 @@ def formula(spm_unit, period, parameters): default=0, ) # This is an ungated table ceiling, separate from heat-in-rent payments. - # Final income/immigration eligibility and actual unpaid charges are not - # applied here. Gas/electric meter dates must fall in October 1-May 15; - # oil/propane fills in July 1-April 30. Paid charges are not reimbursable. - # Annual heating_expense cannot identify balances or charge dates. + # sd_liheap applies supported eligibility and the approved assumption + # that heating_expense reports eligible unpaid seasonal vendor charges. # Unknown region or unpriced fuel returns an unsupported-component zero, # not a finding that the household is legally ineligible. size = spm_unit("spm_unit_size", period) diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_heat_in_rent_payment.py b/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_heat_in_rent_payment.py index 3372fd7f902..6dead3822ad 100644 --- a/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_heat_in_rent_payment.py +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_heat_in_rent_payment.py @@ -31,8 +31,9 @@ def formula(spm_unit, period, parameters): # The fuel-neutral heat-in-rent schedule permits unknown primary fuel; # no separate heating bill is required. Subsidy does not disqualify a # positive tenant share, while zero share yields zero through the cap. - # This is not final eligibility or a full award: income, immigration, - # tribal routing, landlord verification, and prior payments remain needed. + # sd_liheap applies supported eligibility. This annual estimate assumes + # seven eligible months of steady rent and completed landlord verification; + # prior payments, changes in rent, and tribal routing are not observed. return where( supported_region & heat_in_rent & has_heat, min_(maximum, cap), diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/sd_liheap.py b/policyengine_us/variables/gov/states/sd/dss/liheap/sd_liheap.py new file mode 100644 index 00000000000..43d840e7f61 --- /dev/null +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/sd_liheap.py @@ -0,0 +1,42 @@ +from policyengine_us.model_api import * + + +class sd_liheap(Variable): + value_type = float + entity = SPMUnit + definition_period = YEAR + unit = USD + label = "South Dakota LIEAP regular heating assistance" + defined_for = "sd_liheap_eligible" + reference = ( + "https://liheapch.acf.gov/docs/2026/benefits-matricies/SD_BenefitMatrix_2026.pdf", + "https://dss.sd.gov/formsandpubs/docs/ENERGY/energyassistanceapplication.pdf#page=2", + # Physical pages 30-32: vendor claims, rent payments, and cash fuels. + "https://liheapch.acf.gov/sites/default/files/webfiles/docs/SD_Policy-and-Procedures-Manual2018.pdf#page=30", + ) + + def formula(spm_unit, period, parameters): + base_payment = spm_unit("sd_liheap_base_payment", period) + heating_type = spm_unit("heating_type", period) + fuels = heating_type.possible_values + # The historical manual pays these fuels' full award to the household. + # The current application groups kerosene with oil for the price only. + cash_payment = ( + (heating_type == fuels.WOOD) + | (heating_type == fuels.COAL) + | (heating_type == fuels.KEROSENE) + ) + # Approved input assumption: heating_expense represents unpaid eligible + # seasonal charges, not already-paid bills or an unrestricted annual + # total. Gas/electric meter dates: October 1-May 15; propane/oil fills: + # July 1-April 30. Existing inputs cannot establish those dates/balances. + expense = max_(spm_unit("heating_expense", period), 0) + direct_payment = where(cash_payment, base_payment, min_(base_payment, expense)) + # Heat-in-rent uses a steady tenant share for all seven eligible months; + # changes in rent, already-paid months and prior awards are not tracked. + # Mixed-status households and unpriced fuels remain outside coverage. + return where( + spm_unit("heat_expense_included_in_rent", period), + spm_unit("sd_liheap_heat_in_rent_payment", period), + direct_payment, + ) From c10ea1f8552494bdc99f1b9fc0129c7b597dd3b2 Mon Sep 17 00:00:00 2001 From: Ziming Date: Tue, 6 Oct 2026 18:31:17 -0400 Subject: [PATCH 4/9] Fix South Dakota LIHEAP tables and income counting from review - Encode the published FY2025 three-month income limits (sizes 1-10) as a dated table, since no single rule reproduces them, while FY2024 and FY2026 keep the rule-based limits. - Date the direct-payment matrix from October 2021 (FY2022/FY2023 cells) and October 2023 (FY2024-FY2026 cells). - Count Social Security net of the Medicare Part B premium (ARSD 67:15:01:18(12)), military retirement pay, trust income (manual p25) and gambling winnings (manual p27, followed over the plan); floor each unearned source at zero. - Make mixed-status households eligible: only citizens and eligible aliens count toward household size (manual p9, p18), and ineligible members' income counts in full because the manual's citizenship income calculator does not publish its proration formula. - Count TANF through the take-up-gated tanf variable instead of sd_tanf. - Put SD on the LIHEAP coverage line; verified_years 2024-2026; short note. - Add and update tests for each change. Co-Authored-By: Claude Opus 5.5 --- .../liheap/income/limits/table/amount.yaml | 61 ++++ .../liheap/income/limits/table/in_effect.yaml | 20 ++ .../dss/liheap/income/sources/unearned.yaml | 21 +- .../states/sd/dss/liheap/payment/direct.yaml | 117 ++++--- policyengine_us/programs.yaml | 17 +- .../eligibility/sd_liheap_eligible.yaml | 96 +++++- .../eligibility/sd_liheap_household_size.yaml | 80 +++++ .../eligibility/sd_liheap_income_limit.yaml | 28 +- .../income/sd_liheap_countable_income.yaml | 291 +++++++++++++++++- .../payment/sd_liheap_base_payment.yaml | 24 +- .../sd_liheap_heat_in_rent_payment.yaml | 10 +- .../payment/sd_liheap_income_threshold.yaml | 9 +- .../gov/states/sd/dss/liheap/sd_liheap.yaml | 230 +++++++++++++- .../liheap/eligibility/sd_liheap_eligible.py | 19 +- .../eligibility/sd_liheap_household_size.py | 14 + .../eligibility/sd_liheap_income_limit.py | 16 +- .../income/sd_liheap_countable_income.py | 47 ++- .../payment/sd_liheap_income_threshold.py | 5 +- .../gov/states/sd/dss/liheap/sd_liheap.py | 2 +- 19 files changed, 982 insertions(+), 125 deletions(-) create mode 100644 policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/table/amount.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/table/in_effect.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/eligibility/sd_liheap_household_size.yaml create mode 100644 policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_household_size.py diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/table/amount.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/table/amount.yaml new file mode 100644 index 00000000000..5def67ed8fd --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/table/amount.yaml @@ -0,0 +1,61 @@ +description: South Dakota limits three-month household income to this amount under the Low Income Energy Assistance Program. +# The FY2025 plan, section 2.1 (SD_Plan_2025.pdf#page=8), lists 60% of state +# median income for sizes 1-9 and 150% of the poverty guideline for size 10; +# the published limits for sizes 1-3 equal 200% of the 2024 guideline instead. +# The table stops at size 10; larger households use the income limit rule. +metadata: + type: single_amount + threshold_unit: person + amount_unit: currency-USD + unit: currency-USD + # Amounts are three-month totals; sd_liheap_income_limit multiplies them by + # four. No quarter period exists, so this follows the year convention of + # other quarterly dollar amounts (Pennsylvania minimum_high_quarter_wages, New + # York high_quarter_minimum). + period: year + label: South Dakota LIEAP published three-month income limit + reference: + - title: South Dakota DSS, 2024-2025 Heating Season Income Limits (archived January 14, 2025) + href: https://web.archive.org/web/20250114001211/https://dss.sd.gov/economicassistance/energy_weatherization_assistance.aspx + +brackets: + - threshold: + 2024-10-01: 1 + amount: + 2024-10-01: 7_530 + - threshold: + 2024-10-01: 2 + amount: + 2024-10-01: 10_220 + - threshold: + 2024-10-01: 3 + amount: + 2024-10-01: 12_910 + - threshold: + 2024-10-01: 4 + amount: + 2024-10-01: 15_858 + - threshold: + 2024-10-01: 5 + amount: + 2024-10-01: 18_396 + - threshold: + 2024-10-01: 6 + amount: + 2024-10-01: 20_933 + - threshold: + 2024-10-01: 7 + amount: + 2024-10-01: 21_409 + - threshold: + 2024-10-01: 8 + amount: + 2024-10-01: 21_884 + - threshold: + 2024-10-01: 9 + amount: + 2024-10-01: 22_360 + - threshold: + 2024-10-01: 10 + amount: + 2024-10-01: 23_805 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/table/in_effect.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/table/in_effect.yaml new file mode 100644 index 00000000000..979209e7449 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/table/in_effect.yaml @@ -0,0 +1,20 @@ +description: South Dakota uses its published three-month income limit table when this is true under the Low Income Energy Assistance Program. +values: + # The poverty guideline and state median income rule reproduces every + # FY2024 matrix limit (sizes 1-15). + 2023-10-01: false + # The published 2024-2025 limits for sizes 4-7 depart from that rule. + 2024-10-01: true + # The rule reproduces every published 2025-2026 limit (sizes 1-10). + 2025-10-01: false +metadata: + unit: bool + period: year + label: South Dakota LIEAP published income limit table in effect + reference: + - title: South Dakota FY2024 LIHEAP Benefit Matrix, maximum income, pages 1-5 + href: https://liheapch.acf.gov/docs/2024/benefits-matricies/SD_BenefitMatrix_2024.pdf#page=1 + - title: South Dakota DSS, 2024-2025 Heating Season Income Limits (archived January 14, 2025) + href: https://web.archive.org/web/20250114001211/https://dss.sd.gov/economicassistance/energy_weatherization_assistance.aspx + - title: South Dakota DSS, 2025-2026 Heating Season Income Limits + href: https://dss.sd.gov/economicassistance/energy_weatherization_assistance.aspx diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/unearned.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/unearned.yaml index b55fc81b7d5..e111b20f91a 100644 --- a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/unearned.yaml +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/unearned.yaml @@ -1,9 +1,14 @@ description: South Dakota counts these unearned income sources for members of all ages under the Low Income Energy Assistance Program. values: 2025-10-01: - - social_security + # Social Security is counted in the formula, net of the Medicare Part B + # premium (rule 67:15:01:18(12)), and TANF through the take-up-gated tanf. - ssi - pension_income + # Military retirement pay is a separate input from the pension inputs that + # pension_income adds, so both are listed; a household reports the amount + # in one or the other. + - military_retirement_pay - railroad_benefits - survivor_benefits - unemployment_compensation @@ -14,9 +19,15 @@ values: - farm_rent_income - strike_benefits - general_assistance - - sd_tanf -# State TANF is counted once at SPM level. Pension/SSA subcomponents are not -# added again. Interest, dividends, capital gains, and cash gifts are excluded. + # The manual (page 25) counts trust income ("Trusts", Y). + - estate_income + # The manual (page 27) counts "Lottery or gaming winnings" in full: "If + # received in the 3 month LIEAP time frame count entire amount." The FY2026 + # plan, section 1.9 (page 6), leaves "One-time lump-sum payments, such as + # rebates/credits, winnings from lotteries" unchecked. The manual is followed. + - gambling_winnings +# Pension and Social Security subcomponents are not added again. Interest, +# dividends, capital gains and cash gifts are excluded. metadata: unit: list period: year @@ -26,3 +37,5 @@ metadata: href: https://liheapch.acf.gov/docs/2026/state-plans/SD_Plan_2026.pdf#page=5 - title: South Dakota Administrative Rule 67:15:01:18, Income exclusions href: https://sdlegislature.gov/Rules/Administrative/67:15:01:18 + - title: "South Dakota LIEAP Policy and Procedures Manual (2018), countable income table, #page 25,27,28" + href: https://liheapch.acf.gov/sites/default/files/webfiles/docs/SD_Policy-and-Procedures-Manual2018.pdf#page=25 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/direct.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/direct.yaml index 8e69165d626..8a5ba863ac7 100644 --- a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/direct.yaml +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/direct.yaml @@ -1,110 +1,139 @@ -description: South Dakota provides these direct-heating base payments by fuel, heating region, and income tier under the Low Income Energy Assistance Program. These are table ceilings before unpaid-charge limits and final eligibility. +description: South Dakota provides this amount as the direct-heating base payment under the Low Income Energy Assistance Program. +# The FY2022 and FY2023 matrices print the same cells, as do the FY2024 and +# FY2026 matrices. No FY2025 matrix is published; the FY2025 plan's $668 +# minimum and $3,437 maximum (section 2.6, SD_Plan_2025.pdf#page=9) match the +# FY2024 cells. Coal and wood and natural gas cells are unchanged from FY2022 +# through FY2026. coal_wood: 1: 1: - 2025-10-01: 742 + 2021-10-01: 742 2: - 2025-10-01: 668 + 2021-10-01: 668 2: 1: - 2025-10-01: 770 + 2021-10-01: 770 2: - 2025-10-01: 694 + 2021-10-01: 694 3: 1: - 2025-10-01: 812 + 2021-10-01: 812 2: - 2025-10-01: 730 + 2021-10-01: 730 4: 1: - 2025-10-01: 854 + 2021-10-01: 854 2: - 2025-10-01: 768 + 2021-10-01: 768 electricity: 1: 1: - 2025-10-01: 1331 + 2021-10-01: 1_210 + 2023-10-01: 1_331 2: - 2025-10-01: 1198 + 2021-10-01: 1_089 + 2023-10-01: 1_198 2: 1: - 2025-10-01: 1410 + 2021-10-01: 1_281 + 2023-10-01: 1_410 2: - 2025-10-01: 1270 + 2021-10-01: 1_154 + 2023-10-01: 1_270 3: 1: - 2025-10-01: 1516 + 2021-10-01: 1_378 + 2023-10-01: 1_516 2: - 2025-10-01: 1369 + 2021-10-01: 1_244 + 2023-10-01: 1_369 4: 1: - 2025-10-01: 1626 + 2021-10-01: 1_478 + 2023-10-01: 1_626 2: - 2025-10-01: 1461 + 2021-10-01: 1_328 + 2023-10-01: 1_461 fuel_oil: 1: 1: - 2025-10-01: 2858 + 2021-10-01: 1_905 + 2023-10-01: 2_858 2: - 2025-10-01: 2573 + 2021-10-01: 1_715 + 2023-10-01: 2_573 2: 1: - 2025-10-01: 3018 + 2021-10-01: 2_012 + 2023-10-01: 3_018 2: - 2025-10-01: 2715 + 2021-10-01: 1_810 + 2023-10-01: 2_715 3: 1: - 2025-10-01: 3236 + 2021-10-01: 2_157 + 2023-10-01: 3_236 2: - 2025-10-01: 2910 + 2021-10-01: 1_940 + 2023-10-01: 2_910 4: 1: - 2025-10-01: 3437 + 2021-10-01: 2_291 + 2023-10-01: 3_437 2: - 2025-10-01: 3093 + 2021-10-01: 2_062 + 2023-10-01: 3_093 natural_gas: 1: 1: - 2025-10-01: 880 + 2021-10-01: 880 2: - 2025-10-01: 792 + 2021-10-01: 792 2: 1: - 2025-10-01: 938 + 2021-10-01: 938 2: - 2025-10-01: 844 + 2021-10-01: 844 3: 1: - 2025-10-01: 1007 + 2021-10-01: 1_007 2: - 2025-10-01: 907 + 2021-10-01: 907 4: 1: - 2025-10-01: 1058 + 2021-10-01: 1_058 2: - 2025-10-01: 953 + 2021-10-01: 953 propane: 1: 1: - 2025-10-01: 2499 + 2021-10-01: 1_666 + 2023-10-01: 2_499 2: - 2025-10-01: 2249 + 2021-10-01: 1_499 + 2023-10-01: 2_249 2: 1: - 2025-10-01: 2658 + 2021-10-01: 1_772 + 2023-10-01: 2_658 2: - 2025-10-01: 2394 + 2021-10-01: 1_596 + 2023-10-01: 2_394 3: 1: - 2025-10-01: 2834 + 2021-10-01: 1_889 + 2023-10-01: 2_834 2: - 2025-10-01: 2552 + 2021-10-01: 1_701 + 2023-10-01: 2_552 4: 1: - 2025-10-01: 3066 + 2021-10-01: 2_044 + 2023-10-01: 3_066 2: - 2025-10-01: 2762 + 2021-10-01: 1_841 + 2023-10-01: 2_762 metadata: unit: currency-USD @@ -116,5 +145,11 @@ metadata: - range(1, 3) propagate_metadata_to_children: true reference: + - title: South Dakota FY2022 LIHEAP Benefit Matrix, Own and Rent Cases + href: https://liheapch.acf.gov/sites/default/files/webfiles/docs/SD_BenefitMatrix_2022.pdf#page=1 + - title: South Dakota FY2023 LIHEAP Benefit Matrix, Own and Rent Cases + href: https://liheapch.acf.gov/docs/2023/benefits-matricies/SD_BenefitMatrix_2023.pdf#page=1 + - title: South Dakota FY2024 LIHEAP Benefit Matrix, Own and Rent Cases + href: https://liheapch.acf.gov/docs/2024/benefits-matricies/SD_BenefitMatrix_2024.pdf#page=1 - title: South Dakota FY2026 LIHEAP Benefit Matrix, Own and Rent Cases href: https://liheapch.acf.gov/docs/2026/benefits-matricies/SD_BenefitMatrix_2026.pdf#page=1 diff --git a/policyengine_us/programs.yaml b/policyengine_us/programs.yaml index 5af0788c673..d9c6cc2b762 100644 --- a/policyengine_us/programs.yaml +++ b/policyengine_us/programs.yaml @@ -802,15 +802,8 @@ programs: category: Benefits agency: HHS status: partial - coverage: OR, DC, Riverside County, MA, IL, KS, MS, TX + coverage: OR, DC, Riverside County, MA, IL, KS, MS, SD, TX state_implementations: - - state: SD - status: in_progress - name: South Dakota LIHEAP - full_name: South Dakota Low Income Home Energy Assistance Program - variable: sd_liheap - parameter_prefix: gov.states.sd.dss.liheap - notes: Supported wholly qualified households receive an FY2026 regular heating estimate. Vendor payments assume heating_expense reports qualifying unpaid seasonal charges; wood, coal and kerosene use the historical manual's full-payment method. Heat-in-rent assumes seven eligible months of steady tenant rent. Mixed-status arithmetic and unpriced direct fuels remain unsupported, and tribal routing and administrative verification are assumed. Current Mid thresholds and household sizes above ten rely on documented historical patterns. Historical backfills do not establish verified coverage. - state: OR status: in_progress name: LIHEAP @@ -846,6 +839,14 @@ programs: 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. + - state: SD + status: partial + name: South Dakota LIHEAP + full_name: South Dakota Low Income Home Energy Assistance Program + variable: sd_liheap + parameter_prefix: gov.states.sd.dss.liheap + verified_years: "2024-2026" + notes: FY2024-FY2026 regular heating only. Crisis, cooling and weatherization assistance are out of scope. - state: TX status: partial name: Texas CEAP diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/eligibility/sd_liheap_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/eligibility/sd_liheap_eligible.yaml index 77caf778534..3ba82157314 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/eligibility/sd_liheap_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/eligibility/sd_liheap_eligible.yaml @@ -11,7 +11,7 @@ heating_expense: [2000, 2000, 2000] employment_income: [0, 31300, 31300.01] ssi: [0, 0, 0] - sd_tanf: [0, 0, 0] + tanf: [0, 0, 0] snap: 2026-01: [0, 0, 0] output: @@ -29,7 +29,7 @@ heating_expense: [100, 0, 0, 0, 1000] employment_income: [0, 0, 0, 0, 0] ssi: [0, 0, 0, 0, 0] - sd_tanf: [0, 0, 0, 0, 0] + tanf: [0, 0, 0, 0, 0] snap: 2026-01: [0, 0, 0, 0, 0] heat_expense_included_in_rent: [false, false, true, true, false] @@ -47,7 +47,7 @@ heating_expense: [2000, 2000, 2000, 2000] employment_income: [0, 0, 0, 0] ssi: [0, 0, 0, 0] - sd_tanf: [0, 0, 0, 0] + tanf: [0, 0, 0, 0] snap: 2026-01: [0, 0, 0, 0] output: @@ -80,7 +80,7 @@ members: &id001 [adult1, child1] heating_type: NATURAL_GAS heating_expense: 2000 - sd_tanf: 0 + tanf: 0 snap: 2026-01: 100 snap_unit_size: @@ -89,7 +89,7 @@ members: &id002 [adult2, child2] heating_type: NATURAL_GAS heating_expense: 2000 - sd_tanf: 0 + tanf: 0 snap: 2026-01: 100 snap_unit_size: @@ -104,7 +104,11 @@ output: sd_liheap_countable_income: [100000, 100000] sd_liheap_eligible: [true, false] -- name: Case 5, mixed-status households return unsupported coverage rather than asserting legal denial. +# Manual physical page 9: mixed households are eligible, and only citizens and +# eligible aliens count as members. Size 1 (the child): limit $31,300, Mid +# $19,564; $0 income is tier 1, so Custer County (region 1) gas pays +# min($880, $2,000) = $880. +- name: Case 5, a mixed-status household is eligible and counts only its qualified member. period: 2026 absolute_error_margin: 0.001 input: @@ -122,16 +126,19 @@ members: [adult, child] heating_type: NATURAL_GAS heating_expense: 2000 - sd_tanf: 0 + tanf: 0 snap: 2026-01: 0 households: household: members: [adult, child] state_code: SD + county: CUSTER_COUNTY_SD output: - sd_liheap_eligible: false - sd_liheap: 0 + sd_liheap_household_size: 1 + sd_liheap_income_limit: 31_300 + sd_liheap_eligible: true + sd_liheap: 880 - name: Case 6, another state does not receive South Dakota eligibility. period: 2026 absolute_error_margin: 0.001 @@ -143,9 +150,78 @@ heating_expense: [2000] employment_income: [0] ssi: [0] - sd_tanf: [0] + tanf: [0] snap: 2026-01: [0] output: sd_liheap_eligible: [false] sd_liheap: [0] + +# 2024-2025 published table (DSS page archived January 14, 2025): size 4 limit +# $15,858 a quarter x 4 = $63,432. Income at the limit is eligible; one cent +# above is not. +- name: Case 7, four-person households in 2025 include the published limit and exclude one cent above. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + employment_income: 63_432 + ssi: 0 + person2: + age: 38 + immigration_status: CITIZEN + ssi: 0 + person3: + age: 10 + immigration_status: CITIZEN + ssi: 0 + person4: + age: 7 + immigration_status: CITIZEN + ssi: 0 + person5: + age: 40 + immigration_status: CITIZEN + employment_income: 63_432.01 + ssi: 0 + person6: + age: 38 + immigration_status: CITIZEN + ssi: 0 + person7: + age: 10 + immigration_status: CITIZEN + ssi: 0 + person8: + age: 7 + immigration_status: CITIZEN + ssi: 0 + spm_units: + spm_unit1: + members: [person1, person2, person3, person4] + heating_type: NATURAL_GAS + heating_expense: 1_000 + tanf: 0 + snap: + 2025-01: 0 + spm_unit2: + members: [person5, person6, person7, person8] + heating_type: NATURAL_GAS + heating_expense: 1_000 + tanf: 0 + snap: + 2025-01: 0 + households: + household1: + members: [person1, person2, person3, person4] + state_code: SD + household2: + members: [person5, person6, person7, person8] + state_code: SD + output: + sd_liheap_income_limit: [63_432, 63_432] + sd_liheap_countable_income: [63_432, 63_432.01] + sd_liheap_eligible: [true, false] diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/eligibility/sd_liheap_household_size.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/eligibility/sd_liheap_household_size.yaml new file mode 100644 index 00000000000..8ced372acbf --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/eligibility/sd_liheap_household_size.yaml @@ -0,0 +1,80 @@ +# Manual physical page 9: "Only citizens and eligible aliens are counted as +# actual HH members"; page 18: ineligible aliens are not in the household count. + +# Two citizens and a lawful permanent resident (a qualified noncitizen) = 3. +- name: Case 1, citizens and qualified noncitizens all count. + period: 2026 + input: + people: + person1: + age: 35 + immigration_status: CITIZEN + person2: + age: 33 + immigration_status: LEGAL_PERMANENT_RESIDENT + person3: + age: 6 + immigration_status: CITIZEN + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: SD + output: + sd_liheap_household_size: 3 + +# Citizen adult + citizen child = 2; the undocumented adult is not counted. +- name: Case 2, an undocumented adult is excluded from the household count. + period: 2026 + input: + people: + person1: + age: 35 + immigration_status: CITIZEN + person2: + age: 33 + immigration_status: UNDOCUMENTED + person3: + age: 6 + immigration_status: CITIZEN + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: SD + output: + sd_liheap_household_size: 2 + +# Neither undocumented adult is counted, so the size is 0. +- name: Case 3, a household with no citizen or qualified noncitizen has size zero. + period: 2026 + input: + people: + person1: + age: 35 + immigration_status: UNDOCUMENTED + person2: + age: 33 + immigration_status: UNDOCUMENTED + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: SD + output: + sd_liheap_household_size: 0 + +# Defined for South Dakota only. +- name: Case 4, another state has no South Dakota household size. + period: 2026 + input: + state_code: [ND] + immigration_status: [CITIZEN] + output: + sd_liheap_household_size: [0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.yaml index 2c95a56e6a4..cac1d74bad5 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.yaml @@ -8,7 +8,7 @@ state_code: [SD, SD, SD, SD, SD, SD, SD, SD, SD, SD] heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] - spm_unit_size: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + sd_liheap_household_size: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] output: sd_liheap_income_limit: [31300, 42300, 53300, 64300, 75300, 86300, 90056, 92056, 94056, 97724] @@ -20,7 +20,7 @@ state_code: [SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD] heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] - spm_unit_size: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] + sd_liheap_household_size: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] output: sd_liheap_income_limit: [29160, 39308, 48560, 57808, 67056, 76308, 78040, 79776, 83548, 91260, 98968, 106680, 114388, 122100, 129808] @@ -32,17 +32,35 @@ input: state_code: [SD, SD, SD, SD, SD] heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] - spm_unit_size: [11, 12, 13, 14, 15] + sd_liheap_household_size: [11, 12, 13, 14, 15] output: sd_liheap_income_limit: [105972, 114224, 122472, 130724, 138972] -# Source coverage is SouthDakota fully qualified households; size0 guard supplies no one-person limit, while size1 uses7825quarter*4. This is not a mixed-status eligibility test. +# A household size of 0 (no citizen or eligible alien) and another state get +# no limit; size 1 uses $7,825 x 4 = $31,300. - name: Case 4, zero household size and a different state have no South Dakota income limit. period: 2026 absolute_error_margin: 0.01 input: state_code: [SD, ND, SD] heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] - spm_unit_size: [0, 1, 1] + sd_liheap_household_size: [0, 1, 1] output: sd_liheap_income_limit: [0, 0, 31300] + +# 2024-2025 published table (DSS page archived January 14, 2025), quarterly +# $7,530, $10,220, $12,910, $15,858, $18,396, $20,933, $21,409, $21,884, +# $22,360, $23,805, each x 4. Size 11 uses the rule: 2024 FPG $68,860 x 150% = +# $103,290 (above 60% SMI share 63,433 x 1.47 = $93,246.51, below 200%), quarter +# floored to $25,822 x 4 = $103,288. Cases 1 and 2 keep 2026 and 2024 unchanged. +- name: Case 5, the published 2025 three month limits apply for sizes one to ten. + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: [SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD] + heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, + NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] + sd_liheap_household_size: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] + output: + sd_liheap_income_limit: [30_120, 40_880, 51_640, 63_432, 73_584, 83_732, 85_636, 87_536, 89_440, 95_220, + 103_288] diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/income/sd_liheap_countable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/income/sd_liheap_countable_income.yaml index 1f9acb46d4c..f47def136ba 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/income/sd_liheap_countable_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/income/sd_liheap_countable_income.yaml @@ -21,7 +21,7 @@ members: [person1] heating_type: NATURAL_GAS gas_expense: 1_200 - sd_tanf: 0 + tanf: 0 households: household: members: [person1] @@ -50,7 +50,7 @@ members: [person1] heating_type: NATURAL_GAS gas_expense: 1_200 - sd_tanf: 0 + tanf: 0 households: household: members: [person1] @@ -85,7 +85,7 @@ members: [person1, person2] heating_type: NATURAL_GAS gas_expense: 1_200 - sd_tanf: 0 + tanf: 0 households: household: members: [person1, person2] @@ -120,7 +120,7 @@ members: [person1, person2] heating_type: NATURAL_GAS gas_expense: 1_200 - sd_tanf: 0 + tanf: 0 households: household: members: [person1, person2] @@ -155,7 +155,7 @@ members: [person1, person2] heating_type: NATURAL_GAS gas_expense: 1_200 - sd_tanf: 0 + tanf: 0 households: household: members: [person1, person2] @@ -190,7 +190,7 @@ members: [person1, person2] heating_type: NATURAL_GAS gas_expense: 1_200 - sd_tanf: 0 + tanf: 0 households: household: members: [person1, person2] @@ -227,7 +227,7 @@ members: [person1] heating_type: NATURAL_GAS gas_expense: 1_200 - sd_tanf: 0 + tanf: 0 households: household: members: [person1] @@ -257,7 +257,7 @@ members: [person1] heating_type: NATURAL_GAS gas_expense: 1_200 - sd_tanf: 0 + tanf: 0 snap: 6_000 households: household: @@ -289,7 +289,7 @@ members: [person1, person2] heating_type: NATURAL_GAS gas_expense: 1_200 - sd_tanf: 3_600 + tanf: 3_600 households: household: members: [person1, person2] @@ -316,7 +316,7 @@ members: [person1] heating_type: NATURAL_GAS gas_expense: 1_200 - sd_tanf: 0 + tanf: 0 households: household: members: [person1] @@ -343,7 +343,7 @@ members: [person1] heating_type: NATURAL_GAS gas_expense: 1_200 - sd_tanf: 0 + tanf: 0 households: household: members: [person1] @@ -372,7 +372,7 @@ members: [person1] heating_type: NATURAL_GAS gas_expense: 1_200 - sd_tanf: 0 + tanf: 0 households: household: members: [person1] @@ -381,3 +381,270 @@ output: sd_liheap_countable_income: 40_000 sd_liheap_income_limit: 31_300 + +# Rule 67:15:01:18(12); manual page 28. 2026 Part B premium $202.90 x 12 = +# $2,434.80; $32,000 - $2,434.80 = $29,565.20. +- name: Case 13, Social Security counts net of the Medicare Part B premium. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + social_security: 32_000 + medicare_part_b_premium: 2_434.8 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 1_200 + tanf: 0 + households: + household: + members: [person1] + state_code: SD + county: CUSTER_COUNTY_SD + output: + sd_liheap_countable_income: 29_565.2 + +# The netted Social Security floors at zero: max($1,800 - $2,434.80, 0) = $0, +# so the unrecovered $634.80 of premium does not reduce the $24,000 pension. +- name: Case 14, a Part B premium above the Social Security benefit does not reduce other income. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 72 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + social_security: 1_800 + medicare_part_b_premium: 2_434.8 + pension_income: 24_000 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 1_200 + tanf: 0 + households: + household: + members: [person1] + state_code: SD + county: CUSTER_COUNTY_SD + output: + sd_liheap_countable_income: 24_000 + +# Manual page 25 counts retirement payments; military retirement pay is a +# separate input from pension_income: $35,000 counts in full. +- name: Case 15, military retirement pay counts as retirement income. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 50 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + military_retirement_pay: 35_000 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 1_200 + tanf: 0 + households: + household: + members: [person1] + state_code: SD + county: CUSTER_COUNTY_SD + output: + sd_liheap_countable_income: 35_000 + +# Rule 67:15:01:18.01; manual page 25. Each source floors at zero: $12,000 wages +# + $0 rental (-$4,000) + $0 farm rent (-$1,000) + $2,400 child support received +# = $14,400, not $12,000 - $4,000 - $1,000 + $2,400 = $9,400. +- name: Case 16, rental and farm rent losses do not offset wages or other unearned income. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + employment_income: 12_000 + rental_income: -4_000 + farm_rent_income: -1_000 + child_support_received: 2_400 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 1_200 + tanf: 0 + households: + household: + members: [person1] + state_code: SD + county: CUSTER_COUNTY_SD + output: + sd_liheap_countable_income: 14_400 + +# Manual page 27 counts lottery or gaming winnings in full (the plan, section +# 1.9, leaves winnings unchecked; the manual is followed): $12,000 + $5,000. +- name: Case 17, lottery and gaming winnings count in full. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + employment_income: 12_000 + gambling_winnings: 5_000 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 1_200 + tanf: 0 + households: + household: + members: [person1] + state_code: SD + county: CUSTER_COUNTY_SD + output: + sd_liheap_countable_income: 17_000 + +# Plan section 1.9 counts TANF received. The take-up-gated tanf includes the +# $3,600 sd_tanf grant: $6,000 wages + $3,600 = $9,600. +- name: Case 18, TANF the household takes up counts as income. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + employment_income: 6_000 + person2: + age: 5 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + spm_units: + spm_unit: + members: [person1, person2] + heating_type: NATURAL_GAS + gas_expense: 1_200 + sd_tanf: 3_600 + takes_up_tanf_if_eligible: true + households: + household: + members: [person1, person2] + state_code: SD + county: CUSTER_COUNTY_SD + output: + tanf: 3_600 + sd_liheap_countable_income: 9_600 + +# An sd_tanf entitlement the household does not take up is not received: +# tanf = $0, so countable income is the $6,000 of wages. +- name: Case 19, TANF the household does not take up is not counted. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + employment_income: 6_000 + person2: + age: 5 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + spm_units: + spm_unit: + members: [person1, person2] + heating_type: NATURAL_GAS + gas_expense: 1_200 + sd_tanf: 3_600 + takes_up_tanf_if_eligible: false + households: + household: + members: [person1, person2] + state_code: SD + county: CUSTER_COUNTY_SD + output: + tanf: 0 + sd_liheap_countable_income: 6_000 + +# Manual page 25 counts trust income: $20,000 wages + $6,000 estate and trust +# income = $26,000. +- name: Case 20, estate and trust income counts as unearned income. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 50 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + employment_income: 20_000 + estate_income: 6_000 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 1_200 + tanf: 0 + households: + household: + members: [person1] + state_code: SD + county: CUSTER_COUNTY_SD + output: + sd_liheap_countable_income: 26_000 + +# Each source floors at zero: $20,000 wages + max(-$3,000 trust loss, 0) = +# $20,000, not $17,000. +- name: Case 21, an estate or trust loss does not offset wages. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 50 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + employment_income: 20_000 + estate_income: -3_000 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 1_200 + tanf: 0 + households: + household: + members: [person1] + state_code: SD + county: CUSTER_COUNTY_SD + output: + sd_liheap_countable_income: 20_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_base_payment.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_base_payment.yaml index bb59e6e0a4b..387be19dfca 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_base_payment.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_base_payment.yaml @@ -45,6 +45,7 @@ heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] sd_liheap_region: [1, 1, 1, 1, 1] spm_unit_size: [1, 1, 2, 2, 2] + sd_liheap_household_size: [1, 1, 2, 2, 2] sd_liheap_countable_income: [18224, 18224.01, 24650, 24652, 24652.01] output: sd_liheap_income_threshold: [18224, 18224, 24652, 24652, 24652] @@ -76,7 +77,9 @@ output: sd_liheap_base_payment: [0, 0, 0, 0, 0, 0, 0, 0] -# Wholly qualified households: size1Mid19564 puts20000in tier2; size2Mid26436 puts identicalincome in tier1. Region1gas retains792/880; size does not independently multiply the payment. +# Size 1 Mid $19,564 puts $20,000 in tier 2; size 2 Mid $26,436 puts the same +# income in tier 1. Region 1 gas pays $792 and $880; size does not multiply the +# payment. - name: Case 6, actual household membership changes the tier while leaving the regional fuel schedule shared. period: 2026 absolute_error_margin: 0.01 @@ -115,3 +118,22 @@ output: sd_liheap_income_threshold: [19564, 26436] sd_liheap_base_payment: [792, 880] + +# FY2022 and FY2023 matrices print the same cells. 2023 size 1 Mid: 125% x 2022 +# FPG $13,590 / 4 = $4,246.875, rounded half up to $4,247 x 4 = $16,988, so $0 +# is tier 1 and $20,000 is tier 2. Electricity r2 t1 $1,281, r4 t2 $1,328; fuel +# oil r3 t1 $2,157, r4 t2 $2,062; kerosene uses fuel oil r2 t2 $1,810; propane +# r2 t1 $1,772, r3 t2 $1,701; natural gas r4 t1 $1,058 and coal r3 t2 $730 are +# unchanged from FY2026. +- name: Case 7, 2023 base payments use the FY2022 and FY2023 matrix cells. + period: 2023 + absolute_error_margin: 0.01 + input: + state_code: [SD, SD, SD, SD, SD, SD, SD, SD, SD] + heating_type: [ELECTRICITY, ELECTRICITY, FUEL_OIL, FUEL_OIL, KEROSENE, PROPANE, PROPANE, NATURAL_GAS, COAL] + sd_liheap_region: [2, 4, 3, 4, 2, 2, 3, 4, 3] + spm_unit_size: [1, 1, 1, 1, 1, 1, 1, 1, 1] + sd_liheap_countable_income: [0, 20_000, 0, 20_000, 20_000, 0, 20_000, 0, 20_000] + output: + sd_liheap_income_threshold: [16_988, 16_988, 16_988, 16_988, 16_988, 16_988, 16_988, 16_988, 16_988] + sd_liheap_base_payment: [1_281, 1_328, 2_157, 2_062, 1_810, 1_772, 1_701, 1_058, 730] diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_heat_in_rent_payment.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_heat_in_rent_payment.yaml index 1fae90eabd5..d4f840093e2 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_heat_in_rent_payment.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_heat_in_rent_payment.yaml @@ -32,7 +32,7 @@ members: [person1] heating_type: NATURAL_GAS gas_expense: 0 - sd_tanf: 0 + tanf: 0 heat_expense_included_in_rent: true households: household: @@ -71,7 +71,7 @@ members: [person1, person2, person3] heating_type: NATURAL_GAS gas_expense: 0 - sd_tanf: 0 + tanf: 0 heat_expense_included_in_rent: true households: household: @@ -110,7 +110,7 @@ members: [person1, person2, person3] heating_type: NATURAL_GAS gas_expense: 0 - sd_tanf: 0 + tanf: 0 heat_expense_included_in_rent: true housing_assistance: 9_600 receives_housing_assistance: true @@ -141,7 +141,7 @@ members: [person1] heating_type: NATURAL_GAS gas_expense: 0 - sd_tanf: 0 + tanf: 0 heat_expense_included_in_rent: true housing_assistance: 12_000 receives_housing_assistance: true @@ -171,7 +171,7 @@ members: [person1] heating_type: NATURAL_GAS gas_expense: 1_200 - sd_tanf: 0 + tanf: 0 heat_expense_included_in_rent: false households: household: diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_income_threshold.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_income_threshold.yaml index c744a7e8bf1..d53a0694821 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_income_threshold.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_income_threshold.yaml @@ -8,7 +8,7 @@ state_code: [SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD, SD] heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] - spm_unit_size: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] + sd_liheap_household_size: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] output: sd_liheap_income_threshold: [18224, 24652, 31076, 37500, 43924, 50352, 56776, 63200, 69624, 76052, 82476, 88900, 95324, 101752, 108176] @@ -20,17 +20,18 @@ input: state_code: [SD, SD, SD, SD, SD] heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] - spm_unit_size: [1, 2, 9, 10, 15] + sd_liheap_household_size: [1, 2, 9, 10, 15] output: sd_liheap_income_threshold: [19564, 26436, 74564, 81436, 115812] -# Fully qualified ordinary-household scope and state/size guards; size1 annualizedMid4891*4=19564. +# A household size of 0 and another state get no threshold; size 1 Mid is +# $4,891 x 4 = $19,564. - name: Case 3, zero size and another state do not receive a South Dakota tier threshold. period: 2026 absolute_error_margin: 0.01 input: state_code: [SD, ND, SD] heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] - spm_unit_size: [0, 1, 1] + sd_liheap_household_size: [0, 1, 1] output: sd_liheap_income_threshold: [0, 0, 19564] diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/sd_liheap.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/sd_liheap.yaml index afd76f75590..a7479e7b06d 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/sd_liheap.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/sd_liheap.yaml @@ -12,7 +12,7 @@ heating_expense: [500, 2000, 500, 2000, 500, 3000, 500, 4000] employment_income: [0, 0, 0, 0, 0, 0, 0, 0] ssi: [0, 0, 0, 0, 0, 0, 0, 0] - sd_tanf: [0, 0, 0, 0, 0, 0, 0, 0] + tanf: [0, 0, 0, 0, 0, 0, 0, 0] snap: 2026-01: [0, 0, 0, 0, 0, 0, 0, 0] county_str: [CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, @@ -31,7 +31,7 @@ heating_expense: [1, 1, 1] employment_income: [0, 0, 0] ssi: [0, 0, 0] - sd_tanf: [0, 0, 0] + tanf: [0, 0, 0] snap: 2026-01: [0, 0, 0] county_str: [CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, CUSTER_COUNTY_SD] @@ -48,7 +48,7 @@ heating_expense: [2000, 2000, 2000, 0] employment_income: [20000, 31300, 31300.01, 0] ssi: [0, 0, 0, 0] - sd_tanf: [0, 0, 0, 0] + tanf: [0, 0, 0, 0] snap: 2026-01: [0, 0, 0, 0] county_str: [CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, CUSTER_COUNTY_SD] @@ -65,7 +65,7 @@ heating_expense: [0, 0, 0, 0, 0] employment_income: [0, 0, 0, 0, 0] ssi: [0, 0, 0, 0, 0] - sd_tanf: [0, 0, 0, 0, 0] + tanf: [0, 0, 0, 0, 0] snap: 2026-01: [0, 0, 0, 0, 0] heat_expense_included_in_rent: [true, true, true, true, true] @@ -84,7 +84,7 @@ heating_expense: [2000, 2000, 2000] employment_income: [0, 0, 0] ssi: [0, 0, 0] - sd_tanf: [0, 0, 0] + tanf: [0, 0, 0] snap: 2026-01: [0, 0, 0] has_heating_expense: [true, true, true] @@ -103,7 +103,7 @@ heating_expense: [-100, 0, 100] employment_income: [0, 0, 0] ssi: [0, 0, 0] - sd_tanf: [0, 0, 0] + tanf: [0, 0, 0] snap: 2026-01: [0, 0, 0] has_heating_expense: [true, true, true] @@ -111,3 +111,221 @@ output: sd_liheap_eligible: [true, true, true] sd_liheap: [0, 0, 0] + +# 2024-2025 published table (DSS page archived January 14, 2025): size 4 limit +# $15,858 a quarter x 4 = $63,432; $63,000 of wages is under it. The rule alone +# would cap size 4 at 200% x 2024 FPG $31,200 = $62,400. Mid: 125% x $31,200 = +# $39,000, so tier 2. Lincoln County is region 1: natural gas tier 2 = $792; +# min($792, $1,000 unpaid charges) = $792. +- name: Case 7, a four-person household under the published 2025 limit receives the tier two gas award. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + employment_income: 63_000 + ssi: 0 + person2: + age: 38 + immigration_status: CITIZEN + ssi: 0 + person3: + age: 10 + immigration_status: CITIZEN + ssi: 0 + person4: + age: 7 + immigration_status: CITIZEN + ssi: 0 + spm_units: + spm_unit: + members: [person1, person2, person3, person4] + heating_type: NATURAL_GAS + heating_expense: 1_000 + tanf: 0 + snap: + 2025-01: 0 + households: + household: + members: [person1, person2, person3, person4] + state_code: SD + county: LINCOLN_COUNTY_SD + output: + sd_liheap_countable_income: 63_000 + sd_liheap_income_limit: 63_432 + sd_liheap_income_threshold: 39_000 + sd_liheap_region: 1 + sd_liheap_eligible: true + sd_liheap_base_payment: 792 + sd_liheap: 792 + +# FY2022 and FY2023 matrices, region 1. 2023 size 1: limit 200% x 2022 FPG +# $13,590 = $27,180; Mid 125% x $13,590 / 4 = $4,246.875, rounded half up to +# $4,247 x 4 = $16,988. Tier 1: propane $1,666, fuel oil and kerosene $1,905, +# electricity $1,210; $20,000 of wages is tier 2: propane $1,499. Vendor fuels +# pay min(cell, $3,000); kerosene pays its cell in full. +- name: Case 8, 2023 direct payments use the FY2022 and FY2023 matrix amounts. + period: 2023 + absolute_error_margin: 0.01 + input: + state_code: [SD, SD, SD, SD, SD] + age: [40, 40, 40, 40, 40] + immigration_status: [CITIZEN, CITIZEN, CITIZEN, CITIZEN, CITIZEN] + heating_type: [PROPANE, FUEL_OIL, KEROSENE, ELECTRICITY, PROPANE] + heating_expense: [3_000, 3_000, 3_000, 3_000, 3_000] + employment_income: [0, 0, 0, 0, 20_000] + ssi: [0, 0, 0, 0, 0] + tanf: [0, 0, 0, 0, 0] + snap: + 2023-01: [0, 0, 0, 0, 0] + county_str: [CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, CUSTER_COUNTY_SD] + output: + sd_liheap_income_limit: [27_180, 27_180, 27_180, 27_180, 27_180] + sd_liheap_income_threshold: [16_988, 16_988, 16_988, 16_988, 16_988] + sd_liheap_eligible: [true, true, true, true, true] + sd_liheap: [1_666, 1_905, 1_905, 1_210, 1_499] + +# The FY2024 matrix prints the FY2026 cells, so 2024 region 1 tier 1 pays +# propane $2,499, fuel oil $2,858 and electricity $1,331, each below the $3,000 +# unpaid charges. 2024 size 1 limit $29,160. +- name: Case 9, 2024 direct payments already use the current matrix amounts. + period: 2024 + absolute_error_margin: 0.01 + input: + state_code: [SD, SD, SD] + age: [40, 40, 40] + immigration_status: [CITIZEN, CITIZEN, CITIZEN] + heating_type: [PROPANE, FUEL_OIL, ELECTRICITY] + heating_expense: [3_000, 3_000, 3_000] + employment_income: [0, 0, 0] + ssi: [0, 0, 0] + tanf: [0, 0, 0] + snap: + 2024-01: [0, 0, 0] + county_str: [CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, CUSTER_COUNTY_SD] + output: + sd_liheap_income_limit: [29_160, 29_160, 29_160] + sd_liheap_eligible: [true, true, true] + sd_liheap: [2_499, 2_858, 1_331] + +# Rule 67:15:01:18(12); manual page 28. 2026 Part B premium $202.90 x 12 = +# $2,434.80. $32,000 - $2,434.80 = $29,565.20 <= $31,300 limit and > $19,564 +# Mid: tier 2 gas $792. $21,000 - $2,434.80 = $18,565.20 <= Mid: tier 1 $880. +# Both are below the $2,000 unpaid charges. +- name: Case 10, Social Security net of the Medicare premium sets eligibility and the tier. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [SD, SD] + age: [70, 70] + immigration_status: [CITIZEN, CITIZEN] + heating_type: [NATURAL_GAS, NATURAL_GAS] + heating_expense: [2_000, 2_000] + social_security: [32_000, 21_000] + medicare_part_b_premium: [2_434.8, 2_434.8] + ssi: [0, 0] + tanf: [0, 0] + snap: + 2026-01: [0, 0] + county_str: [CUSTER_COUNTY_SD, CUSTER_COUNTY_SD] + output: + sd_liheap_countable_income: [29_565.2, 18_565.2] + sd_liheap_eligible: [true, true] + sd_liheap: [792, 880] + +# Manual page 25 counts retirement payments. $35,000 of military retirement +# pay exceeds the $31,300 size 1 limit, so no award. +- name: Case 11, military retirement pay above the limit makes the household ineligible. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [SD] + age: [50] + immigration_status: [CITIZEN] + heating_type: [NATURAL_GAS] + heating_expense: [2_000] + military_retirement_pay: [35_000] + ssi: [0] + tanf: [0] + snap: + 2026-01: [0] + county_str: [CUSTER_COUNTY_SD] + output: + sd_liheap_countable_income: [35_000] + sd_liheap_eligible: [false] + sd_liheap: [0] + +# Rule 67:15:01:18.01; manual page 25. The rental loss counts as zero: +# ($26,400 - $2,434.80 Part B premium) + $9,000 pension + $0 = $32,965.20, +# above the $31,300 limit. Letting the loss offset would give $28,965.20 and a +# $792 award. +- name: Case 12, a rental loss does not offset a retiree's other income. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: [SD] + age: [68] + immigration_status: [CITIZEN] + heating_type: [NATURAL_GAS] + heating_expense: [1_200] + social_security: [26_400] + medicare_part_b_premium: [2_434.8] + pension_income: [9_000] + rental_income: [-4_000] + ssi: [0] + tanf: [0] + snap: + 2026-01: [0] + county_str: [CUSTER_COUNTY_SD] + output: + sd_liheap_countable_income: [32_965.2] + sd_liheap_eligible: [false] + sd_liheap: [0] + +# Manual page 9: mixed households are eligible; only the two citizens count, so +# size 2. Page 17 prorates ineligible members' income through an unpublished +# calculator, so the undocumented adult's wages count in full: $18,000 + +# $9,000 = $27,000 <= size 2 limit $42,300 (200% x 2025 FPG $21,150). Mid: 125% +# x $21,150 / 4 = $6,609.375, rounded half up to $6,609 x 4 = $26,436, so tier +# 2: gas $792 = min($792, $1,200). Size 3 would give Mid $33,312, tier 1, $880. +- name: Case 13, a mixed-status household is sized by its qualified members and receives an award. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + immigration_status: CITIZEN + employment_income: 18_000 + ssi: 0 + person2: + age: 33 + immigration_status: UNDOCUMENTED + employment_income: 9_000 + ssi: 0 + person3: + age: 6 + immigration_status: CITIZEN + ssi: 0 + spm_units: + spm_unit: + members: [person1, person2, person3] + heating_type: NATURAL_GAS + heating_expense: 1_200 + tanf: 0 + snap: + 2026-01: 0 + households: + household: + members: [person1, person2, person3] + state_code: SD + county: CUSTER_COUNTY_SD + output: + sd_liheap_household_size: 2 + sd_liheap_countable_income: 27_000 + sd_liheap_income_limit: 42_300 + sd_liheap_income_threshold: 26_436 + sd_liheap_eligible: true + sd_liheap: 792 diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_eligible.py b/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_eligible.py index 9c2e4c1ccea..a701e88745e 100644 --- a/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_eligible.py +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_eligible.py @@ -12,23 +12,26 @@ class sd_liheap_eligible(Variable): "https://sdlegislature.gov/Rules/Administrative/67:15:01:09", # Sections 1.4a and 17.3: SNAP recipients and qualified noncitizens. "https://liheapch.acf.gov/docs/2026/state-plans/SD_Plan_2026.pdf", + # Physical pages 9 and 17: mixed households and ineligible members. + "https://liheapch.acf.gov/sites/default/files/webfiles/docs/SD_Policy-and-Procedures-Manual2018.pdf#page=9", ) def formula(spm_unit, period, parameters): - size = spm_unit("spm_unit_size", period) - qualified_members = add(spm_unit, period, ["is_citizen_or_legal_immigrant"]) - # The mixed-status income worksheet is unavailable. False for a mixed - # household marks unsupported coverage, not a legal denial of LIEAP. - supported_household = (size > 0) & (qualified_members == size) + # Manual physical page 9: "Mixed households are eligible for assistance." + # At least one citizen or eligible alien must be in the household. + has_qualified_member = spm_unit("sd_liheap_household_size", period) > 0 income = spm_unit("sd_liheap_countable_income", period) limit = spm_unit("sd_liheap_income_limit", period) # All application members must receive SNAP for categorical income # eligibility. January receipt and SNAP unit size approximate a stable # application-month caseload; a benefit for only some members is not - # enough. SNAP-specific member exclusions do not gate ordinary LIEAP. + # enough. A member ineligible for SNAP, such as an ineligible alien, + # sends the household to the income worksheet (physical page 17), so + # SNAP unit size is compared with every member. receives_snap = spm_unit("snap", period.first_month) > 0 all_receive_snap = ( - spm_unit("snap_unit_size", period.first_month) == size + spm_unit("snap_unit_size", period.first_month) + == spm_unit("spm_unit_size", period) ) & receives_snap income_eligible = (income <= limit) | all_receive_snap heating_type = spm_unit("heating_type", period) @@ -43,4 +46,4 @@ def formula(spm_unit, period, parameters): # with required heating/landlord verification and no duplicate tribal # award. Existing inputs cannot establish tribal service-area routing, # institutional residence, or completed administrative verification. - return supported_household & income_eligible & has_heat & responsible_for_heat + return has_qualified_member & income_eligible & has_heat & responsible_for_heat diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_household_size.py b/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_household_size.py new file mode 100644 index 00000000000..67d808fc3cc --- /dev/null +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_household_size.py @@ -0,0 +1,14 @@ +from policyengine_us.model_api import * + + +class sd_liheap_household_size(Variable): + value_type = int + entity = SPMUnit + definition_period = YEAR + label = "South Dakota LIEAP household size" + defined_for = StateCode.SD + # Physical page 9: "Only citizens and eligible aliens are counted as actual HH + # members"; page 18: ineligible aliens "are not included in the household + # count". + reference = "https://liheapch.acf.gov/sites/default/files/webfiles/docs/SD_Policy-and-Procedures-Manual2018.pdf#page=9" + adds = ["is_citizen_or_legal_immigrant"] diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.py b/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.py index e6771dbca36..adbdf411354 100644 --- a/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.py +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.py @@ -13,14 +13,14 @@ class sd_liheap_income_limit(Variable): reference = ( # Pages 1-5 publish three-month limits for sizes 1-15. "https://liheapch.acf.gov/docs/2024/benefits-matricies/SD_BenefitMatrix_2024.pdf#page=1", + "https://web.archive.org/web/20250114001211/https://dss.sd.gov/economicassistance/energy_weatherization_assistance.aspx", "https://dss.sd.gov/economicassistance/energy_weatherization_assistance.aspx", ) def formula(spm_unit, period, parameters): p = parameters(period).gov.states.sd.dss.liheap.income - size = spm_unit("spm_unit_size", period) - # This component supports wholly qualified households. The citizenship - # calculator's mixed-status size and income allocation remain unresolved. + # Only citizens and eligible aliens count toward household size. + size = spm_unit("sd_liheap_household_size", period) state_group = spm_unit.household("state_group_str", period) guideline = fpg( max_(size, 1), state_group, period, parameters, year_lag=p.fpg_year_lag @@ -37,4 +37,12 @@ def formula(spm_unit, period, parameters): # three-month maximum. Annualize the rounded quarter, not its unrounded # annual equivalent. Sizes 11+ carry the FY2024 rule forward; earlier # backfilled years are unverified and FY2022 used different rounding. - return where(size > 0, 4 * np.floor(annual_limit / 4), 0) + limit = 4 * np.floor(annual_limit / 4) + if p.limits.table.in_effect: + # The published 2024-2025 limits exceed the rule for sizes 4-7 and + # follow no consistent rounding, so the printed table applies to + # the sizes it lists. + table = p.limits.table.amount + in_table = size <= table.thresholds[-1] + limit = where(in_table, 4 * table.calc(size), limit) + return where(size > 0, limit, 0) 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 74a589d4905..8615be935c7 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 @@ -12,6 +12,8 @@ class sd_liheap_countable_income(Variable): "https://sdlegislature.gov/Rules/Administrative/67:15:01:18", "https://sdlegislature.gov/Rules/Administrative/67:15:01:18.01", "https://liheapch.acf.gov/docs/2026/state-plans/SD_Plan_2026.pdf#page=6", + # Physical pages 17 and 25-28: ineligible members and countable income. + "https://liheapch.acf.gov/sites/default/files/webfiles/docs/SD_Policy-and-Procedures-Manual2018.pdf#page=25", ) def formula(spm_unit, period, parameters): @@ -22,21 +24,40 @@ def formula(spm_unit, period, parameters): (age < p.secondary_school_age_limit) & person("is_in_secondary_school", period) ) + # Rule 67:15:01:18.01 counts self-employment losses as zero, and the + # manual (page 25) computes rental income on the self-employment sheet. + # Each source is floored at zero, so a loss in one source cannot offset + # another. Use existing net sources without another business deduction. earned = 0 for source in p.sources.earned: - # Rule 67:15:01:18.01 counts self-employment losses as zero. - # Use existing net sources without another business deduction. earned = earned + max_(person(source, period), 0) - unearned = add(spm_unit, period, p.sources.unearned, options=[ADD]) + 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) + # 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 + # a person who is not enrolled and excludes the share a Medicare Savings + # Program pays; no Part D premium input exists. + premium = person("medicare_part_b_premium", period) + social_security = max_(person("social_security", period) - premium, 0) + # Rule 67:15:01:18(1) exempts earnings, not minor unearned income. + income = spm_unit.sum(earned * ~excluded_earnings + unearned + social_security) + # The annual tanf aggregate includes sd_tanf and applies take-up; sd_tanf + # alone is the entitlement a nonrecipient could get. + tanf = spm_unit("tanf", period) + # Subsection (20) excludes income used to pay child support. child_support_paid = add(spm_unit, period, ["child_support_expense"]) - # Rule 67:15:01:18(1) exempts earnings, not minor unearned income; - # subsection (20) excludes income used to pay child support. State TANF - # is counted once at SPM level; SSI and TANF are explicitly annualized. - income = spm_unit.sum(earned * ~excluded_earnings) + unearned - # This component supports fully qualified households. The manual refers - # to an unavailable citizenship worksheet for allocating nonqualified - # members' income; that mixed-status calculation is not implemented. + # Manual physical page 17: for ineligible members, "a portion of their + # income and resources are considered when determining the household + # benefit" through the Citizenship Income Calculator (also page 26). The + # calculator's proration formula is not published and is unclear: its one + # example turns $3,000 eligible and $4,000 ineligible income into $6,200, + # matching neither full counting ($7,000) nor SNAP pro rata ($6,000). The + # model counts ineligible members' income in full, which overstates + # income for mixed-status households. # Annual inputs approximate stable income over the prior three months. - # Medicare deductions are deferred. Existing inputs cannot isolate all - # jury/foster receipts, program overpayment withholding, or expense refunds. - return max_(income - child_support_paid, 0) + # Existing inputs cannot isolate all jury/foster receipts, program + # overpayment withholding, or expense refunds. + return max_(income + tanf - child_support_paid, 0) diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_income_threshold.py b/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_income_threshold.py index 7c9cf002fe2..d2b5b45655e 100644 --- a/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_income_threshold.py +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_income_threshold.py @@ -17,9 +17,8 @@ class sd_liheap_income_threshold(Variable): def formula(spm_unit, period, parameters): p = parameters(period).gov.states.sd.dss.liheap - size = spm_unit("spm_unit_size", period) - # Use total size for wholly qualified households; the mixed-status - # citizenship calculator's allocation is not established by the sources. + # Only citizens and eligible aliens count toward household size. + size = spm_unit("sd_liheap_household_size", period) state_group = spm_unit.household("state_group_str", period) guideline = fpg( max_(size, 1), diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/sd_liheap.py b/policyengine_us/variables/gov/states/sd/dss/liheap/sd_liheap.py index 43d840e7f61..2e20b9dc3a6 100644 --- a/policyengine_us/variables/gov/states/sd/dss/liheap/sd_liheap.py +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/sd_liheap.py @@ -34,7 +34,7 @@ def formula(spm_unit, period, parameters): direct_payment = where(cash_payment, base_payment, min_(base_payment, expense)) # Heat-in-rent uses a steady tenant share for all seven eligible months; # changes in rent, already-paid months and prior awards are not tracked. - # Mixed-status households and unpriced fuels remain outside coverage. + # Unpriced fuels remain outside coverage. return where( spm_unit("heat_expense_included_in_rent", period), spm_unit("sd_liheap_heat_in_rent_payment", period), From 08ec35c38247f057fc9c8ad3fe93b61973db12ab Mon Sep 17 00:00:00 2001 From: Ziming Date: Wed, 7 Oct 2026 22:56:54 -0400 Subject: [PATCH 5/9] Fix issues from review: count workers' compensation Add workers_compensation to the unearned sources (EA-297 application page 5; ARSD 67:15:01:01(4)) and document the FY2026 plan-versus-DSS income-limit conflict for sizes 4-6. Co-Authored-By: Claude Opus 5.5 --- .../dss/liheap/income/sources/unearned.yaml | 8 +++++ .../income/sd_liheap_countable_income.yaml | 30 +++++++++++++++++++ .../eligibility/sd_liheap_income_limit.py | 7 +++++ 3 files changed, 45 insertions(+) diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/unearned.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/unearned.yaml index e111b20f91a..b0b863d148c 100644 --- a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/unearned.yaml +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/unearned.yaml @@ -12,6 +12,10 @@ values: - railroad_benefits - survivor_benefits - unemployment_compensation + # The application (page 5) lists "Worker's Compensation" as gross income, + # rule 67:15:01:01(4) counts unearned income before deductions, and rule + # 67:15:01:18 does not exclude it. + - workers_compensation - veterans_benefits - child_support_received - alimony_income @@ -35,7 +39,11 @@ metadata: reference: - title: South Dakota FY2026 LIHEAP State Plan, sections 1.5 and 1.9 and fixed-income processing, pages 5-7, 10 href: https://liheapch.acf.gov/docs/2026/state-plans/SD_Plan_2026.pdf#page=5 + - title: South Dakota Administrative Rule 67:15:01:01(4), Gross income + href: https://sdlegislature.gov/Rules/Administrative/67:15:01:01 - title: South Dakota Administrative Rule 67:15:01:18, Income exclusions href: https://sdlegislature.gov/Rules/Administrative/67:15:01:18 + - title: South Dakota Energy Assistance Application EA-297 (08/25), income section + href: https://dss.sd.gov/formsandpubs/docs/ENERGY/energyassistanceapplication.pdf#page=5 - title: "South Dakota LIEAP Policy and Procedures Manual (2018), countable income table, #page 25,27,28" href: https://liheapch.acf.gov/sites/default/files/webfiles/docs/SD_Policy-and-Procedures-Manual2018.pdf#page=25 diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/income/sd_liheap_countable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/income/sd_liheap_countable_income.yaml index f47def136ba..6658b67424c 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/income/sd_liheap_countable_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/income/sd_liheap_countable_income.yaml @@ -648,3 +648,33 @@ county: CUSTER_COUNTY_SD output: sd_liheap_countable_income: 20_000 + +# Application EA-297 page 5 lists Worker's Compensation as gross income. SDCL +# 62-4-3 pays 66 2/3% of weekly earnings: 2/3 x $900 = $600 a week, within the +# DLR $554-$1,108 range for July 2025-June 2026. 12 weeks of wages at $900 = +# $10,800, plus 26 weeks of benefits at $600 = $15,600, totals $26,400. +- name: Case 22, workers' compensation counts as unearned income alongside wages. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + employment_income: 10_800 + workers_compensation: 15_600 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 1_200 + tanf: 0 + households: + household: + members: [person1] + state_code: SD + county: CUSTER_COUNTY_SD + output: + sd_liheap_countable_income: 26_400 diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.py b/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.py index adbdf411354..040f7ff6335 100644 --- a/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.py +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.py @@ -37,6 +37,13 @@ def formula(spm_unit, period, parameters): # three-month maximum. Annualize the rounded quarter, not its unrounded # annual equivalent. Sizes 11+ carry the FY2024 rule forward; earlier # backfilled years are unverified and FY2022 used different rounding. + # NOTE: FY2026 sources conflict for sizes 4-6. The FY2026 plan, section + # 2.1, sets 60% of state median income for sizes 4-9: + # https://liheapch.acf.gov/docs/2026/state-plans/SD_Plan_2026.pdf#page=9 + # Applied to the $111,183 FY2026 state median income, that rate gives + # three-month limits of $16,677, $19,345 and $22,014 (our derivation). + # The DSS 2025-2026 published limits cited above are $16,075, $18,825 + # and $21,575 (200% of the 2025 poverty guideline). The model follows DSS. limit = 4 * np.floor(annual_limit / 4) if p.limits.table.in_effect: # The published 2024-2025 limits exceed the rule for sizes 4-7 and From 7b25bbbd10eebe8ea1a85806b4e6049f684e3bcf Mon Sep 17 00:00:00 2001 From: Ziming Date: Wed, 7 Oct 2026 23:24:42 -0400 Subject: [PATCH 6/9] Apply the reference page-citation convention One reference entry per source; single-page citations keep the page only in the href; multi-page titles end with a quoted #page list and the href opens the first cited page; variable references use '# PDF pages ...' comments above multi-page hrefs (CLAUDE.md, PolicyEngine/policyengine-us PR 10009). Reference lines only. Co-Authored-By: Claude Opus 5.5 --- .../gov/states/sd/dss/liheap/income/fpg_year_lag.yaml | 2 +- .../states/sd/dss/liheap/income/limits/fpg_lower_rate.yaml | 2 +- .../states/sd/dss/liheap/income/limits/fpg_upper_rate.yaml | 2 +- .../gov/states/sd/dss/liheap/income/limits/smi_rate.yaml | 2 +- .../states/sd/dss/liheap/income/limits/table/in_effect.yaml | 2 +- .../gov/states/sd/dss/liheap/income/sources/earned.yaml | 2 +- .../gov/states/sd/dss/liheap/income/sources/unearned.yaml | 4 ++-- .../states/sd/dss/liheap/payment/heat_in_rent/months.yaml | 2 +- .../states/sd/dss/liheap/payment/income_threshold_rate.yaml | 2 +- .../states/sd/dss/liheap/eligibility/sd_liheap_eligible.py | 2 +- .../sd/dss/liheap/eligibility/sd_liheap_household_size.py | 6 +++--- .../sd/dss/liheap/eligibility/sd_liheap_income_limit.py | 2 +- .../sd/dss/liheap/income/sd_liheap_countable_income.py | 4 ++-- .../states/sd/dss/liheap/payment/sd_liheap_base_payment.py | 2 +- .../sd/dss/liheap/payment/sd_liheap_heat_in_rent_payment.py | 2 +- .../sd/dss/liheap/payment/sd_liheap_income_threshold.py | 2 +- .../variables/gov/states/sd/dss/liheap/sd_liheap.py | 2 +- 17 files changed, 21 insertions(+), 21 deletions(-) diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/fpg_year_lag.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/fpg_year_lag.yaml index 3c8e65f9a3f..ed917a0a8de 100644 --- a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/fpg_year_lag.yaml +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/fpg_year_lag.yaml @@ -6,5 +6,5 @@ metadata: period: year label: South Dakota LIEAP poverty guideline year lag reference: - - title: South Dakota FY2024 LIHEAP Benefit Matrix, pages 1-5, income thresholds reconciled to 2023 guidelines + - title: "South Dakota FY2024 LIHEAP Benefit Matrix, income thresholds reconciled to 2023 guidelines #page 1-5" href: https://liheapch.acf.gov/docs/2024/benefits-matricies/SD_BenefitMatrix_2024.pdf#page=1 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/fpg_lower_rate.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/fpg_lower_rate.yaml index ff09ce13ffe..39a1f7362ed 100644 --- a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/fpg_lower_rate.yaml +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/fpg_lower_rate.yaml @@ -6,7 +6,7 @@ metadata: period: year label: South Dakota LIEAP income limit lower poverty rate reference: - - title: South Dakota FY2024 LIHEAP Benefit Matrix, pages 1-5, maximum income + - title: "South Dakota FY2024 LIHEAP Benefit Matrix, maximum income #page 1-5" href: https://liheapch.acf.gov/docs/2024/benefits-matricies/SD_BenefitMatrix_2024.pdf#page=1 - title: South Dakota DSS, FY2026 three-month income limits href: https://dss.sd.gov/economicassistance/energy_weatherization_assistance.aspx diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/fpg_upper_rate.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/fpg_upper_rate.yaml index 426ac0a1537..bdc6544c1f4 100644 --- a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/fpg_upper_rate.yaml +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/fpg_upper_rate.yaml @@ -6,7 +6,7 @@ metadata: period: year label: South Dakota LIEAP income limit upper poverty rate reference: - - title: South Dakota FY2024 LIHEAP Benefit Matrix, pages 1-5, maximum income + - title: "South Dakota FY2024 LIHEAP Benefit Matrix, maximum income #page 1-5" href: https://liheapch.acf.gov/docs/2024/benefits-matricies/SD_BenefitMatrix_2024.pdf#page=1 - title: South Dakota DSS, FY2026 three-month income limits href: https://dss.sd.gov/economicassistance/energy_weatherization_assistance.aspx diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/smi_rate.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/smi_rate.yaml index 3ac9ac69ecf..53ca9c77aec 100644 --- a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/smi_rate.yaml +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/smi_rate.yaml @@ -6,7 +6,7 @@ metadata: period: year label: South Dakota LIEAP income limit state median income rate reference: - - title: South Dakota FY2024 LIHEAP Benefit Matrix, pages 1-5, maximum income + - title: "South Dakota FY2024 LIHEAP Benefit Matrix, maximum income #page 1-5" href: https://liheapch.acf.gov/docs/2024/benefits-matricies/SD_BenefitMatrix_2024.pdf#page=1 - title: South Dakota DSS, FY2026 three-month income limits href: https://dss.sd.gov/economicassistance/energy_weatherization_assistance.aspx diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/table/in_effect.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/table/in_effect.yaml index 979209e7449..ab510e9431e 100644 --- a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/table/in_effect.yaml +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/table/in_effect.yaml @@ -12,7 +12,7 @@ metadata: period: year label: South Dakota LIEAP published income limit table in effect reference: - - title: South Dakota FY2024 LIHEAP Benefit Matrix, maximum income, pages 1-5 + - title: "South Dakota FY2024 LIHEAP Benefit Matrix, maximum income #page 1-5" href: https://liheapch.acf.gov/docs/2024/benefits-matricies/SD_BenefitMatrix_2024.pdf#page=1 - title: South Dakota DSS, 2024-2025 Heating Season Income Limits (archived January 14, 2025) href: https://web.archive.org/web/20250114001211/https://dss.sd.gov/economicassistance/energy_weatherization_assistance.aspx diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/earned.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/earned.yaml index ff066b813eb..35071e8070d 100644 --- a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/earned.yaml +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/earned.yaml @@ -15,5 +15,5 @@ metadata: reference: - title: South Dakota Administrative Rule 67:15:01:18.01, Self-employed households href: https://sdlegislature.gov/Rules/Administrative/67:15:01:18.01 - - title: South Dakota FY2026 LIHEAP State Plan, section 1.9, pages 6-7 + - title: "South Dakota FY2026 LIHEAP State Plan, section 1.9 #page 6-7" href: https://liheapch.acf.gov/docs/2026/state-plans/SD_Plan_2026.pdf#page=6 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/unearned.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/unearned.yaml index b0b863d148c..8b18f2a47d3 100644 --- a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/unearned.yaml +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/unearned.yaml @@ -37,7 +37,7 @@ metadata: period: year label: South Dakota LIEAP unearned income sources reference: - - title: South Dakota FY2026 LIHEAP State Plan, sections 1.5 and 1.9 and fixed-income processing, pages 5-7, 10 + - title: "South Dakota FY2026 LIHEAP State Plan, sections 1.5 and 1.9 and fixed-income processing #page 5-7,10" href: https://liheapch.acf.gov/docs/2026/state-plans/SD_Plan_2026.pdf#page=5 - title: South Dakota Administrative Rule 67:15:01:01(4), Gross income href: https://sdlegislature.gov/Rules/Administrative/67:15:01:01 @@ -45,5 +45,5 @@ metadata: href: https://sdlegislature.gov/Rules/Administrative/67:15:01:18 - title: South Dakota Energy Assistance Application EA-297 (08/25), income section href: https://dss.sd.gov/formsandpubs/docs/ENERGY/energyassistanceapplication.pdf#page=5 - - title: "South Dakota LIEAP Policy and Procedures Manual (2018), countable income table, #page 25,27,28" + - title: "South Dakota LIEAP Policy and Procedures Manual (2018), countable income table #page 25,27-28" href: https://liheapch.acf.gov/sites/default/files/webfiles/docs/SD_Policy-and-Procedures-Manual2018.pdf#page=25 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/heat_in_rent/months.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/heat_in_rent/months.yaml index 6ecbd7df353..1a8d773da07 100644 --- a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/heat_in_rent/months.yaml +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/heat_in_rent/months.yaml @@ -6,5 +6,5 @@ metadata: period: year label: South Dakota LIEAP heat-in-rent covered months reference: - - title: South Dakota LIEAP Policy and Procedures Manual, 2018, physical pages 31-32 (October through April) + - title: "South Dakota LIEAP Policy and Procedures Manual, 2018, October through April #page 31-32" href: https://liheapch.acf.gov/sites/default/files/webfiles/docs/SD_Policy-and-Procedures-Manual2018.pdf#page=31 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/income_threshold_rate.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/income_threshold_rate.yaml index 8f05ff0a275..93ed6375d5b 100644 --- a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/income_threshold_rate.yaml +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/income_threshold_rate.yaml @@ -6,7 +6,7 @@ metadata: period: year label: South Dakota LIEAP first-tier poverty rate reference: - - title: South Dakota FY2024 LIHEAP Benefit Matrix, pages 1-5, first-tier maximum income + - title: "South Dakota FY2024 LIHEAP Benefit Matrix, first-tier maximum income #page 1-5" href: https://liheapch.acf.gov/docs/2024/benefits-matricies/SD_BenefitMatrix_2024.pdf#page=1 - title: South Dakota FY2026 LIHEAP Benefit Matrix, Income Tier 1, 0-Mid href: https://liheapch.acf.gov/docs/2026/benefits-matricies/SD_BenefitMatrix_2026.pdf#page=1 diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_eligible.py b/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_eligible.py index a701e88745e..1c64ade4f1b 100644 --- a/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_eligible.py +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_eligible.py @@ -12,7 +12,7 @@ class sd_liheap_eligible(Variable): "https://sdlegislature.gov/Rules/Administrative/67:15:01:09", # Sections 1.4a and 17.3: SNAP recipients and qualified noncitizens. "https://liheapch.acf.gov/docs/2026/state-plans/SD_Plan_2026.pdf", - # Physical pages 9 and 17: mixed households and ineligible members. + # PDF pages 9, 17 "https://liheapch.acf.gov/sites/default/files/webfiles/docs/SD_Policy-and-Procedures-Manual2018.pdf#page=9", ) diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_household_size.py b/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_household_size.py index 67d808fc3cc..8e50bf29665 100644 --- a/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_household_size.py +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_household_size.py @@ -7,8 +7,8 @@ class sd_liheap_household_size(Variable): definition_period = YEAR label = "South Dakota LIEAP household size" defined_for = StateCode.SD - # Physical page 9: "Only citizens and eligible aliens are counted as actual HH - # members"; page 18: ineligible aliens "are not included in the household - # count". + # "Only citizens and eligible aliens are counted as actual HH members"; + # ineligible aliens "are not included in the household count". + # PDF pages 9, 18 reference = "https://liheapch.acf.gov/sites/default/files/webfiles/docs/SD_Policy-and-Procedures-Manual2018.pdf#page=9" adds = ["is_citizen_or_legal_immigrant"] diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.py b/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.py index 040f7ff6335..55d8cf4b2cf 100644 --- a/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.py +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.py @@ -11,7 +11,7 @@ class sd_liheap_income_limit(Variable): label = "South Dakota LIEAP annualized income limit" defined_for = StateCode.SD reference = ( - # Pages 1-5 publish three-month limits for sizes 1-15. + # PDF pages 1-5 "https://liheapch.acf.gov/docs/2024/benefits-matricies/SD_BenefitMatrix_2024.pdf#page=1", "https://web.archive.org/web/20250114001211/https://dss.sd.gov/economicassistance/energy_weatherization_assistance.aspx", "https://dss.sd.gov/economicassistance/energy_weatherization_assistance.aspx", 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 8615be935c7..1973c3e0043 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 @@ -12,8 +12,8 @@ class sd_liheap_countable_income(Variable): "https://sdlegislature.gov/Rules/Administrative/67:15:01:18", "https://sdlegislature.gov/Rules/Administrative/67:15:01:18.01", "https://liheapch.acf.gov/docs/2026/state-plans/SD_Plan_2026.pdf#page=6", - # Physical pages 17 and 25-28: ineligible members and countable income. - "https://liheapch.acf.gov/sites/default/files/webfiles/docs/SD_Policy-and-Procedures-Manual2018.pdf#page=25", + # PDF pages 17, 25-28 + "https://liheapch.acf.gov/sites/default/files/webfiles/docs/SD_Policy-and-Procedures-Manual2018.pdf#page=17", ) def formula(spm_unit, period, parameters): diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_base_payment.py b/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_base_payment.py index d52923dc705..020ca5c6376 100644 --- a/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_base_payment.py +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_base_payment.py @@ -10,7 +10,7 @@ class sd_liheap_base_payment(Variable): defined_for = StateCode.SD reference = ( "https://liheapch.acf.gov/docs/2026/benefits-matricies/SD_BenefitMatrix_2026.pdf#page=1", - # Pages 2 and 4: unpaid heating charges and fuel-oil/kerosene grouping. + # PDF pages 2, 4 "https://dss.sd.gov/formsandpubs/docs/ENERGY/energyassistanceapplication.pdf#page=2", ) diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_heat_in_rent_payment.py b/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_heat_in_rent_payment.py index 6dead3822ad..855456dd39d 100644 --- a/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_heat_in_rent_payment.py +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_heat_in_rent_payment.py @@ -11,7 +11,7 @@ class sd_liheap_heat_in_rent_payment(Variable): reference = ( "https://sdlegislature.gov/Rules/Administrative/67:15:01:41.02", "https://liheapch.acf.gov/docs/2026/benefits-matricies/SD_BenefitMatrix_2026.pdf", - # Physical pages 31-32: October-April payments and tenant rent share. + # PDF pages 31-32 "https://liheapch.acf.gov/sites/default/files/webfiles/docs/SD_Policy-and-Procedures-Manual2018.pdf#page=31", ) diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_income_threshold.py b/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_income_threshold.py index d2b5b45655e..16c9daf6d23 100644 --- a/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_income_threshold.py +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_income_threshold.py @@ -10,7 +10,7 @@ class sd_liheap_income_threshold(Variable): label = "South Dakota LIEAP annualized first-tier income threshold" defined_for = StateCode.SD reference = ( - # Pages 1-5 publish both income tiers for sizes 1-15. + # PDF pages 1-5 "https://liheapch.acf.gov/docs/2024/benefits-matricies/SD_BenefitMatrix_2024.pdf#page=1", "https://liheapch.acf.gov/docs/2026/benefits-matricies/SD_BenefitMatrix_2026.pdf#page=1", ) diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/sd_liheap.py b/policyengine_us/variables/gov/states/sd/dss/liheap/sd_liheap.py index 2e20b9dc3a6..920f54b3ee2 100644 --- a/policyengine_us/variables/gov/states/sd/dss/liheap/sd_liheap.py +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/sd_liheap.py @@ -11,7 +11,7 @@ class sd_liheap(Variable): reference = ( "https://liheapch.acf.gov/docs/2026/benefits-matricies/SD_BenefitMatrix_2026.pdf", "https://dss.sd.gov/formsandpubs/docs/ENERGY/energyassistanceapplication.pdf#page=2", - # Physical pages 30-32: vendor claims, rent payments, and cash fuels. + # PDF pages 30-32 "https://liheapch.acf.gov/sites/default/files/webfiles/docs/SD_Policy-and-Procedures-Manual2018.pdf#page=30", ) From 0a8763daba20deb0209b9e723c1b3e6194e72905 Mon Sep 17 00:00:00 2001 From: Ziming Date: Thu, 8 Oct 2026 00:41:31 -0400 Subject: [PATCH 7/9] Correct South Dakota LIHEAP verification coverage --- policyengine_us/programs.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/policyengine_us/programs.yaml b/policyengine_us/programs.yaml index 92efc71ff14..3c9e4e0af6c 100644 --- a/policyengine_us/programs.yaml +++ b/policyengine_us/programs.yaml @@ -890,8 +890,7 @@ programs: full_name: South Dakota Low Income Home Energy Assistance Program variable: sd_liheap parameter_prefix: gov.states.sd.dss.liheap - verified_years: "2024-2026" - notes: FY2024-FY2026 regular heating only. Crisis, cooling and weatherization assistance are out of scope. + notes: Regular heating only. Published FY2024 and FY2026 benefit amounts are tested, but no complete benefit year is verified. The citizenship income proration worksheet is unavailable, so mixed-status income remains overstated. FY2026 Mid thresholds are inferred from prior matrices. Crisis, cooling and weatherization assistance are out of scope. - state: TX status: partial name: Texas CEAP From ee24e71ea5ddeeccab8dbbefcfa4e2bfd0bc13c3 Mon Sep 17 00:00:00 2001 From: Ziming Date: Sun, 11 Oct 2026 23:45:53 -0400 Subject: [PATCH 8/9] Use the shared SMI helper for the South Dakota LIHEAP income limit Calls liheap_smi_limit from PR 10019 instead of repeating the HHS floor-then-floor calculation. Limits are unchanged. Co-Authored-By: Claude Opus 5.5 --- .../sd/dss/liheap/eligibility/sd_liheap_income_limit.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.py b/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.py index 55d8cf4b2cf..34498b9b130 100644 --- a/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.py +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_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 from policyengine_us.variables.gov.hhs.tax_unit_fpg import fpg @@ -26,9 +26,7 @@ def formula(spm_unit, period, parameters): max_(size, 1), state_group, period, parameters, year_lag=p.fpg_year_lag ) state = spm_unit.household("state_code_str", period) - four_person_smi = parameters(period).gov.hhs.smi.amount[state] - size_share = smi(size, state, period, parameters) / four_person_smi - smi_limit = np.floor(four_person_smi * p.limits.smi_rate) * size_share + smi_limit = liheap_smi_limit(size, state, p.limits.smi_rate, period, parameters) annual_limit = min_( guideline * p.limits.fpg_upper_rate, max_(guideline * p.limits.fpg_lower_rate, smi_limit), From cd74fd52d66d7ac033d7afcb39cf7f24cbb941be Mon Sep 17 00:00:00 2001 From: Ziming Date: Thu, 15 Oct 2026 16:11:38 -0400 Subject: [PATCH 9/9] Address review round 1 on South Dakota LIHEAP - S1: Unearned list reads the raw unemployment_compensation (main's #9558 guard fails) - S2: Regular cash help from family (financial_assistance) is excluded - S3: Twelve parameter descriptions are off the house template (five have trailing sentences) - S4: References: plan link without a page, incomplete page lists, rate parameters missing plan item 2.1 Also applies the in-scope suggestions. Tests not run locally; CI validates. Co-Authored-By: Claude Opus 5.5 --- changelog.d/sd-liheap.added.md | 2 +- .../sd/dss/liheap/income/fpg_year_lag.yaml | 4 +- .../liheap/income/limits/fpg_lower_rate.yaml | 13 +++- .../liheap/income/limits/fpg_upper_rate.yaml | 13 +++- .../sd/dss/liheap/income/limits/smi_rate.yaml | 14 +++- .../liheap/income/limits/table/in_effect.yaml | 4 +- .../sd/dss/liheap/income/sources/earned.yaml | 3 +- .../dss/liheap/income/sources/unearned.yaml | 14 +++- .../states/sd/dss/liheap/payment/direct.yaml | 2 + .../liheap/payment/heat_in_rent/months.yaml | 2 +- .../liheap/payment/income_threshold_rate.yaml | 6 +- .../dss/liheap/payment/regions/region_1.yaml | 2 +- .../dss/liheap/payment/regions/region_2.yaml | 2 +- .../dss/liheap/payment/regions/region_3.yaml | 2 +- .../dss/liheap/payment/regions/region_4.yaml | 2 +- policyengine_us/programs.yaml | 6 +- .../eligibility/sd_liheap_income_limit.yaml | 14 +++- .../income/sd_liheap_countable_income.yaml | 74 +++++++++++++++---- .../payment/sd_liheap_base_payment.yaml | 38 +++++----- .../sd_liheap_heat_in_rent_payment.yaml | 28 ++++--- .../payment/sd_liheap_income_threshold.yaml | 12 ++- .../dss/liheap/payment/sd_liheap_region.yaml | 7 +- .../gov/states/sd/dss/liheap/sd_liheap.yaml | 6 +- .../liheap/eligibility/sd_liheap_eligible.py | 3 +- .../eligibility/sd_liheap_income_limit.py | 7 +- .../income/sd_liheap_countable_income.py | 1 + .../liheap/payment/sd_liheap_base_payment.py | 12 ++- .../payment/sd_liheap_heat_in_rent_payment.py | 4 +- .../sd/dss/liheap/payment/sd_liheap_region.py | 11 +-- .../gov/states/sd/dss/liheap/sd_liheap.py | 17 +++-- 30 files changed, 222 insertions(+), 103 deletions(-) diff --git a/changelog.d/sd-liheap.added.md b/changelog.d/sd-liheap.added.md index bceeff695d6..a6e58f42e10 100644 --- a/changelog.d/sd-liheap.added.md +++ b/changelog.d/sd-liheap.added.md @@ -1 +1 @@ -Add South Dakota LIHEAP income, eligibility, county regions, and regular heating estimates for supported wholly qualified households, with seasonal expense assumptions and documented coverage limitations. +Add South Dakota LIEAP regular heating assistance with FY2024-FY2026 income limits, county heating regions, direct and heat-in-rent payments, and mixed-status households. diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/fpg_year_lag.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/fpg_year_lag.yaml index ed917a0a8de..5677a947522 100644 --- a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/fpg_year_lag.yaml +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/fpg_year_lag.yaml @@ -1,4 +1,6 @@ -description: South Dakota uses federal poverty guidelines from this many years before the modeled heating year for the Low Income Energy Assistance Program. The date records the FY2024 schedule in the annual model, not policy commencement; earlier backfilled years are unverified. +description: South Dakota uses poverty guidelines from this many years before the heating year under the Low Income Energy Assistance Program. +# The 2024-01-01 date records the FY2024 schedule in the annual model, not policy +# commencement; earlier backfilled years are unverified. values: 2024-01-01: 1 metadata: diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/fpg_lower_rate.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/fpg_lower_rate.yaml index 39a1f7362ed..9301c647b2e 100644 --- a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/fpg_lower_rate.yaml +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/fpg_lower_rate.yaml @@ -1,4 +1,7 @@ -description: South Dakota uses this federal poverty guideline rate as the lower bound of its income limit under the Low Income Energy Assistance Program. This numerical rule reproduces the FY2024 schedule and carries forward to FY2026; the date is an annual-model observation, not policy commencement. +description: South Dakota uses this poverty guideline rate as the lower bound of its income limit under the Low Income Energy Assistance Program. +# This numerical rule reproduces the FY2024 schedule and carries forward to FY2026; +# the date is an annual-model observation, not policy commencement. Plan section +# 2.1 prints 150% of the poverty guideline for size 10 in FY2025 and FY2026. values: 2024-01-01: 1.5 metadata: @@ -8,5 +11,9 @@ metadata: reference: - title: "South Dakota FY2024 LIHEAP Benefit Matrix, maximum income #page 1-5" href: https://liheapch.acf.gov/docs/2024/benefits-matricies/SD_BenefitMatrix_2024.pdf#page=1 - - title: South Dakota DSS, FY2026 three-month income limits - href: https://dss.sd.gov/economicassistance/energy_weatherization_assistance.aspx + - title: South Dakota DSS, 2025-2026 Heating Season Income Limits (archived January 1, 2026) + href: https://web.archive.org/web/20260101052007/https://dss.sd.gov/economicassistance/energy_weatherization_assistance.aspx + - title: South Dakota FY2026 LIHEAP State Plan, section 2.1 + href: https://liheapch.acf.gov/docs/2026/state-plans/SD_Plan_2026.pdf#page=9 + - title: South Dakota FY2025 LIHEAP State Plan, section 2.1 + href: https://liheapch.acf.gov/docs/2025/state-plans/SD_Plan_2025.pdf#page=8 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/fpg_upper_rate.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/fpg_upper_rate.yaml index bdc6544c1f4..7e14a787934 100644 --- a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/fpg_upper_rate.yaml +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/fpg_upper_rate.yaml @@ -1,4 +1,9 @@ -description: South Dakota caps the income limit at this federal poverty guideline rate under the Low Income Energy Assistance Program. This numerical rule reproduces the FY2024 schedule and carries forward to FY2026; the date is an annual-model observation, not policy commencement. +description: South Dakota limits household income to at most this share of the poverty guideline under the Low Income Energy Assistance Program. +# This numerical rule reproduces the FY2024 schedule and carries forward to FY2026; +# the date is an annual-model observation, not policy commencement. The FY2026 +# plan, section 2.1, prints 54.14%, 55.95% and 57.07% of state median income for +# sizes 1-3, which equal 200% of the 2025 guideline at the $111,183 median: +# $31,300 / ($111,183 x 52%), $42,300 / ($111,183 x 68%), $53,300 / ($111,183 x 84%). values: 2024-01-01: 2 metadata: @@ -8,5 +13,7 @@ metadata: reference: - title: "South Dakota FY2024 LIHEAP Benefit Matrix, maximum income #page 1-5" href: https://liheapch.acf.gov/docs/2024/benefits-matricies/SD_BenefitMatrix_2024.pdf#page=1 - - title: South Dakota DSS, FY2026 three-month income limits - href: https://dss.sd.gov/economicassistance/energy_weatherization_assistance.aspx + - title: South Dakota DSS, 2025-2026 Heating Season Income Limits (archived January 1, 2026) + href: https://web.archive.org/web/20260101052007/https://dss.sd.gov/economicassistance/energy_weatherization_assistance.aspx + - title: South Dakota FY2026 LIHEAP State Plan, section 2.1 + href: https://liheapch.acf.gov/docs/2026/state-plans/SD_Plan_2026.pdf#page=9 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/smi_rate.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/smi_rate.yaml index 53ca9c77aec..79a27593a4e 100644 --- a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/smi_rate.yaml +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/smi_rate.yaml @@ -1,4 +1,8 @@ -description: South Dakota applies this rate to four-person state median income before household-size adjustment, subject to poverty guideline bounds, under the Low Income Energy Assistance Program. Flooring before size adjustment and quarterly flooring reproduce the FY2024 and FY2026 tables; this is a reconciled numerical pattern. The date is an FY2024 annual-model observation, not policy commencement. +description: South Dakota limits household income to this share of state median income, within the poverty guideline bounds, under the Low Income Energy Assistance Program. +# The rate applies to the four-person state median income before the household +# size adjustment. Flooring before the size adjustment and flooring each quarter +# reproduce the FY2024 and FY2026 tables, a reconciled numerical pattern. The date +# is an FY2024 annual-model observation, not policy commencement. values: 2024-01-01: 0.6 metadata: @@ -8,5 +12,9 @@ metadata: reference: - title: "South Dakota FY2024 LIHEAP Benefit Matrix, maximum income #page 1-5" href: https://liheapch.acf.gov/docs/2024/benefits-matricies/SD_BenefitMatrix_2024.pdf#page=1 - - title: South Dakota DSS, FY2026 three-month income limits - href: https://dss.sd.gov/economicassistance/energy_weatherization_assistance.aspx + - title: South Dakota DSS, 2025-2026 Heating Season Income Limits (archived January 1, 2026) + href: https://web.archive.org/web/20260101052007/https://dss.sd.gov/economicassistance/energy_weatherization_assistance.aspx + - title: South Dakota FY2026 LIHEAP State Plan, section 2.1 + href: https://liheapch.acf.gov/docs/2026/state-plans/SD_Plan_2026.pdf#page=9 + - title: South Dakota FY2025 LIHEAP State Plan, section 2.1 + href: https://liheapch.acf.gov/docs/2025/state-plans/SD_Plan_2025.pdf#page=8 diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/table/in_effect.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/table/in_effect.yaml index ab510e9431e..38302ef40c4 100644 --- a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/table/in_effect.yaml +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/limits/table/in_effect.yaml @@ -16,5 +16,5 @@ metadata: href: https://liheapch.acf.gov/docs/2024/benefits-matricies/SD_BenefitMatrix_2024.pdf#page=1 - title: South Dakota DSS, 2024-2025 Heating Season Income Limits (archived January 14, 2025) href: https://web.archive.org/web/20250114001211/https://dss.sd.gov/economicassistance/energy_weatherization_assistance.aspx - - title: South Dakota DSS, 2025-2026 Heating Season Income Limits - href: https://dss.sd.gov/economicassistance/energy_weatherization_assistance.aspx + - title: South Dakota DSS, 2025-2026 Heating Season Income Limits (archived January 1, 2026) + href: https://web.archive.org/web/20260101052007/https://dss.sd.gov/economicassistance/energy_weatherization_assistance.aspx diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/earned.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/earned.yaml index 35071e8070d..c469e817885 100644 --- a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/earned.yaml +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/earned.yaml @@ -1,4 +1,4 @@ -description: South Dakota counts these earned income sources subject to its age and school exclusions under the Low Income Energy Assistance Program. +description: South Dakota uses these sources as earned income under the Low Income Energy Assistance Program. values: 2025-10-01: - employment_income @@ -6,6 +6,7 @@ values: - sstb_self_employment_income - farm_operations_income - partnership_s_corp_income +# Earnings of members under 18, or under 19 in high school, are excluded. # Use existing net business inputs and floor source losses without an additional # business-expense deduction or reconstructing gross receipts. metadata: diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/unearned.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/unearned.yaml index 8b18f2a47d3..70c259e11f7 100644 --- a/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/unearned.yaml +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/income/sources/unearned.yaml @@ -1,4 +1,4 @@ -description: South Dakota counts these unearned income sources for members of all ages under the Low Income Energy Assistance Program. +description: South Dakota uses these sources as unearned income for members of all ages under the Low Income Energy Assistance Program. values: 2025-10-01: # Social Security is counted in the formula, net of the Medicare Part B @@ -11,7 +11,7 @@ values: - military_retirement_pay - railroad_benefits - survivor_benefits - - unemployment_compensation + - total_unemployment_compensation # The application (page 5) lists "Worker's Compensation" as gross income, # rule 67:15:01:01(4) counts unearned income before deductions, and rule # 67:15:01:18 does not exclude it. @@ -30,8 +30,16 @@ values: # plan, section 1.9 (page 6), leaves "One-time lump-sum payments, such as # rebates/credits, winnings from lotteries" unchecked. The manual is followed. - gambling_winnings + # Regular cash help from family or friends counts. Rule 67:15:01:18(17) + # excludes only "Cash gifts for recurring occasions such as birthdays and + # Christmas"; the manual (page 25) counts "Cash Gifts (Monthly)" listed for + # all three months, and the application (page 5) lists "Money from Family + # or Friends" as gross income. The FY2026 plan, section 1.9 (page 6), leaves + # "Cash gifts" unchecked; the rule and manual are followed. An annual input + # is read as regular help, not a one-time or occasion gift. + - financial_assistance # Pension and Social Security subcomponents are not added again. Interest, -# dividends, capital gains and cash gifts are excluded. +# dividends and capital gains are excluded. metadata: unit: list period: year diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/direct.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/direct.yaml index 8a5ba863ac7..dbf2ebfb7b4 100644 --- a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/direct.yaml +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/direct.yaml @@ -5,6 +5,8 @@ description: South Dakota provides this amount as the direct-heating base paymen # minimum and $3,437 maximum (section 2.6, SD_Plan_2025.pdf#page=9) match the # FY2024 cells. Coal and wood and natural gas cells are unchanged from FY2022 # through FY2026. +# The FY2021 plan (section 2.6, SD_Plan_2021.pdf#page=9) prints a $334 minimum +# benefit, so the cells that backfill before October 2021 are unverified. coal_wood: 1: 1: diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/heat_in_rent/months.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/heat_in_rent/months.yaml index 1a8d773da07..f865d52e151 100644 --- a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/heat_in_rent/months.yaml +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/heat_in_rent/months.yaml @@ -1,4 +1,4 @@ -description: South Dakota covers this many months of tenant-paid heat in rent, from October through April, under the Low Income Energy Assistance Program. +description: South Dakota provides heat-in-rent payments for this many months, October through April, under the Low Income Energy Assistance Program. values: 2025-10-01: 7 metadata: diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/income_threshold_rate.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/income_threshold_rate.yaml index 93ed6375d5b..4fa23e8c7ac 100644 --- a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/income_threshold_rate.yaml +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/income_threshold_rate.yaml @@ -1,4 +1,8 @@ -description: South Dakota uses this federal poverty guideline rate for the first income tier under the Low Income Energy Assistance Program. Rounding its three-month equivalent half up reproduces all published FY2022 and FY2024 first-tier thresholds; this inferred rule carries forward to the FY2026 Mid tier. The date is the latest explicit FY2024 annual-model observation, not policy commencement. +description: South Dakota uses this poverty guideline rate as the first income tier limit under the Low Income Energy Assistance Program. +# Rounding its three-month equivalent half up reproduces all published FY2022 and +# FY2024 first-tier thresholds; this inferred rule carries forward to the FY2026 +# Mid tier. The date is the latest explicit FY2024 annual-model observation, not +# policy commencement. values: 2024-01-01: 1.25 metadata: diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_1.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_1.yaml index 9f12ffa210c..9ea0ac21351 100644 --- a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_1.yaml +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_1.yaml @@ -1,4 +1,4 @@ -description: South Dakota assigns these counties to heating region 1 under the Low Income Energy Assistance Program. +description: South Dakota uses these counties as heating region 1 under the Low Income Energy Assistance Program. values: 2025-10-01: - BENNETT_COUNTY_SD diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_2.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_2.yaml index c5db4ca8116..62e783c8f81 100644 --- a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_2.yaml +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_2.yaml @@ -1,4 +1,4 @@ -description: South Dakota assigns these counties to heating region 2 under the Low Income Energy Assistance Program. +description: South Dakota uses these counties as heating region 2 under the Low Income Energy Assistance Program. values: 2025-10-01: - AURORA_COUNTY_SD diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_3.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_3.yaml index 7fc71dd6667..540e9a1a639 100644 --- a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_3.yaml +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_3.yaml @@ -1,4 +1,4 @@ -description: South Dakota assigns these counties to heating region 3 under the Low Income Energy Assistance Program. +description: South Dakota uses these counties as heating region 3 under the Low Income Energy Assistance Program. values: 2025-10-01: - BEADLE_COUNTY_SD diff --git a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_4.yaml b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_4.yaml index d48740ac5e1..68908908c19 100644 --- a/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_4.yaml +++ b/policyengine_us/parameters/gov/states/sd/dss/liheap/payment/regions/region_4.yaml @@ -1,4 +1,4 @@ -description: South Dakota assigns these counties to heating region 4 under the Low Income Energy Assistance Program. +description: South Dakota uses these counties as heating region 4 under the Low Income Energy Assistance Program. values: 2025-10-01: - BROOKINGS_COUNTY_SD diff --git a/policyengine_us/programs.yaml b/policyengine_us/programs.yaml index 4093b4ba638..036d477b143 100644 --- a/policyengine_us/programs.yaml +++ b/policyengine_us/programs.yaml @@ -910,11 +910,11 @@ programs: notes: PY2025-PY2027 regular heating only, with the two county regions, five fuel columns and the roomer half payment. Crisis, cooling, Fuel Payment, equipment and OEAP assistance are out of scope. - state: SD status: partial - name: South Dakota LIHEAP - full_name: South Dakota Low Income Home Energy Assistance Program + name: South Dakota LIEAP + full_name: South Dakota Low Income Energy Assistance Program variable: sd_liheap parameter_prefix: gov.states.sd.dss.liheap - notes: Regular heating only. Published FY2024 and FY2026 benefit amounts are tested, but no complete benefit year is verified. The citizenship income proration worksheet is unavailable, so mixed-status income remains overstated. FY2026 Mid thresholds are inferred from prior matrices. Crisis, cooling and weatherization assistance 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. Crisis, cooling and weatherization are out of scope. - state: TX status: partial name: Texas CEAP diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.yaml index cac1d74bad5..d2225a81a01 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.yaml @@ -1,6 +1,8 @@ -# South Dakota independent ordinary-household components; no final award is asserted. +# South Dakota LIEAP annual income limits: four times the three-month maximum. -# Current DSS page explicitly labels2025-2026season and prints quarterly7825,10575,13325,16075,18825,21575,22514,23014,23514,24431. Annual model returns each multipliedby4; size9 tests the material inner SMI floor. +# DSS 2025-2026 Heating Season Income Limits print three-month maxima of $7,825, +# $10,575, $13,325, $16,075, $18,825, $21,575, $22,514, $23,014, $23,514 and +# $24,431; each is multiplied by 4. Sizes 7-9 use the 60% state median income term. - name: Case 1, all ten published FY2026 three month maxima are annualized once. period: 2026 absolute_error_margin: 0.01 @@ -12,7 +14,9 @@ output: sd_liheap_income_limit: [31300, 42300, 53300, 64300, 75300, 86300, 90056, 92056, 94056, 97724] -# FY2024matrix physicalpp1-5 publishes quarterly maxima for sizes1-15. Annualize each literal quarter amount by4. Size5 and7 distinguish inner SMI floor; odd large-sizequarters distinguish latest floor from older FY2022 rounding. This does not claim a common FY2022 maximum convention. +# FY2024 matrix pages 1-5 print three-month maxima for sizes 1-15; each is multiplied +# by 4. Size 1 is 200% of the 2023 guideline, sizes 2-8 the 60% state median income +# term and sizes 9-15 150% of the guideline. - name: Case 2, all fifteen FY2024 published quarterly maxima retain their rounded amounts. period: 2024 absolute_error_margin: 0.01 @@ -25,7 +29,9 @@ sd_liheap_income_limit: [29160, 39308, 48560, 57808, 67056, 76308, 78040, 79776, 83548, 91260, 98968, 106680, 114388, 122100, 129808] -# Latest reconciled rule carried forward, not printed FY2026cells:2025FPG for11-15 is70650,76150,81650,87150,92650. At150percent, floor quarter gives26493,28556,30618,32681,34743; annualizeby4. No size10 clamp. +# DSS prints no limit above size 10, so sizes 11-15 follow the rule: 150% of the +# 2025 guideline ($70,650, $76,150, $81,650, $87,150, $92,650) is $105,975, +# $114,225, $122,475, $130,725 and $138,975; each quarter is floored and x 4. - name: Case 3, derived FY2026 large household limits continue beyond the printed ten person table. period: 2026 absolute_error_margin: 0.01 diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/income/sd_liheap_countable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/income/sd_liheap_countable_income.yaml index 6658b67424c..b850917e365 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/income/sd_liheap_countable_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/income/sd_liheap_countable_income.yaml @@ -1,6 +1,7 @@ -# South Dakota supported income region and heat-in-rent components only; no final award is asserted. +# South Dakota LIEAP countable income: rules 67:15:01:18 and 67:15:01:18.01, FY2026 plan section 1.9 and the manual's income chart. -# Rule67:15:01:18.01 and approved net-input approximation:12000+600+600+1200+600=15000; no extra business expense deduction. +# Rule 67:15:01:18.01: existing net business inputs count with no further business +# deduction: $12,000 + $600 + $600 + $1,200 + $600 = $15,000. - name: Case 1, adult wages and existing net business sources count once. period: 2026 absolute_error_margin: 0.01 @@ -30,7 +31,8 @@ output: sd_liheap_countable_income: 15_000 -# Rule67:15:01:18.01 explicitly treats self-employment loss aszero:12000 wages+600 positivebusiness=12600. +# Rule 67:15:01:18.01 treats a self-employment loss as zero: $12,000 of wages + +# $600 of partnership income = $12,600. - name: Case 2, business losses are zero before adding other income. period: 2026 absolute_error_margin: 0.01 @@ -59,7 +61,9 @@ output: sd_liheap_countable_income: 12_600 -# Rule67:15:01:18(1) exempts earnings under18 or under19 while in highschool. UnearnedSSA1200 always counts; when earnings count add6000+1200 to parent12000+SSA1200. College does not satisfy secondary-school rule. +# Rule 67:15:01:18(1) excludes the earnings of a member under 18. The +# 17-year-old's $6,000 of wages and $1,200 of self-employment are excluded; their +# $1,200 of Social Security counts: $12,000 + $1,200 = $13,200. - name: Case 3, age 17 nonstudent follows the precise earned income exception. period: 2026 absolute_error_margin: 0.01 @@ -94,7 +98,8 @@ output: sd_liheap_countable_income: 13_200 -# Rule67:15:01:18(1) exempts earnings under18 or under19 while in highschool. UnearnedSSA1200 always counts; when earnings count add6000+1200 to parent12000+SSA1200. College does not satisfy secondary-school rule. +# Rule 67:15:01:18(1) also excludes the earnings of a high school student under +# 19: $12,000 + $1,200 of the student's Social Security = $13,200. - name: Case 4, age 18 secondary student follows the precise earned income exception. period: 2026 absolute_error_margin: 0.01 @@ -129,7 +134,8 @@ output: sd_liheap_countable_income: 13_200 -# Rule67:15:01:18(1) exempts earnings under18 or under19 while in highschool. UnearnedSSA1200 always counts; when earnings count add6000+1200 to parent12000+SSA1200. College does not satisfy secondary-school rule. +# A college student aged 18 is not in high school, so the earnings count: +# $12,000 + $6,000 + $1,200 + $1,200 = $20,400. - name: Case 5, age 18 college student follows the precise earned income exception. period: 2026 absolute_error_margin: 0.01 @@ -164,7 +170,8 @@ output: sd_liheap_countable_income: 20_400 -# Rule67:15:01:18(1) exempts earnings under18 or under19 while in highschool. UnearnedSSA1200 always counts; when earnings count add6000+1200 to parent12000+SSA1200. College does not satisfy secondary-school rule. +# A 19-year-old high school student is not under 19, so the earnings count: +# $12,000 + $6,000 + $1,200 + $1,200 = $20,400. - name: Case 6, age 19 secondary student follows the precise earned income exception. period: 2026 absolute_error_margin: 0.01 @@ -199,7 +206,9 @@ output: sd_liheap_countable_income: 20_400 -# Current plan pp5-7,10:1200+2400+600+3600+1200+600+1200+600+1200+600+300+300=13800. Railroad, farm rent and survivor inputs are disjoint. +# FY2026 plan pages 5-7 and 10: $1,200 + $2,400 + $600 + $3,600 + $1,200 + $600 + +# $1,200 + $600 + $1,200 + $600 + $300 + $300 = $13,800. The railroad, farm rent +# and survivor inputs are disjoint. - name: Case 7, disjoint rental survivor and ordinary unearned sources count at every age. period: 2026 absolute_error_margin: 0.01 @@ -236,8 +245,12 @@ output: sd_liheap_countable_income: 13_800 -# Current planpp6-7 excludes investmentinterest/dividends, gifts andcapital gains; rule67:15:01:18(14) confirms gains.12000 wages remain12000;SNAP also excluded. -- name: Case 8, interest dividends capital gains and gifts do not enter income. +# Rule 67:15:01:18 excludes capital gains (14) and only gifts for recurring +# occasions (17). Plan section 1.9 leaves interest and dividends unchecked and +# manual page 25 excludes bank interest and dividends but counts monthly cash +# gifts; application page 5 lists money from family or friends. SNAP is not +# income: $12,000 wages + $2,400 regular help = $14,400. +- name: Case 8, interest dividends and capital gains are excluded while regular help from family counts. period: 2026 absolute_error_margin: 0.01 input: @@ -265,9 +278,10 @@ state_code: SD county: AURORA_COUNTY_SD output: - sd_liheap_countable_income: 12_000 + sd_liheap_countable_income: 14_400 -# Current plan countsSSI/TANF for allages:6000 adultwages+1200 adultSSI+2400 childSSI+3600 familyTANF=13200. Monthly inputs supplied asannual totals are aggregated once. +# Plan section 1.9 counts SSI and TANF at every age: $6,000 adult wages + $1,200 +# adult SSI + $2,400 child SSI + $3,600 TANF = $13,200; annual totals sum once. - name: Case 9, adult and child SSI and the state TANF grant are annualized once. period: 2026 absolute_error_margin: 0.01 @@ -298,7 +312,7 @@ output: sd_liheap_countable_income: 13_200 -# Rule67:15:01:18(20):24000-3600 childsupportpaid=20400. +# Rule 67:15:01:18(20): $24,000 - $3,600 of child support paid = $20,400. - name: Case 10, income used for legally mandated child support is deducted. period: 2026 absolute_error_margin: 0.01 @@ -325,7 +339,7 @@ output: sd_liheap_countable_income: 20_400 -# Rule67:15:01:18(20) exclusion with householdfloor:max(1200-2400,0)=0. +# Rule 67:15:01:18(20) with a household floor: max($1,200 - $2,400, 0) = $0. - name: Case 11, child support deductions cannot produce negative countable income. period: 2026 absolute_error_margin: 0.01 @@ -678,3 +692,35 @@ county: CUSTER_COUNTY_SD output: sd_liheap_countable_income: 26_400 + +# Plan section 1.9 (page 6) checks "Unemployment insurance" and manual page 28 +# says "All UI payments received are countable." South Dakota pays 1/26 of the +# high-quarter wages a week (DLR benefit page, $28-$575 a week): $800 a week x +# 13 weeks = $10,400 / 26 = $400. 26 weeks of wages ($20,800) and 16 weeks of +# benefits ($6,400) = $27,200. +- name: Case 23, unemployment insurance counts alongside wages earned before a layoff. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + immigration_status: CITIZEN + is_in_secondary_school: false + ssi: 0 + employment_income: 20_800 + unemployment_compensation: 6_400 + spm_units: + spm_unit: + members: [person1] + heating_type: NATURAL_GAS + gas_expense: 1_200 + tanf: 0 + households: + household: + members: [person1] + state_code: SD + county: CUSTER_COUNTY_SD + output: + total_unemployment_compensation: 6_400 + sd_liheap_countable_income: 27_200 diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_base_payment.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_base_payment.yaml index 387be19dfca..1a6105ab784 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_base_payment.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_base_payment.yaml @@ -1,6 +1,7 @@ -# South Dakota independent ordinary-household components; no final award is asserted. +# South Dakota LIEAP direct-heating matrix cells by fuel row, region and income tier. -# FY2026matrix physicalp1:5fuelgroups x2income tiers x4regions=40literalcells. Size1 Mid19564annual;0is tier1 and20000is tier2 below current31300max. Lower payments are literal, not uniformly90percent. This is a table component; unpaid-balance and date-of-charge eligibility are not established. +# FY2026 matrix: 5 fuel rows x 2 income tiers x 4 regions = 40 cells. The size 1 Mid +# is $19,564, so $0 is tier 1 and $20,000 is tier 2, below the $31,300 maximum. - name: Case 1, all forty published direct heating cells retain their literal amounts. period: 2026 absolute_error_margin: 0.01 @@ -13,8 +14,6 @@ NATURAL_GAS, PROPANE, PROPANE, PROPANE, PROPANE, PROPANE, PROPANE, PROPANE, PROPANE] sd_liheap_region: [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4] - spm_unit_size: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1] sd_liheap_countable_income: [0, 0, 0, 0, 20000, 20000, 20000, 20000, 0, 0, 0, 0, 20000, 20000, 20000, 20000, 0, 0, 0, 0, 20000, 20000, 20000, 20000, 0, 0, 0, 0, 20000, 20000, 20000, 20000, 0, 0, 0, 0, 20000, 20000, 20000, 20000] @@ -23,7 +22,8 @@ 2858, 3018, 3236, 3437, 2573, 2715, 2910, 3093, 880, 938, 1007, 1058, 792, 844, 907, 953, 2499, 2658, 2834, 3066, 2249, 2394, 2552, 2762] -# FY2026matrixp1 expressly groupsCoal&Wood. Currentapplication EA297 physicalp4 lists one FuelOil/Kerosene category; use the oil schedule for both. This maps price only, not older kerosene payment timing. +# The FY2026 matrix prints one Coal & Wood row. Application EA-297 (page 4) lists one +# Fuel Oil/Kerosene category, so kerosene uses the Fuel Oil row. - name: Case 2, wood coal and the application oil kerosene category use matching source rows. period: 2026 absolute_error_margin: 0.01 @@ -31,12 +31,13 @@ state_code: [SD, SD, SD, SD, SD, SD, SD, SD] heating_type: [WOOD, COAL, FUEL_OIL, KEROSENE, WOOD, COAL, FUEL_OIL, KEROSENE] sd_liheap_region: [1, 1, 2, 2, 3, 3, 4, 4] - spm_unit_size: [1, 1, 1, 1, 1, 1, 1, 1] sd_liheap_countable_income: [0, 0, 0, 0, 20000, 20000, 20000, 20000] output: sd_liheap_base_payment: [742, 742, 3018, 3018, 730, 730, 3093, 3093] -# FY2024matrixp1: size1Midquarter4556=>18224annual (not18225); size2quarter6163=>24652 (not24650). Firsttier includes exactquarter threshold, centsabove move to secondtier. Region1naturalgas880/792. +# FY2024 matrix: the size 1 three-month Mid of $4,556 gives $18,224 a year (not the +# annual column's $18,225); size 2's $6,163 gives $24,652 (not $24,650). Income at +# the threshold is tier 1 and one cent above is tier 2: region 1 gas $880 and $792. - name: Case 3, the rounded quarterly threshold controls exact and cent boundaries rather than the annual column. period: 2024 absolute_error_margin: 0.01 @@ -44,14 +45,14 @@ state_code: [SD, SD, SD, SD, SD] heating_type: [NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS, NATURAL_GAS] sd_liheap_region: [1, 1, 1, 1, 1] - spm_unit_size: [1, 1, 2, 2, 2] sd_liheap_household_size: [1, 1, 2, 2, 2] sd_liheap_countable_income: [18224, 18224.01, 24650, 24652, 24652.01] output: sd_liheap_income_threshold: [18224, 18224, 24652, 24652, 24652] sd_liheap_base_payment: [880, 792, 880, 880, 792] -# FY2026reconciled Midquarter4891=>19564annual. Region3electricity tier1=1516 andtier2=1369; exactthreshold remains firsttier. No normal90percent replacement. +# FY2026 size 1 Mid: $4,891 x 4 = $19,564. Region 3 electricity pays $1,516 in +# tier 1 and $1,369 in tier 2; income at the threshold stays in tier 1. - name: Case 4, the current threshold includes its exact boundary and switches one cent above. period: 2026 absolute_error_margin: 0.01 @@ -59,23 +60,23 @@ state_code: [SD, SD, SD] heating_type: [ELECTRICITY, ELECTRICITY, ELECTRICITY] sd_liheap_region: [3, 3, 3] - spm_unit_size: [1, 1, 1] sd_liheap_countable_income: [19563.99, 19564, 19564.01] output: sd_liheap_base_payment: [1516, 1516, 1369] -# No supported directprice forunknownregion orSolar/Other/Unspecified. These zeros are component placeholders, not legal denials. NONE,stateandzero-size guards also return0. -- name: Case 5, unknown region unpriced fuels zero size and other states return component placeholders. +# No matrix price exists for an unknown region (0 or 5) or for NONE, SOLAR, +# OTHER or UNSPECIFIED fuel, and another state gets nothing. These zeros are +# coverage gaps, not denials. +- name: Case 5, unknown regions unpriced fuels and other states return component placeholders. period: 2026 absolute_error_margin: 0.01 input: - state_code: [SD, SD, SD, SD, SD, SD, SD, ND] - heating_type: [NATURAL_GAS, NATURAL_GAS, NONE, SOLAR, OTHER, UNSPECIFIED, NATURAL_GAS, NATURAL_GAS] - sd_liheap_region: [0, 5, 1, 1, 1, 1, 1, 1] - spm_unit_size: [1, 1, 1, 1, 1, 1, 0, 1] - sd_liheap_countable_income: [0, 0, 0, 0, 0, 0, 0, 0] + state_code: [SD, SD, SD, SD, SD, SD, ND] + heating_type: [NATURAL_GAS, NATURAL_GAS, NONE, SOLAR, OTHER, UNSPECIFIED, NATURAL_GAS] + sd_liheap_region: [0, 5, 1, 1, 1, 1, 1] + sd_liheap_countable_income: [0, 0, 0, 0, 0, 0, 0] output: - sd_liheap_base_payment: [0, 0, 0, 0, 0, 0, 0, 0] + sd_liheap_base_payment: [0, 0, 0, 0, 0, 0, 0] # Size 1 Mid $19,564 puts $20,000 in tier 2; size 2 Mid $26,436 puts the same # income in tier 1. Region 1 gas pays $792 and $880; size does not multiply the @@ -132,7 +133,6 @@ state_code: [SD, SD, SD, SD, SD, SD, SD, SD, SD] heating_type: [ELECTRICITY, ELECTRICITY, FUEL_OIL, FUEL_OIL, KEROSENE, PROPANE, PROPANE, NATURAL_GAS, COAL] sd_liheap_region: [2, 4, 3, 4, 2, 2, 3, 4, 3] - spm_unit_size: [1, 1, 1, 1, 1, 1, 1, 1, 1] sd_liheap_countable_income: [0, 20_000, 0, 20_000, 20_000, 0, 20_000, 0, 20_000] output: sd_liheap_income_threshold: [16_988, 16_988, 16_988, 16_988, 16_988, 16_988, 16_988, 16_988, 16_988] diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_heat_in_rent_payment.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_heat_in_rent_payment.yaml index d4f840093e2..f2aa000a752 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_heat_in_rent_payment.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_heat_in_rent_payment.yaml @@ -1,6 +1,8 @@ -# South Dakota supported income region and heat-in-rent components only; no final award is asserted. +# South Dakota LIEAP heat-in-rent payments: up to 30% of monthly tenant rent for +# October-April, capped by region. -# FY2026 matrix HIR cells800/800/850/850; annualrent6000 gives500monthly*30%*7=1050, so regionalmax binds. No separate heating bill required. +# FY2026 matrix heat-in-rent amounts are $800, $800, $850 and $850. Annual rent of +# $6,000 is $500 a month x 30% x 7 = $1,050, so the regional amount binds. - name: Case 1, all four regions use their source heat in rent maximum. period: 2026 absolute_error_margin: 0.01 @@ -15,7 +17,8 @@ sd_liheap_region: [1, 2, 3, 4] sd_liheap_heat_in_rent_payment: [800, 800, 850, 850] -# Rule67:15:01:41.02 andmanual31-32:1200annual/12*30%*7=210, belowregion2 maximum800. +# Rule 67:15:01:41.02 and manual pages 30-32: $1,200 / 12 x 30% x 7 = $210, below +# the region 2 amount of $800. - name: Case 2, one hundred dollars monthly tenant rent caps seven month assistance at two hundred ten. period: 2026 absolute_error_margin: 0.01 @@ -42,7 +45,8 @@ output: sd_liheap_heat_in_rent_payment: 210 -# Two adults each pay600annual rent; child0. Household1200/12*30%*7=210, not a separate regional benefit perperson. +# Two adults each pay $600 a year of rent and the child $0: $1,200 / 12 x 30% x 7 = +# $210 for the household, not a regional amount per person. - name: Case 3, person rent shares are summed once for the household cap. period: 2026 absolute_error_margin: 0.01 @@ -81,7 +85,9 @@ output: sd_liheap_heat_in_rent_payment: 210 -# Manualp32 allows positive subsidizedtenantshare.12000grossannualrent-9600reportedhousingassistance=2400tenantannualrent; equaladultshares1200each.2400/12*30%*7=420 belowregion3 maximum850. +# Manual page 32 allows a positive subsidized tenant share: $12,000 of gross rent - +# $9,600 of housing assistance = $2,400, $1,200 per adult. $2,400 / 12 x 30% x 7 = +# $420, below the region 3 amount of $850. - name: Case 4, subsidized rent uses the actual positive tenant share across multiple people. period: 2026 absolute_error_margin: 0.01 @@ -124,7 +130,8 @@ rent: [1_200, 1_200, 0] sd_liheap_heat_in_rent_payment: 420 -# Manualp32 zeroTenantPayment has no heat cost.12000grossrent-12000reportedassistance=0netrent, so cap0. +# Manual page 32: a $0 tenant payment has no heat cost. $12,000 of gross rent - +# $12,000 of housing assistance = $0. - name: Case 5, zero tenant share after housing assistance produces zero component. period: 2026 absolute_error_margin: 0.01 @@ -154,7 +161,8 @@ rent: 0 sd_liheap_heat_in_rent_payment: 0 -# This component is limited to heat included in rent. A directbill belongs to the unresolved separate directvendor award path. +# This component applies only to heat included in rent; a direct heating bill goes +# through the vendor payment. - name: Case 6, a direct heating bill without heat in rent does not receive this component. period: 2026 absolute_error_margin: 0.01 @@ -181,7 +189,8 @@ output: sd_liheap_heat_in_rent_payment: 0 -# Current matrixHIR isfuel-neutral; withverifiedHIR, unknownfuel uses1200/12*.3*7=210. ExplicitNONE means noheat and gives0. +# The heat-in-rent amount does not depend on fuel: UNSPECIFIED with heat in rent +# gets $1,200 / 12 x 30% x 7 = $210. NONE means no heat and gets $0. - name: Case 7, fuel neutral heat in rent permits unknown fuel but rejects explicit no heating. period: 2026 absolute_error_margin: 0.01 @@ -195,7 +204,8 @@ output: sd_liheap_heat_in_rent_payment: [210, 0] -# Explicitunknowngeography cannot choose a verifiedregionalamount; region0 is safely handled. OutsideSD component also0; neither is a final legal eligibility adjudication. +# An explicit UNKNOWN county has no region and gets $0, as does a household outside +# South Dakota. - name: Case 8, unknown region returns a safe unsupported estimate without inventing a county. period: 2026 absolute_error_margin: 0.01 diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_income_threshold.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_income_threshold.yaml index d53a0694821..56e6f59babc 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_income_threshold.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_income_threshold.yaml @@ -1,6 +1,9 @@ -# South Dakota independent ordinary-household components; no final award is asserted. +# South Dakota LIEAP first-tier (Mid) thresholds: four times the three-month Mid. -# FY2024matrix pp1-5 Midquarter values4556,6163,7769,9375,10981,12588,14194,15800,17406,19013,20619,22225,23831,25438,27044. Return4times these, not the sometimes-different printedannual column. E.g. size2quarter6163 means24652 rather than24650. +# FY2024 matrix pages 1-5 print three-month Mid values of 4,556, 6,163, 7,769, 9,375, +# 10,981, 12,588, 14,194, 15,800, 17,406, 19,013, 20,619, 22,225, 23,831, 25,438 and +# 27,044. The annual threshold is 4 x each, not the printed annual column: size 2 is +# 4 x $6,163 = $24,652, not $24,650. - name: Case 1, all fifteen FY2024 tier thresholds use the printed three month amounts. period: 2024 absolute_error_margin: 0.01 @@ -13,7 +16,10 @@ sd_liheap_income_threshold: [18224, 24652, 31076, 37500, 43924, 50352, 56776, 63200, 69624, 76052, 82476, 88900, 95324, 101752, 108176] -# FY2024reconciled125percent lag1FPG rule carried forward: FY2025FPG15650,21150,59650,65150,92650. Quarter raw4890.625,6609.375,18640.625,20359.375,28953.125 rounds halfup then times4. These are formula-derived FY2026tier thresholds, not printed current numericalMidcells. +# The FY2026 matrix names Mid without printing it, so the FY2024 rule carries +# forward: 125% of the 2025 guideline ($15,650, $21,150, $59,650, $65,150, $92,650) +# over 4 is $4,890.625, $6,609.375, $18,640.625, $20,359.375 and $28,953.125; +# rounding half up and x 4 gives $19,564, $26,436, $74,564, $81,436 and $115,812. - name: Case 2, FY2026 threshold carry forward preserves half up quarter rounding. period: 2026 absolute_error_margin: 0.01 diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_region.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_region.yaml index feb5e015975..e02b5fa75d1 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_region.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/payment/sd_liheap_region.yaml @@ -1,6 +1,7 @@ -# South Dakota supported income region and heat-in-rent components only; no final award is asserted. +# South Dakota LIEAP heating regions under rule 67:15:01:35. -# Officialrule67:15:01:35 table, independently extracted in sources/sd_county_regions.json and visually corroborated manualp52. HistoricShannon maps tocurrentOglalaLakota region1. Expected regions come fromoriginalsource, notimplementationparameters. +# Rule 67:15:01:35 assigns all 66 counties to four regions (manual page 52 maps the +# same regions); former Shannon County is Oglala Lakota County, region 1. - name: Case 1, all sixty six official county assignments match including renamed Shannon County. period: 2026 input: @@ -38,7 +39,7 @@ 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 1, 2, 3, 1, 2, 1, 2, 1, 2, 1, 2] -# ExplicitUNKNOWN/foreigncounty returnsunsupported marker0 safely; missingcounty is a separate sharedmodelAurora fallback and is deliberately not treated asverifiedlocation. +# An explicit UNKNOWN or out-of-state county returns region 0. - name: Case 2, explicit unknown and foreign counties never default to a verified region. period: 2026 input: diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/sd_liheap.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/sd_liheap.yaml index a7479e7b06d..c3b5668c275 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/sd_liheap.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/sd_liheap.yaml @@ -1,6 +1,6 @@ -# FY2026 matrix page 1; current application page 2; 2018 manual physical pages 30-32. -# Vendor cases use the approved eligible-unpaid-expense assumption. Cash-fuel -# payments carry forward the manual method; heat-in-rent assumes seven eligible months. +# FY2026 matrix, page 1; application EA-297, page 2; manual pages 30-32. +# heating_expense is read as unpaid charges in the claim window. Wood, coal and +# kerosene are paid in full (manual page 31); heat in rent assumes seven months. - name: Case 1, vendor fuels pay the smaller of the schedule and qualifying unpaid seasonal charges. period: 2026 absolute_error_margin: 0.001 diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_eligible.py b/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_eligible.py index 1c64ade4f1b..2929c1ed5cc 100644 --- a/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_eligible.py +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_eligible.py @@ -11,7 +11,8 @@ class sd_liheap_eligible(Variable): "https://sdlegislature.gov/Rules/Administrative/67:15:01:01", "https://sdlegislature.gov/Rules/Administrative/67:15:01:09", # Sections 1.4a and 17.3: SNAP recipients and qualified noncitizens. - "https://liheapch.acf.gov/docs/2026/state-plans/SD_Plan_2026.pdf", + # PDF pages 5, 34 + "https://liheapch.acf.gov/docs/2026/state-plans/SD_Plan_2026.pdf#page=5", # PDF pages 9, 17 "https://liheapch.acf.gov/sites/default/files/webfiles/docs/SD_Policy-and-Procedures-Manual2018.pdf#page=9", ) diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.py b/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.py index 34498b9b130..12d4c669ccf 100644 --- a/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.py +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/eligibility/sd_liheap_income_limit.py @@ -14,7 +14,7 @@ class sd_liheap_income_limit(Variable): # PDF pages 1-5 "https://liheapch.acf.gov/docs/2024/benefits-matricies/SD_BenefitMatrix_2024.pdf#page=1", "https://web.archive.org/web/20250114001211/https://dss.sd.gov/economicassistance/energy_weatherization_assistance.aspx", - "https://dss.sd.gov/economicassistance/energy_weatherization_assistance.aspx", + "https://web.archive.org/web/20260101052007/https://dss.sd.gov/economicassistance/energy_weatherization_assistance.aspx", ) def formula(spm_unit, period, parameters): @@ -42,6 +42,11 @@ def formula(spm_unit, period, parameters): # three-month limits of $16,677, $19,345 and $22,014 (our derivation). # The DSS 2025-2026 published limits cited above are $16,075, $18,825 # and $21,575 (200% of the 2025 poverty guideline). The model follows DSS. + # The FY2027 draft plan (status "Saved"), section 2.1, lists 60% of state + # median income for sizes 4-10; its size 1-3 rates (52.02%, 53.94% and + # 55.13%) match 200% of the 2026 guideline to within 0.01 point. Revisit + # sizes 4-6 when DSS posts its 2026-2027 limits: + # https://dss.sd.gov/docs/hearings/State_Plan.pdf#page=9 limit = 4 * np.floor(annual_limit / 4) if p.limits.table.in_effect: # The published 2024-2025 limits exceed the rule for sizes 4-7 and 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 1973c3e0043..9d931a93543 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 @@ -11,6 +11,7 @@ class sd_liheap_countable_income(Variable): reference = ( "https://sdlegislature.gov/Rules/Administrative/67:15:01:18", "https://sdlegislature.gov/Rules/Administrative/67:15:01:18.01", + # PDF pages 6-7 "https://liheapch.acf.gov/docs/2026/state-plans/SD_Plan_2026.pdf#page=6", # PDF pages 17, 25-28 "https://liheapch.acf.gov/sites/default/files/webfiles/docs/SD_Policy-and-Procedures-Manual2018.pdf#page=17", diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_base_payment.py b/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_base_payment.py index 020ca5c6376..0b2d7ce437a 100644 --- a/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_base_payment.py +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_base_payment.py @@ -41,10 +41,8 @@ def formula(spm_unit, period, parameters): ], default=0, ) - # This is an ungated table ceiling, separate from heat-in-rent payments. - # sd_liheap applies supported eligibility and the approved assumption - # that heating_expense reports eligible unpaid seasonal vendor charges. - # Unknown region or unpriced fuel returns an unsupported-component zero, - # not a finding that the household is legally ineligible. - size = spm_unit("spm_unit_size", period) - return where(supported_region & (size > 0), amount, 0) + # sd_liheap applies eligibility and caps vendor fuels at heating_expense, + # read as unpaid seasonal vendor charges. An unknown region or a fuel + # without a matrix row returns zero as a coverage gap, not a denial; the + # default for UNSPECIFIED fuel is tracked in #9754. + return where(supported_region, amount, 0) diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_heat_in_rent_payment.py b/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_heat_in_rent_payment.py index 855456dd39d..e6719f957ad 100644 --- a/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_heat_in_rent_payment.py +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_heat_in_rent_payment.py @@ -11,8 +11,8 @@ class sd_liheap_heat_in_rent_payment(Variable): reference = ( "https://sdlegislature.gov/Rules/Administrative/67:15:01:41.02", "https://liheapch.acf.gov/docs/2026/benefits-matricies/SD_BenefitMatrix_2026.pdf", - # PDF pages 31-32 - "https://liheapch.acf.gov/sites/default/files/webfiles/docs/SD_Policy-and-Procedures-Manual2018.pdf#page=31", + # PDF pages 30-32 + "https://liheapch.acf.gov/sites/default/files/webfiles/docs/SD_Policy-and-Procedures-Manual2018.pdf#page=30", ) def formula(spm_unit, period, parameters): diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_region.py b/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_region.py index fceaa903923..902c49d7c5e 100644 --- a/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_region.py +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/payment/sd_liheap_region.py @@ -8,15 +8,16 @@ class sd_liheap_region(Variable): label = "South Dakota LIEAP heating region" defined_for = StateCode.SD reference = "https://sdlegislature.gov/Rules/Administrative/67:15:01:35" + documentation = ( + "Regions 1 to 4 follow ARSD 67:15:01:35. Zero means the county is unknown " + "or outside South Dakota. A household with no county input takes the first " + "South Dakota county, Aurora, in region 2." + ) def formula(spm_unit, period, parameters): p = parameters(period).gov.states.sd.dss.liheap.payment.regions county = spm_unit.household("county_str", period) - # All 66 counties are assigned by rule; historic Shannon is represented - # by the existing Oglala Lakota county identifier. Unknown/foreign county - # is an unsupported location (0), never silently region 1. - # The shared county formula may infer the first county if no geography - # is supplied; an observed county is needed for a verified regional amount. + # Former Shannon County is the Oglala Lakota county identifier. return select( [ np.isin(county, p.region_1), diff --git a/policyengine_us/variables/gov/states/sd/dss/liheap/sd_liheap.py b/policyengine_us/variables/gov/states/sd/dss/liheap/sd_liheap.py index 920f54b3ee2..83057f65578 100644 --- a/policyengine_us/variables/gov/states/sd/dss/liheap/sd_liheap.py +++ b/policyengine_us/variables/gov/states/sd/dss/liheap/sd_liheap.py @@ -10,6 +10,7 @@ class sd_liheap(Variable): defined_for = "sd_liheap_eligible" reference = ( "https://liheapch.acf.gov/docs/2026/benefits-matricies/SD_BenefitMatrix_2026.pdf", + # PDF pages 2, 4 "https://dss.sd.gov/formsandpubs/docs/ENERGY/energyassistanceapplication.pdf#page=2", # PDF pages 30-32 "https://liheapch.acf.gov/sites/default/files/webfiles/docs/SD_Policy-and-Procedures-Manual2018.pdf#page=30", @@ -19,17 +20,21 @@ def formula(spm_unit, period, parameters): base_payment = spm_unit("sd_liheap_base_payment", period) heating_type = spm_unit("heating_type", period) fuels = heating_type.possible_values - # The historical manual pays these fuels' full award to the household. - # The current application groups kerosene with oil for the price only. + # Manual page 31 pays wood, coal and kerosene households "the entire + # benefit amount" by check. Rule 67:15:01:40 names only wood and coal, and + # application EA-297 (page 4) lists "Fuel Oil/Kerosene" with a supplier + # fill ticket and says "The payment for Heat in Rent, Wood, or Coal will be + # either direct deposited or put on an EPC card for you." Neither bars a + # check for kerosene, so the manual is followed. cash_payment = ( (heating_type == fuels.WOOD) | (heating_type == fuels.COAL) | (heating_type == fuels.KEROSENE) ) - # Approved input assumption: heating_expense represents unpaid eligible - # seasonal charges, not already-paid bills or an unrestricted annual - # total. Gas/electric meter dates: October 1-May 15; propane/oil fills: - # July 1-April 30. Existing inputs cannot establish those dates/balances. + # heating_expense is read as unpaid eligible seasonal charges, not bills + # already paid or an unrestricted annual total. Gas and electric meter + # reads: October 1-May 15; propane and oil fills: July 1-April 30. The + # inputs do not show charge dates or balances. expense = max_(spm_unit("heating_expense", period), 0) direct_payment = where(cash_payment, base_payment, min_(base_payment, expense)) # Heat-in-rent uses a steady tenant share for all seven eligible months;