Resolve solid-fuel room-heater overrides off Gas CPSU to their SAP codes 🟥

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-07-01 15:39:31 +00:00
parent de9159f9b8
commit f253a7b9ba
2 changed files with 112 additions and 0 deletions

View file

@ -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 631636 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: <band> 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).

View file

@ -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"),
[