Skip to content

Pay the DC LIHEAP Regular benefit to cooling-only households - #10742

Merged
hua7450 merged 6 commits into
mainfrom
dc-liheap-cooling
Nov 12, 2026
Merged

hua7450 merged 6 commits into
mainfrom
dc-liheap-cooling

Conversation

@hua7450

@hua7450 hua7450 commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator

Pays DC's regular LIHEAP benefit to households whose only energy cost is cooling.

Fixes #10741

What and why

DOEE says "LIHEAP assists income-eligible District households with heating and cooling energy costs", with a one-time regular benefit between $200 and $1,800 for FY2026 (DOEE LIHEAP page, April 2026 archive; the page now shows FY2027's $250 minimum). Its FAQ says "The energy program is designed to help with your heating/cooling costs" (Q3). The FY2026 plan runs heating and cooling from October 1, 2025 to September 30, 2026 (item 1.1). dc_liheap_eligible has no fuel condition. On main, though, dc_liheap_payment caps the matrix amount at heating_expense, which is $0 for a home with no heating, so a cooling-only household got $0.

dc_liheap_payment now uses:

  • cooling_only = (heating_type != UNSPECIFIED) & ~heat_in_rent & (heating_expense == 0) & has_cooling_expense
  • expense_cap = where(cooling_only, pre_subsidy_electricity_expense, heating_expenses)

Everything else is unchanged:

  • A cooling-only household is paid the Electric amount for its housing type, income level and size, capped at the electricity bill, whatever its heating row is. DOEE sets no fuel condition on the benefit, so a heated home with no heating bill that pays for cooling is paid like a home without heating, as the Oregon and Florida PRs do.
  • A household with a heating bill keeps its heating row and its heating-bill cap.
  • A $0 electricity bill pays $0.

The payment reference now also cites the FY26 benefit matrix. The registry entry gains a note.

Income limit. dc_liheap_eligible compared income with an unrounded 60% of state median income, which admitted a whole-dollar income $1 above DOEE's published limits at some sizes (FY2026: 80,870.09 against $80,869 for two people, 99,898.34 against $99,897 for three, 156,983.11 against $156,982 for six). It now uses HHS's 60% amounts, rounded down by household size (liheap_smi_limit), which match DOEE's limits. This also changes heating eligibility for households within that dollar.

Test plan

Tests were not run locally; CI runs them.

  • YAML tests with expected values from the FY2026 matrix (values dated 2025-10-01). Eleven new cases:
    • Electric row: single-family, one person, income level 3: min($440, $1,358.76 bill) = $440.
    • Bill cap: single-family, two people, income level 1: min($1,481, $1,358.76 bill) = $1,358.76.
    • No costs: a home with no heating and no cooling cost gets $0.
    • Heated home, no heating bill: wood heat the household gathers itself and a cooling cost: Electric $440.
    • Heated home with a heating bill: gas heat with a $300 gas bill and a cooling cost keeps the Gas amount capped at the gas bill, $300.
    • Gas heat, no gas bill: gas heat with no gas bill and a cooling cost gets the Electric $440, not its Gas row ($1,193) against the electricity bill.
    • Heat in the rent with a cooling cost: keeps the flat $200 heat-in-rent amount.
    • Income above the limit: $0.
    • Income limit: FY2026, two people, $80,869 is eligible and $80,870 is not (dc_liheap_eligible.yaml Case 6). Comments in Cases 1, 3 and 5 now show the rounded-down limits; no expected value changes.
    • UNSPECIFIED with a cooling cost: keeps the legacy adapter. A $300 person-level heating total caps the $440 Electric amount at $300, so the electricity bill sets no cap. On the deprecated direct GAS row with no gas bill it stays at $0, as on main.
  • Source-conflict register: not applicable.
  • make format (ruff)
  • Changelog fragment in changelog.d/

Test wages use DC's $17.95 minimum wage from July 1, 2025: 15 hours a week is $14,001 and 5 hours a week is $4,667. The electricity bill of $1,358.76 a year is EIA's 2024 DC average residential bill of $113.23 a month (Table 5A).

Not modeled

  • The bill cap has no DOEE text behind it. It is the cap main's heating formula already applies, and it can pay a cooling-only household less than the minimum benefit DOEE states.
  • The Electric row for a cooling-only household is a modeling choice. The matrix is indexed by "main heating fuel" and does not address homes whose only energy cost is cooling.
  • A small heating bill can pay less than none. Gas heat with a $100 gas bill and a cooling cost is paid $100, the Gas amount capped at the gas bill, while the same home with no gas bill is paid the Electric $440. This follows from main's heating-bill cap and from keeping a home with a heating bill on its heating row.
  • UNSPECIFIED still pays in DC through the legacy adapter, unchanged. Main's deprecated adapter pays the Electric row capped at the person-level heating total, or at pre_subsidy_electricity_expense when that total is $0. Removing it is tracked in issue 9754 and would change existing results.
  • Unchanged follow-ups: the missing SNAP/TANF/SSI categorical path, and the FY2027 matrix ($250 minimum) not yet on main.

axiom: TheAxiomFoundation/rulespec-us#2179 queued

🤖 Generated with Claude Code

hua7450 and others added 4 commits November 8, 2026 22:01
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace the placeholder bill with EIA's 2024 DC residential average
($113.23 a month), move the bill-cap case to income level 1, add an
UNSPECIFIED cooling case that keeps the legacy adapter, and cite the FY26
benefit matrix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (aae7f98) to head (d684d7d).
⚠️ Report is 183 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff            @@
##              main    #10742   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            1         3    +2     
  Lines           37        72   +35     
=========================================
+ Hits            37        72   +35     
Flag Coverage Δ
unittests 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@DTrim99

DTrim99 commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator

PR Review: Pay the DC LIHEAP Regular benefit to cooling-only households

Reviewed at head 563acb3ee9 (4 commits, 0 behind main).

Summary

The PR does what its sources support. DOEE runs one Regular benefit a fiscal year for heating and cooling costs, the cooling component is open all year in every plan from FY2024 to FY2027, and it uses the same matrix and the same minimum and maximum as heating. The new route pays a home with heating_type NONE and a cooling cost from the Electric column, capped at its electricity bill, and changes no other household's payment. The income-limit change reproduces DOEE's printed limits for FY2024, FY2026 and FY2027. All 56 FY2026 Electric and Gas cells on main match the DOEE matrix. CI is green at the head.

One item should be settled before merge: the cooling route is narrower than the PR's stated scope and narrower than the sibling PRs, with no source for the difference and no test on that edge. There are no critical findings.

Critical (must fix)

None.

Should address

  1. The cooling route excludes a heated home that has no heating bill but pays for cooling, and nothing sources or tests that limit. dc_liheap_payment.py lines 57-59 define cooling_only = (heating_type == NONE) & has_cooling_expense.

    • Rule. DOEE sets no fuel condition on the benefit: "LIHEAP assists income-eligible District households with heating and cooling energy costs" (DOEE page, 2026-04-17 capture), and "The energy program is designed to help with your heating/cooling costs" (FAQ Q3, page 1). The plan's cooling section has no extra eligibility requirement (item 3.2 is checked No, FY2026 plan, page 10). The FY2024 matrix, already cited by this variable, says of a gas-heated home that "The maximum benefit is $1800, which may be applied to the electric bill" (page 1).
    • Household. DC, period 2026, one adult aged 40 with $14,001 of wages, single-family home, heating_type: WOOD with no wood bill, has_cooling_expense: true, pre_subsidy_electricity_expense: 1_358.76. The row is Electric ($440), cooling_only is false because the type is not NONE, and the cap is the $0 heating cost. Actual: $0. The same household with heating_type: NONE gets $440. Expected: $440, since its only energy cost is cooling, which is the case the title and body say the PR covers. A gas-heated home with no gas bill and no heat-in-rent flag also gets $0.
    • Siblings. Oregon Pay Oregon's LIHEAP standard payment to cooling-only households #10736 uses heating_type != UNSPECIFIED, no heating cost, has_cooling_expense and an electricity bill above $0, and Florida Pay the Florida LIHEAP Home Energy benefit to cooling-only households #10740 pays "a heated home without a heating obligation". DC is the narrow one, and the PR body's "Not modeled" list does not mention it (the queued Axiom issue does, as interpretation 4).
    • Test gap. No case has a known heating fuel together with has_cooling_expense: true. Changing == NONE to != UNSPECIFIED passes every test, yet it would move a gas-heated home with a $300 gas bill and this electricity bill from $300 to $1,193 (SF, level 3, one person).
    • Fix. Either widen the route to the sibling pattern (a known type, heat not in rent, heating_expense == 0, has_cooling_expense), taking the Electric amount explicitly in that branch so a gas or oil home is not paid its heating row against the electricity bill; or keep NONE-only and say so in the body and the formula comment as a deliberate limit. Either way add two cases: a heated home with a positive heating bill and a cooling cost (unchanged result), and a heated home with a $0 heating bill and a cooling cost (whichever result is chosen).

Suggestions

  1. DOEE's FY2027 matrix is published and is not on main. The FY2027 matrix (dated 9/29/26) raises every floor cell and the heat-in-rent amount from $200 to $250 and most Electric cells, and the FY2027 plan prints "Minimum Benefit $250 Maximum Benefit $1,800" (item 3.6, page 12). The payment parameters stop at 2025-10-01, so period 2027 pays FY2026 amounts on the new route too: the first new test's household would get $440 where row SF 10000 1 now prints Electric $697. The body lists this as a follow-up and the PR claims no year beyond FY2026, so it does not block. FY2027 is the open application year, so the follow-up is worth filing now.

  2. The bill cap can pay less than the published minimum. The FY2026 plan prints "Minimum Benefit $200 Maximum Benefit $1,800" for cooling (item 3.6, page 11), and no matrix cell is below $200. A cooling-only home with a $150 electricity bill gets $150. The body discloses this and the cap is main's. Two points would sharpen the disclosure. First, the cap is not wholly unsourced: both plans check "Individual bill" among the benefit variables (items 2.5 and 3.5, pages 9 and 11 for FY2026, pages 10 and 12 for FY2027), and matrix Step 4 calls each amount a "maximum benefit". Neither says how the bill enters. Second, the whole electricity bill is the same cap main already applies to an electric-heated home, so the two routes are consistent.

  3. The Electric column is not enforced in the cooling branch. The comment at lines 54-56 says the home "takes the Electric row", but the row comes from dc_liheap_heating_type, which still accepts the deprecated direct input. With heating_type: NONE, a cooling cost and dc_liheap_heating_type: OIL, the head pays min($1,500, $1,358.76) = $1,358.76. Selecting p.electricity[...] when cooling_only is true would make the code match its comment and the registry note.

  4. References. https://doee.dc.gov/liheap (line 13) is rewritten each fiscal year and now prints "$250 and $1,800"; add the 2026-04-17 capture beside it for the FY2026 text. In dc_liheap_eligible.py the comment at lines 26-27 cites DOEE's published limits, which come from Step 1 of the FY2026 matrix; that file is not among the variable's references, and the live page no longer shows FY2026 limits.

  5. payment/electricity.yaml description still reads "for filers who heat primarily with electricity". The column now also pays homes with no heating that pay for cooling. Oregon Pay Oregon's LIHEAP standard payment to cooling-only households #10736 updated its description for the same change.

  6. Optional cases. The body says "A $0 electricity bill pays $0" with no case behind it (NONE, cooling cost, no electricity bill). A multifamily cooling case would also exercise the other half of the table (MF, level 2, one person: $529).

Verified correct

Sources, each opened and read:

  • DOEE page. The 2026-04-17 capture prints "LIHEAP assists income-eligible District households with heating and cooling energy costs. Eligible households may receive energy bill assistance between $200 and $1,800 as a one-time regular energy assistance benefit." The live page prints the same sentence with $250. Both match the body.
  • FAQ Q3, page 1. "The energy program is designed to help with your heating/cooling costs." Q1 adds "a one-time per fiscal year Regular benefit", so there is one benefit, not one for each season.
  • FY2026 plan. Item 1.1 (page 4): heating and cooling both checked, 10/01/2025 to 09/30/2026. Item 1.2: 13% of funds to cooling. Items 3.1 to 3.6 (pages 10-11): 60% of state median income, no additional requirements, the same matrix text as heating, minimum $200 and maximum $1,800. The reference's #page=4 and its "pages 4, 11" comment are right.
  • FY2027 plan. Item 1.1 (page 4): 10/01/2026 to 09/30/2027 for both. Items 2.6 and 3.6 (pages 10 and 12): minimum $250, maximum $1,800.
  • FY2024 and FY2025 plans also run cooling for the full fiscal year, so applying the route to every period with a matrix is consistent.
  • FY2026 matrix. All 56 rows compared with electricity.yaml and gas.yaml at 2026-01-01: no mismatch, smallest cell $200, largest $1,800. Income levels 8 to 10 repeat the last line ($200).

Answers to the batch questions:

  • Regular benefit for cooling. Yes, in each year the parameters cover. The sources agree with each other, so no source-conflict record is needed.
  • Column and key. dc_liheap_heating_type maps NONE to ELECTRICITY, a valid key; the matrix has Gas, Oil, Electric and HIR columns and no cooling column. The matrix is indexed by "main heating fuel type" and is silent on a home without one; Electric is the only fuel that cools and the lowest column, and the body states the choice.
  • Terms. UNSPECIFIED keeps the legacy adapter (pinned by the last new case). has_cooling_expense: false pays $0 (pinned), which fits the federal definition of home energy as "a source of heating or cooling in residential dwellings" (42 U.S.C. 8622(6)). Heat in rent still pays the flat amount whatever the cooling flag. The plan has no separate policy for subsidized housing.
  • No double payment. One variable, one amount. Where cooling_only is false the cap is the same expression as on main, so every household that was paid before is paid the same. Only NONE homes with a cooling cost change, from $0.
  • Mutation check. Removing the NONE term fails the UNSPECIFIED case (440 instead of 300). Removing has_cooling_expense fails the no-cooling case (440 instead of 0). Reverting the cap fails the first case (0 instead of 440). The bill-cap case pins the electricity bill as the cap. Case 6 fails on main's unrounded limit. The one gap is in Should 1.
  • Non-residents. The new terms read ungated inputs and add no parameter lookup. liheap_smi_limit indexes gov.hhs.smi.amount by state exactly as hhs_smi did. The pre-existing lookup of income level 0 for units outside DC is unchanged here; Add a national LIHEAP total, an Illinois regular heating payment and LIHEAP cooling registry notes #10730 floors it.
  • Periods. All variables read are annual; no add call or options argument is involved.
  • Registry and changelog. The programs.yaml note matches the code for NONE homes and claims no verified years. changelog.d/dc-liheap-cooling.fixed.md describes both changes.
  • National total. Add a national LIHEAP total, an Illinois regular heating payment and LIHEAP cooling registry notes #10730 already lists dc_liheap_payment in gov.hhs.liheap.regular_payments; nothing to add here.

Hand-checked expectations:

  1. Cooling only, SF, one person, $14,001: level 3 (10,000 < 14,001 <= 15,000); row SF 10000 1 Electric $440; min(440, 1,358.76) = 440.
  2. Bill cap, SF, two people, $4,667: level 1; row SF 0 2 Electric $1,481; min(1,481, 1,358.76) = 1,358.76.
  3. NONE without cooling: cap is the $0 heating cost; min(440, 0) = 0.
  4. Wages $83,200, one person: limit floor(118,926 x 0.52) = 61,841; 110% floor 1.1 x 15,650 = 17,215; ineligible, 0.
  5. UNSPECIFIED with a $300 person-level total: Electric $440; min(440, 300) = 300.
  6. Eligibility Case 6, FY2026, two people: floor(198,211 x 0.6) = 118,926; floor(118,926 x 0.68 = 80,869.68) = 80,869; $80,869 true, $80,870 false. DOEE's matrix prints $80,869.
  7. Case 1, FY2023, four people: floor(152,043 x 0.6 = 91,225.8) = 91,225; $91,000 true.
  8. Case 3, FY2024, 36 people: 95,797 x 2.22 = 212,669.34, so 212,669; floor 1.1 x 194,480 = 213,928; results [true, true, false, false, false] (35 people: 95,797 x 2.19 = 209,795.43, so 209,795, above the 208,274 floor).
  9. Case 5, FY2023, 39 people: 91,225 x 2.31 = 210,729.75, so 210,729; floor 212,245; [true, false].
  10. Rounding against DOEE's printed limits: FY2024 FAQ Q7 sizes 1 to 8 (49,814; 65,141; 80,469; 95,797; 111,124; 126,452; 129,325; 132,199), FY2026 matrix sizes 1 to 6 and the archived page's sizes 7 to 10 (160,550; 164,117; 167,685; 171,253), and FY2027 matrix sizes 1 to 6 (63,024 to 159,984, from 202,000 x 0.6 = 121,200) all equal the head's limit. HHS's FY2026 table prints the same DC row on page 1 of the newly cited attachment. FY2025 limits were not checked against a DOEE publication.
  11. Test inputs: 15 x 52 x 17.95 = 14,001; 5 x 52 x 17.95 = 4,667; EIA Table 5A prints $113.23 a month for DC in 2024, and 113.23 x 12 = 1,358.76.

Axiom line assessment

axiom: TheAxiomFoundation/rulespec-us#2179 queued is valid. The issue is open, labelled pe-parity, names head 563acb3ee9, and carries module paths, corpus citations, verbatim source text, required outputs and 11 companion cases derived from the matrix. It already covers the income-limit rounding (interpretation 7). Two sentences are out of date and can be fixed with the next edit: it says the tests have not yet passed in CI at this head, and that the PR body quotes the live page sentence (the body quotes the April capture). If Should 1 changes the route, update interpretation 4 and the required-output definition to match.

CI status

All 39 checks pass on the run for head 563acb3ee9 (started 2026-11-12 04:19 UTC). The three "skipping" entries belong to a superseded workflow run. The PR's own files ran: dc_liheap_eligible.yaml 6 passed, dc_liheap_income_level.yaml 3 passed, dc_liheap_payment.yaml 28 passed. Because the head contains current main, this run also covers the main-side guards (source-conflict ratchet, unemployment-compensation and military-income readers). No local run was needed.

Branch status

The head is 0 commits behind main and merges cleanly.

Verdict: REQUEST_CHANGES

@DTrim99 DTrim99 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting one change. The cooling route is heating_type == NONE only, so a heated home with no heating bill that pays for cooling still gets $0, which is narrower than the title, the DOEE text and the Oregon and Florida siblings, and no test sits on that edge (changing == NONE to != UNSPECIFIED passes every case). Please either widen the route or state the limit in the body and the formula comment, and add the two cases either way (Should Address 1 in the report above).

Everything else holds: one Regular benefit for heating and cooling, the Electric column and the bill cap match main's treatment, all 56 FY2026 Electric and Gas cells match DOEE's matrix, and the income-limit rounding reproduces DOEE's printed limits. The six suggestions are optional. CI is green at 563acb3ee9.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hua7450

hua7450 commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks, David. Widened in fe22688810 to the sibling pattern: cooling_only is now a known heating type, no heat in the rent, no heating cost and a cooling cost, and that branch takes the Electric amount explicitly, capped at the electricity bill. Your two cases are added: wood heat with no wood bill and a cooling cost pays $440, and gas heat with a $300 gas bill and a cooling cost keeps $300. The body and TheAxiomFoundation/rulespec-us#2179 are updated. Tests were not run locally.

@hua7450
hua7450 requested a review from DTrim99 November 12, 2026 17:10
@DTrim99

DTrim99 commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator

PR Review (round 2): Pay the DC LIHEAP Regular benefit to cooling-only households

Reviewed at head fe22688810 (one fix commit since 563acb3ee9, no merge of main in between).

Summary

The fix commit does what round 1 asked. The cooling route now takes a household with a known heating type, no heat in the rent, no heating cost and a cooling cost, and pays it the Electric amount explicitly, capped at its electricity bill. Both requested cases are in and are right by hand. No household that main pays gets a different amount, the UNSPECIFIED adapter is unchanged, and heat-in-rent households keep the flat amount. CI is green at the head and the PR's own test files ran there.

One item is open. The fix adds three terms that no committed case exercises: the explicit Electric amount, the heat-in-rent guard and the known-type guard. I removed each in turn on the head tree and all 30 cases in dc_liheap_payment.yaml still passed. Three short cases close it; I ran them at the head and they pass as written. There are no critical findings.

Prior findings

Finding Status Evidence
Should 1: the cooling route excluded a heated home with no heating bill that pays for cooling, with no source or test for that limit Fixed. Two terms the fix added are not yet pinned (new Should 1) dc_liheap_payment.py lines 59-64: ~unspecified & ~heat_in_rent & (heating_expenses == 0) & has_cooling_expense; line 88 takes electricity_amount in that branch. The wood case (test lines 427-450) pins the widening: with the route put back to NONE only it returns 0 instead of 440 (ran). The gas case (lines 452-475) pins the no-heating-cost term: without it the case returns 440 instead of 300 (ran). The body and the queued Axiom issue now describe the widened route.
Suggestion 1: FY2027 matrix not on main Not taken (optional) Still listed in the body as a follow-up. I found no issue filed for it.
Suggestion 2: sharper disclosure of the bill cap Not taken (optional) The body's "Not modeled" line is unchanged.
Suggestion 3: Electric column not enforced in the cooling branch Taken Lines 72 and 88. A home with heating_type: NONE, a cooling cost and the deprecated dc_liheap_heating_type: OIL now gets $440, not $1,358.76 (ran). No committed case pins it; case (a) under new Should 1 would.
Suggestion 4: archive capture beside the live DOEE link; source for the limits comment Not taken (optional) References unchanged in the fix commit.
Suggestion 5: payment/electricity.yaml description Not taken (optional) No parameter file changes.
Suggestion 6: optional cases ($0 electricity bill, multifamily) Not taken (optional) I ran the $0-bill household at the head (NONE, cooling cost, no electricity bill): $0, so the body's sentence is right.

New findings

Critical

None.

Should address

  1. Three terms added by the fix have no test. Each row below is one household, period 2026, DC, one adult aged 40 with $14,001 of wages, single-family, has_cooling_expense: true, pre_subsidy_electricity_expense: 1_358.76. Row SF 10000 1 of the FY2026 matrix prints Gas $1,193, Electric $440 and HIR $200. Every figure in the last two columns comes from a run on the head tree.

    Term (dc_liheap_payment.py) Extra inputs Head With the term removed Committed cases with the term removed
    (a) Explicit Electric amount, line 88 heating_type: NATURAL_GAS, no gas bill $440 $1,193 (the Gas row against the electricity bill) all 30 pass
    (b) Heat-in-rent guard, line 61 heating_type: NATURAL_GAS, heat_expense_included_in_rent: true $200 $440 with no cap; two people with $4,667 of wages get $1,481 instead of $200 all 30 pass
    (c) Known-type guard, line 60 heating_type not set, dc_liheap_heating_type: GAS, no gas bill $0, as on main $440 all 30 pass
    • (a) Both cooling-only homes in the tests (NONE and wood) already sit on the Electric row through dc_liheap_heating_type, so heating_amount equals electricity_amount for them and line 88 is never observed. This is the part of the round-1 fix that keeps a gas or oil home from being paid its heating row against the electricity bill, and the part that closes Suggestion 3.
    • (b) No case combines heat in the rent with a cooling cost. At round 1 the amount came from the row, so no guard was needed. Now the guard is the only thing that keeps a heat-in-rent tenant with a cooling cost from the Electric amount, and that path has no bill cap.
    • (c) The case "Cooling cost with an unspecified fuel keeps the legacy adapter" pinned the NONE term at round 1. It no longer pins anything: its $300 person-level total is a positive heating cost, so line 62 keeps it off the route with or without line 60. The guard matters only for an unspecified type on a non-Electric row with no bill for that fuel, which needs the deprecated dc_liheap_heating_type input, so this is the least important of the three. It is still the term behind the body's statement that UNSPECIFIED is unchanged. The comment above that case (test lines 501-504) also still says "The cooling route keys on the canonical NONE type only", which is no longer true.
    • Fix. Add the three households above as cases, expecting $440, $200 and $0, and reword the comment at lines 501-504. All three pass at the head as written.

Suggestions

  1. Wording left over from the narrower route. changelog.d/dc-liheap-cooling.fixed.md still says the benefit "now pays households without heating that pay their own electricity bill for cooling". It now also pays a heated home with no heating cost; "households with no heating cost" would cover both. The documentation of dc_liheap_heating_type says a direct input "changes only the matrix row"; at this head it no longer moves the row for a cooling-only household.

  2. A small heating bill now pays less than none. Gas heat with a $100 gas bill and a cooling cost pays $100 (min of $1,193 and the bill); the same home with no gas bill pays $440. This follows from main's heating-bill cap and from keeping a home with a heating bill on its heating row, which round 1 asked for, so it is not a finding. One line under "Not modeled" would disclose it.

Verified

What rests on what: "ran" means a local run on the head tree (the 30 committed cases plus ten probe households, then seven one-term variants of the formula); "by hand" means traced from the code and the matrix values on main; CI results are from the job logs.

  • "No heating cost". Line 62 tests heating_expenses == 0. For a known type that local is heating_expense, the bill of the primary fuel: pre_subsidy_electricity_expense for ELECTRICITY and SOLAR, gas_expense for NATURAL_GAS, fuel_oil_expense for FUEL_OIL and KEROSENE, bottled_gas_expense for PROPANE, wood_expense for WOOD, coal_expense for COAL, other_heating_fuel_expense for OTHER, and 0 for NONE. It does not read has_heating_expense, as the Oregon and Florida PRs do. In DC the bill is what the heating cap already reads, so the two routes split exactly: the cooling route opens only where main's heating cap is $0. For an electric or solar home "no heating cost" means no electricity bill, and both routes pay $0.
  • Unchanged households (by hand). Where cooling_only is false, lines 65-89 reduce to main's expressions. cooling_only needs a $0 heating cost and no heat in the rent, and main pays such a household min(amount, 0) = $0. So every household whose result differs from main was paid $0 on main. The 23 cases that predate the PR pass unchanged in CI.
  • Changes since round 1. Two groups only: a known fuel with a $0 heating cost, a cooling cost and no heat in the rent ($0 to the Electric amount capped at the electricity bill), and a NONE home with a cooling cost and the deprecated direct GAS or OIL row (that row to the Electric amount).
  • UNSPECIFIED adapter. Lines 28-53 are main's, and line 60 keeps the route closed. Ran: unspecified with only an electricity bill and a cooling cost pays $440 (main by hand: min(440, 1,358.76)); unspecified on the deprecated GAS row with no gas bill pays $0 (main: min(1,193, 0)); the committed case pays $300.
  • Heat in rent. Line 58 is main's definition moved up. Ran: a heat-in-rent home with a cooling cost pays the flat $200 for one person at level 3 and for two people at level 1.
  • Heated home with a heating bill. Ran: gas heat with a $300 gas bill pays $300 with and without a cooling cost. Electric heat with a cooling cost pays $440 once.
  • Electric lookup keys. Line 72 is main's expression, moved out of the select list, where it was already evaluated for every row. electricity.yaml and gas.yaml each hold all 80 cells (two housing types, income levels 1 to 10, sizes 1 to 4), so every DC household finds a key.
  • Units outside DC. The level-0 lookup is the same as on main, neither better nor worse. A two-household run (DC and Maryland) raises ParameterNotFoundError for payment.electricity.0 at line 72 of the head, and at the same lookup with main's formula (both ran). The fix adds no lookup and no key. In a test merge with Add a national LIHEAP total, an Illinois regular heating payment and LIHEAP cooling registry notes #10730 the floor on income_level feeds line 72 (read, not run).
  • Cap. The cooling route caps at pre_subsidy_electricity_expense (line 67). With the electricity cap removed the three cooling cases return 0 (ran). With the cooling-cost term removed the no-cooling case returns 440 instead of 0 (ran).
  • Hand checks of the two new cases (FY2026 values on main; wages $14,001 give income level 3, since 10,000 < 14,001 <= 15,000):
    1. Wood heat, no wood bill, cooling cost: known type, DC row Electric, heating_expense = wood_expense = 0, so the cooling route applies. Electric SF, level 3, one person = $440; min(440, 1,358.76) = 440.
    2. Gas heat, $300 gas bill, cooling cost: heating_expense = 300, so the heating route applies. Gas SF, level 3, one person = $1,193; min(1,193, 300) = 300.
      No other expectation changed in the fix commit.
  • Source-conflict ratchet. The new comments in the formula and the tests contain none of the ratchet's phrases, and no DC file is in KNOWN_UNMARKED.
  • Registry. The programs.yaml note ("covers cooling-only households, paying the Electric row capped at the electricity bill") still matches the code.

Axiom

axiom: TheAxiomFoundation/rulespec-us#2179 queued is valid. The issue is open, labelled pe-parity, was updated on 2026-11-12 at 17:10 UTC and names head fe22688810. Its scope line, required-output definition and interpretation 4 state the widened route, and companion cases 12 (wood, $440) and 13 (gas with a $300 bill, $300) match the two new tests.

A few sentences are left from the earlier head and can go with the next edit: the Objective still says "a home with no heating"; the Tests line says 28 cases with 5 new (the head has 30, 7 of them new for cooling); case 5's derivation says "The cooling route needs heating_type NONE"; interpretation 2 says the Electric column comes through the NONE-to-Electric mapping (it is now taken explicitly); interpretation 11 still says the body quotes the live page sentence; and "has no review yet" and "not yet passed in CI at this head" are out of date. If the cases under Should 1 are added, mirror them as companion cases.

CI

All 39 checks pass on the run for head fe22688810 (started 2026-11-12 17:09 UTC); none is pending. The PR's files ran at this head: in the selective-tests job dc_liheap_eligible.yaml 6 passed and dc_liheap_payment.yaml 30 passed, and in the baseline states shard dc_liheap_eligible.yaml (6), dc_liheap_income_level.yaml (3) and dc_liheap_payment.yaml (30) all passed.

Branch

The head is 11 commits behind main and merges cleanly. Main now includes the sibling PRs #10732, #10736, #10738 and #10740. The open siblings #10730, #10734 and #10762 each test-merge cleanly with this head. #10730 still edits only the income_level line of dc_liheap_payment.py, and the merged formula keeps both changes.

Verdict: REQUEST_CHANGES

@DTrim99 DTrim99 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting one change: three short test cases. The fix does what round 1 asked. The cooling route now takes a known heating type with no heat in the rent, no heating cost and a cooling cost, and pays the Electric amount explicitly, capped at the electricity bill. Both requested cases are in and right by hand, and no household that main pays gets a different amount.

The fix added three terms that no committed case exercises. With each removed in turn, all 30 cases in dc_liheap_payment.yaml still pass (run on the head tree):

  • the explicit Electric amount: a gas-heated home with no gas bill and a cooling cost should get $440, and gets $1,193 without the term;
  • the heat-in-rent guard: a heat-in-rent tenant with a cooling cost should keep the flat $200, and gets $440 with no cap without it;
  • the known-type guard: an unspecified type on the deprecated GAS row with no gas bill should stay at $0.

The three households are written out in the report above (New findings, Should Address 1), and all three pass at the head as written. The comment above the unspecified-fuel case also still says the route keys on NONE only. Two optional suggestions follow. CI is green at fe22688810.

Add a gas-heated home with no gas bill and a cooling cost (Electric $440, not
the Gas row), a heat-in-rent tenant with a cooling cost (flat $200), and an
unspecified type on the deprecated GAS row with no gas bill ($0). Reword the
comment above the unspecified-fuel case, the changelog fragment and the
dc_liheap_heating_type documentation for the widened route.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hua7450

hua7450 commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks, David. In d684d7d68f:

  • Should 1. The three households are in dc_liheap_payment.yaml as you wrote them:

    • gas heat with no gas bill: $440, with the GAS row asserted too;
    • heat in the rent with a cooling cost: $200 on the HEAT_IN_RENT row;
    • an unspecified type on the direct GAS row with no gas bill: $0.

    The comment above the unspecified-fuel case now says the cooling route needs a known heating type.

  • Suggestion 1. The changelog now says "households with no heating cost". The dc_liheap_heating_type documentation now says a cooling-only household takes the electricity amount whatever the direct row is.

  • Suggestion 2. "Not modeled" now says a small heating bill can pay less than none ($100 against $440).

  • LIHEAP cooling: encode District of Columbia LIHEAP Regular benefit for households whose only energy cost is cooling (PE-US PR 10742) TheAxiomFoundation/rulespec-us#2179 is updated: the objective, the tests line, case 5's derivation, interpretations 2 and 11, the matrix-row output note, the review and CI sentences, and companion cases 14-16 for the three new tests.

No formula changed.

@hua7450
hua7450 requested a review from DTrim99 November 12, 2026 20:15
@DTrim99

DTrim99 commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator

PR Review (round 3): Pay the DC LIHEAP Regular benefit to cooling-only households

Reviewed at head d684d7d68f. There is one commit since fe22688810 and no merge of main in between.

Summary

The fix commit closes the one open item from round 2. It adds the three cases I asked for, with the inputs and expected values I gave. By my reading, each case fails if its term is removed from dc_liheap_payment.py. The commit also takes both suggestions.

No formula changed. The commit touches the payment test file, the changelog fragment and the documentation string of dc_liheap_heating_type.

The PR's test files ran and passed in CI at this head, including 33 payment cases. The branch merges cleanly with current main, which now includes the income-level floor from #10730.

I ran nothing locally this round. Each claim below rests on one of three things, and says which:

  • hand tracing from the head code and the matrix values on main;
  • reading the diff;
  • the CI job logs.

No findings are open.

Prior findings

Finding Status Evidence
Should 1 (a): no test for the explicit Electric amount in the cooling branch (dc_liheap_payment.py line 88) Fixed Case "Cooling only, a gas-heated home with no gas bill takes the electricity amount, not its gas row." (test lines 452-477). By hand at the head it gives row GAS and $440. By my reading, if line 88 becomes matrix_amount = heating_amount, the case returns min($1,193, $1,358.76) = $1,193 and fails.
Should 1 (b): no test for the heat-in-rent guard (line 61) Fixed Case "A heat-in-rent tenant with a cooling cost keeps the flat heat-in-rent amount." (lines 504-529). By hand it gives row HEAT_IN_RENT and $200. By my reading, without ~heat_in_rent the household takes the cooling route, because its gas bill is $0. Line 89 then returns the Electric $440 with no cap, and the case fails.
Should 1 (c): no test for the known-type guard (line 60), and a stale comment above the unspecified-fuel case Fixed Case "Cooling cost with an unspecified fuel on the deprecated gas row and no gas bill pays nothing." (lines 584-608). By hand it gives $0. By my reading, without ~unspecified the case returns min($440, $1,358.76) = $440 and fails. The comment at lines 555-558 now says "The cooling route needs a known heating type", which matches line 60.
Suggestion 1: changelog and dc_liheap_heating_type documentation wording Taken The changelog now says "households with no heating cost that pay for cooling". The documentation adds that a cooling-only household "takes the electricity amount whatever this row is". Both match the code (see Verified).
Suggestion 2: disclose that a small heating bill can pay less than none Taken "Not modeled" now has this line. By hand: a gas-heated home with a $100 gas bill and a cooling cost has a heating cost, so it stays off the cooling route and gets min($1,193, $100) = $100. With no gas bill it takes the cooling route and gets min($440, $1,358.76) = $440.

The three mutation results come from reading the head formula. Each matches what round 2 got when it ran the same household against the same formula with that term removed: $1,193, $440 with no cap, and $440. dc_liheap_payment.py is unchanged between fe22688810 and this head. Round 2's runs also showed that the 30 earlier cases all still pass with each term removed. So these three new cases are what pin the terms.

New findings

Critical

None.

Should address

None.

Suggestions

None.

Verified

  • No formula changed (read). git diff fe22688810 d684d7d68f touches three files:

    • changelog.d/dc-liheap-cooling.fixed.md: the first sentence is reworded (Suggestion 1).
    • dc_liheap_heating_type.py: one clause is added to the documentation string. The enum, the default, defined_for and the formula are unchanged.
    • dc_liheap_payment.yaml: three cases are added and one comment is reworded. No existing input or expectation changed.

    dc_liheap_payment.py, dc_liheap_eligible.py, every parameter file and programs.yaml are untouched. The author's "No formula changed" is correct.

  • Matrix values (read on main). All values are dated 2025-10-01, so they apply to period 2026:

    • payment/electricity.yaml, SINGLE_FAMILY, level 3, size 1: 440.
    • payment/gas.yaml, same cell: 1,193.
    • payment/heat_in_rent.yaml: 200.
    • income_level_increment.yaml: 5,000.

    These match row SF 10000 1 of the FY2026 matrix: Gas $1,193, Electric $440, HIR $200.

  • Hand checks of the three new cases.

    • Common inputs: DC, 2026, one adult aged 40, wages $14,001, single-family, has_cooling_expense: true, pre_subsidy_electricity_expense: 1_358.76.
    • Shared steps: $14,001 is under the FY2026 one-person limit of $61,841. The income level is 3, because the first increment at or above $14,001 is 15,000. The capped size is 1.
    1. Gas heat, no gas bill. No heat is in the rent and the type is NATURAL_GAS, so dc_liheap_heating_type = GAS. The type is known, so heating_expenses = heating_expense = gas_expense = 0, and cooling_only is true. The cap is 1,358.76. Line 88 sets matrix_amount to the Electric 440, not the Gas 1,193. min(440, 1,358.76) = 440, row GAS. Both expectations match.
    2. Heat in the rent. The first branch of its select gives dc_liheap_heating_type = HEAT_IN_RENT. ~heat_in_rent makes cooling_only false. heating_amount = 200, and line 89 returns it with no cap: 200, row HEAT_IN_RENT. Both expectations match.
    3. Unspecified type, direct GAS row, no gas bill. unspecified is true, so heating_expenses is the legacy expense. There is no person-level total, so it is the GAS row's gas_expense, which is 0. ~unspecified makes cooling_only false. min(1,193, 0) = 0. Main's formula also gives min(1,193, 0) = 0, as the case comment says. The expectation matches.
  • Changelog wording (read). "Households with no heating cost that pay for cooling, from the Electric row capped at their electricity bill" covers both a home without heating and a heated home with no heating bill. It leaves out the known-type and heat-in-rent conditions, which is fine for a changelog: heat-in-rent households already got the flat amount, and the unspecified path is unchanged.

  • Documentation wording (read). The new clause defines a cooling-only household as lines 59-64 do. "Whatever this row is" holds for a direct ELECTRICITY, GAS or OIL row, which line 88 overrides. A direct HEAT_IN_RENT row counts as heat in the rent, which the clause's own condition excludes, and line 89 pays it the flat amount.

  • PR body (read; the live copy).

    • The Test plan lists the three new cases and gives eleven new cases in all: ten in dc_liheap_payment.yaml (23 cases before the PR, 33 now) plus Case 6 in dc_liheap_eligible.yaml.
    • The cooling_only line matches lines 59-64.
    • "Not modeled" now has the line comparing $100 with $440, which is right by hand (above).
  • Source-conflict ratchet (read). The ratchet scans comments in variables, parameters and reforms only. This commit's one change in those folders is inside a string, not a comment. The new test comments are outside the scanned folders. No DC file is in KNOWN_UNMARKED.

  • Regressions. None from this commit. It changes no code (read), and CI shows the 30 earlier cases passing unchanged at this head.

Axiom

axiom: TheAxiomFoundation/rulespec-us#2179 queued is valid.

  • The issue is open, labelled pe-parity, was updated on 2026-11-12 at 20:15 UTC, and names head d684d7d68f in the PR line and the implementation section.

  • Companion cases 14, 15 and 16 mirror the three new tests by YAML name, with the same inputs and results:

    • case 14: row GAS and $440;
    • case 15: row HEAT_IN_RENT and $200;
    • case 16: $0.

    Each derivation comes from row SF 10000 1 or is labelled a PolicyEngine convention.

  • The round-2 leftovers are fixed:

    • the Objective says "a home with no heating cost";
    • the Tests line says 33 cases, 10 of them new;
    • case 5's derivation says the route needs a known heating type;
    • interpretation 2 says the Electric amount is taken explicitly;
    • interpretation 11 describes the body's citation as it now reads;
    • the review sentence names round 2.

A few small leftovers can go with the next edit; none blocks:

  • "Why queued" and the Tests line say the tests have not yet run in CI at this head. They have now passed.
  • The implementation section lists dc_liheap_eligible as "Read unchanged", but interpretation 7 says the PR changes it.
  • The Companion tests intro says "Cases 1 to 5 are the PR's new cases". That no longer covers cases 12 to 16.

CI

All 38 checks pass on run 43298495938 for head d684d7d68f, which started on 2026-11-12 at 20:15 UTC. None is pending. The PR's test files ran at this head:

  • Selective-tests job: dc_liheap_eligible.yaml 6 passed; dc_liheap_payment.yaml 33 passed.
  • Baseline states shard 4: dc_liheap_eligible.yaml 6, dc_liheap_income_level.yaml 3 and dc_liheap_payment.yaml 33, all passed.

Branch

The head is 28 commits behind main and merges cleanly. Since round 2, main has taken #10730 and #10734.

Verdict: APPROVE

@DTrim99 DTrim99 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. The three cases I asked for are in, with the inputs and expected values I gave, and by my reading each fails if its term is removed: the explicit Electric amount ($440, not the Gas $1,193), the heat-in-rent guard (the flat $200), and the known-type guard ($0). Both suggestions are taken, and no formula changed. CI is green at d684d7d68f, and the branch merges cleanly with main, which now has #10730's income-level floor beside this PR's cooling block.

@hua7450
hua7450 merged commit ab97e35 into main Nov 12, 2026
38 checks passed
@hua7450
hua7450 deleted the dc-liheap-cooling branch November 12, 2026 21:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement DC LIHEAP Regular benefit for cooling-only households

2 participants