From f253a7b9baeae977d28f7ce3e15d3e4607193d44 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 1 Jul 2026 15:39:31 +0000 Subject: [PATCH] =?UTF-8?q?Resolve=20solid-fuel=20room-heater=20overrides?= =?UTF-8?q?=20off=20Gas=20CPSU=20to=20their=20SAP=20codes=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- ...lid-fuel-and-electric-boiler-archetypes.md | 89 +++++++++++++++++++ .../epc/test_main_heating_system_overlay.py | 23 +++++ 2 files changed, 112 insertions(+) create mode 100644 docs/adr/0045-gas-cpsu-dumping-ground-splits-into-solid-fuel-and-electric-boiler-archetypes.md diff --git a/docs/adr/0045-gas-cpsu-dumping-ground-splits-into-solid-fuel-and-electric-boiler-archetypes.md b/docs/adr/0045-gas-cpsu-dumping-ground-splits-into-solid-fuel-and-electric-boiler-archetypes.md new file mode 100644 index 000000000..49117b477 --- /dev/null +++ b/docs/adr/0045-gas-cpsu-dumping-ground-splits-into-solid-fuel-and-electric-boiler-archetypes.md @@ -0,0 +1,89 @@ +# The "Gas CPSU" dumping ground splits into solid-fuel room-heater and electric-boiler archetypes + +## Status + +accepted + +## Context + +ADR-0041 named **"Gas CPSU" (SAP Table 4a 120, mains gas)** as the original +garbage-drawer archetype the LLM classifier over-used when the heating taxonomy +lacked a correct target. An audit of every `main_heating_system` override still +resolving to `"Gas CPSU"` found it standing in for three *different*, unrelated +systems — a mains-gas CPSU is neither solid fuel nor electric, so every one of +these is mis-scored (wrong system **and**, for the electric ones, wrong fuel): + +| description | rows | true system | +| --- | --- | --- | +| Solid fuel room heaters: Open fire in grate | 16 | open fire (solid) | +| Solid fuel room heaters: Open fire with back boiler (no radiators) | 19 | open fire + back boiler | +| Solid fuel room heaters: Closed room heater with boiler (no radiators) | 2 | closed room heater + boiler | +| Boiler: A rated NA | 58 | **electric** boiler | +| Boiler: A rated CPSU | 1 | electric CPSU | +| Boiler: C rated CPSU | 1 | *genuine* gas CPSU — correct | + +**The electric boilers are the sharpest mis-score.** All 58 `"Boiler: A rated NA"` +rows carry a `main_fuel = electricity` override — "NA" is the Hyde boiler-type +value meaning "not a gas combi / regular / CPSU", i.e. an **electric boiler**. +Classified as Gas CPSU they are scored as a mains-gas stored-water unit. A +lodged-cert check showed **defer-to-lodged is not viable**: 34 of the 58 lodge +**no** main-heating code at all (fuel 0), 11 lodge electric *room heaters* (691), +a few lodge community heating — the very gap the landlord override exists to fill. +So the faithful move is to model the electric boiler the landlord names, informed +by the electricity fuel we hold, not to defer to an empty cert. + +The correct SAP Table 4a codes are exact (confirmed against +`domain/sap10_ml/sap_efficiencies.py`): **631** open fire (32%), **632** open fire ++ back boiler (50%), **634** closed room heater + boiler (65%); **191** +direct-acting electric boiler (100%). All are already scored by the calculator, +and 631/632/634 already sit in the overlay's `_SOLID_FUEL_ROOM_HEATER_CODES` set, +so their coherent companions (house-coal fuel 33, room-heater control 2601, +category 10) drag automatically (ADR-0035). + +## Decision + +Add the missing archetypes and reclassify the bucket. No calculator change. + +1. **Three solid-fuel room-heater archetypes** in `_MAIN_HEATING_CODES`: + `"Solid fuel room heater, open fire"` → 631, + `"Solid fuel room heater, open fire with back boiler"` → 632, + `"Solid fuel room heater, closed with boiler"` → 634. Companions drag from the + code (already-covered 631–636 set) — no overlay-logic change. +2. **`"Electric boiler"` → SAP 191**, dragging **electricity (29)** via + `_natural_fuel_for` — an electric boiler is unambiguously electric (ADR-0041 + "direct-acting/storage → electricity"), and this also closes the pre-existing + gap where 191 dragged no fuel. Meter stays Single, control None (191 is neither + a storage nor a wet-boiler-control system). +3. **Deterministic `main_heating_guard` + reclassify**, TEXT-first / + pgEnum-deferred. The guard resolves the structured phrasings by description + alone: `"…open fire … back boiler"` → 632 (tested before the bare `"open fire"` + → 631), `"…closed … with boiler"` → 634, and `"Boiler: NA"` → + `"Electric boiler"` (the NA type is the electric signal). `scripts/ + reclassify_gas_cpsu.py` reuses the guard as its decision function and defers the + four new `main_heating_system` pgEnum-cache values. + +## Consequences + +- ~95 rows leave the Gas CPSU garbage drawer for faithful archetypes: 37 solid + fuel scored as solid fuel (not mains gas), 58 electric boilers scored as electric + (not a mains-gas CPSU). Correct Rebaselining (ADR-0039). +- **FE-owned pgEnum additions (Dan):** four new `main_heating_system` values — + `"Solid fuel room heater, open fire"`, `"Solid fuel room heater, open fire with + back boiler"`, `"Solid fuel room heater, closed with boiler"`, `"Electric + boiler"`. TEXT read path fixed on merge; cache writes defer to the migration. +- Extends [ADR-0041](0041-landlord-heating-classification-targets-a-complete-modellable-taxonomy.md) + (complete taxonomy — retiring the Gas CPSU garbage drawer it named) and + [ADR-0035](0035-coherent-heating-system-synthesis.md) (companions drag from the + code). + +### Out of scope + +- **Electric CPSU (SAP 192), the 1 `"Boiler: A rated CPSU"` row.** Its description + is identical to the genuine gas `"Boiler: C rated CPSU"` bar the efficiency + band — the electric/gas split is **fuel-determined**, which a pure-string guard + cannot see. Rather than break the guard = reclassify-decision-function symmetry + for one row, this is deferred to a small fuel-aware follow-up; the gas CPSU row + stays correctly on 120. +- **Whether "NA" ever denotes a non-electric boiler.** It is 100% electric in the + corpus and the guard treats it as electric; a future non-electric "NA" would be + a re-open signal, not a silent mis-map (the fuel override would disagree). diff --git a/tests/domain/epc/test_main_heating_system_overlay.py b/tests/domain/epc/test_main_heating_system_overlay.py index b100f75a6..4421fefe1 100644 --- a/tests/domain/epc/test_main_heating_system_overlay.py +++ b/tests/domain/epc/test_main_heating_system_overlay.py @@ -78,6 +78,29 @@ def test_heating_archetypes_decode_to_their_sap_codes( assert simulation.heating.sap_main_heating_code == code +@pytest.mark.parametrize( + ("main_heating_value", "code"), + [ + # Solid-fuel room heaters the LLM funnelled into "Gas CPSU" (120, mains + # gas). SAP Table 4a: 631 open fire, 632 open fire + back boiler, 634 + # closed room heater + boiler (ADR-0045). + ("Solid fuel room heater, open fire", 631), + ("Solid fuel room heater, open fire with back boiler", 632), + ("Solid fuel room heater, closed with boiler", 634), + ], +) +def test_solid_fuel_room_heaters_decode_off_gas_cpsu( + main_heating_value: str, code: int +) -> None: + # Act + simulation = main_heating_overlay_for(main_heating_value, 0) + + # Assert — its own solid-fuel code, not mains-gas CPSU (120). + assert simulation is not None + assert simulation.heating is not None + assert simulation.heating.sap_main_heating_code == code + + @pytest.mark.parametrize( ("main_heating_value", "code"), [