diff --git a/docs/adr/0044-hhrsh-baseline-archetype-drags-its-intrinsic-controls.md b/docs/adr/0044-hhrsh-baseline-archetype-drags-its-intrinsic-controls.md new file mode 100644 index 000000000..354844e20 --- /dev/null +++ b/docs/adr/0044-hhrsh-baseline-archetype-drags-its-intrinsic-controls.md @@ -0,0 +1,94 @@ +# A baseline HHRSH override drags its intrinsic SAP-409 controls, not the generic manual storage default + +## Status + +accepted + +## Context + +A **Landlord Override** naming *"High heat retention storage heaters"* (HHRSH) had +no home in the heating taxonomy: `MainHeatingSystemType` carried +`ELECTRIC_STORAGE_OLD` (SAP 401), `_SLIMLINE` (402), `_CONVECTOR` (403) and +`_FAN` (404), but no High-Heat-Retention member. So the LLM classifier funnelled +the **best modern storage** into **`"Electric storage heaters, old"` (401)** — the +**worst** large-volume type. An audit found **111** `property_overrides` rows on +that mapping (`'Electric Storage Systems: High heat retention storage heaters'` → +`'Electric storage heaters, old'`). Same missing-archetype defect as ADR-0041. + +HHRSH is a distinct, modellable RdSAP system: **SAP Table 4a code 409** (high heat +retention storage heaters), which the calculator already scores +(`sap_efficiencies` carries `409: 1.00`). The **HHRSH *measure*** already installs +this exact code (`_HHR_STORAGE_SAP_CODE = 409`, +`domain/modelling/generators/heating_recommendation.py`) on a **Dual** off-peak +meter with **control 2404**. + +The subtlety is the **charge control**. `main_heating_system_overlay._control_for` +drags the **conservative manual** charge control (`2401`) for *every* storage code +401–409, following ADR-0041's amendment ("an *unobserved* storage charge control +defaults to the conservative manual one"). Table 4e Group 4's mean-internal- +temperature adjustments are **manual +0.7 °C, automatic/Celect +0.4 °C, HHR 0 °C** +— so a manual default carries a penalty. Applying it to a 409 heater would swap +"scored as old-storage 401" for "scored as 409-but-penalised" — still under- +crediting the 111 dwellings, and pairing 409 with manual control is not a +combination RdSAP produces. + +**HHRSH has a single, intrinsic charge control (2404).** The control is therefore +*named by the archetype*, not *unobserved* — unlike a generic storage heater whose +control genuinely is unknown. So ADR-0041's manual-default rule does not apply to +409. + +## Decision + +Add the baseline HHRSH archetype and let it drag its **coherent, intrinsic** +companions — mirroring how a gas-boiler archetype drags full modern controls +(`_control_for`) rather than a conservative default. + +1. **`MainHeatingSystemType.ELECTRIC_STORAGE_HIGH_HEAT_RETENTION` = + `"Electric storage heaters, high heat retention"`**, mapped in + `_MAIN_HEATING_CODES` to **SAP Table 4a code 409**. +2. **Control 2404 (HHRSH controls), not the manual default.** `_control_for` + special-cases 409 → `2404` ahead of the generic storage → `2401`. HHRSH's single + intrinsic control is archetype-implied, an **exception to ADR-0041's + unobserved-storage manual default** (which stands for 401–404). +3. **Electricity fuel (RdSAP 29).** `_natural_fuel_for` drags electricity for 409, + per ADR-0041's "storage → electricity" — so a mis-lodged non-electric fuel can't + leave an electric-storage dwelling incoherent. Scoped to 409 ("scoped per family + as archetypes land"); the pre-existing 401–404 fuel-drag gap is not touched here. +4. **Dual/off-peak meter falls out of the code** — 409 is already in + `_ASSUMED_DUAL_METER_CODES`, so the off-peak meter drags automatically (ADR-0035 + coherent companions). +5. **Deterministic guard + reclassify, TEXT-first / pgEnum-deferred.** A + `main_heating_guard` recognises the structured `"Electric Storage Systems: High + heat retention storage heaters"` phrasing (wired via `GuardedColumnClassifier`); + `scripts/reclassify_hhrsh_storage.py` reuses the same guard as its decision + function, updates `property_overrides.override_value` (TEXT) unconditionally, and + defers the `landlord_main_heating_system_overrides.value` pgEnum-cache write for + the new value. + +## Consequences + +- The 111 HHRSH dwellings score as **409 + HHR control 2404 + off-peak meter + + electricity** — the efficient modern archetype — instead of the worst old + large-volume 401. Correct Rebaselining (ADR-0039). +- **FE-owned pgEnum addition (Dan):** one new `main_heating_system` value, + `"Electric storage heaters, high heat retention"` (memory + `main-heating-system-pgenum-is-fe-owned`). TEXT read path is fixed on merge; the + cache write defers until the migration lands. +- **Baseline vs measure stay distinct but consistent:** both use code 409 + + control 2404; the measure additionally installs a designed HW cylinder / immersion + end-state, which a baseline (existing-system) override does not. +- Extends [ADR-0041](0041-landlord-heating-classification-targets-a-complete-modellable-taxonomy.md) + (complete taxonomy; **refines** its manual-default amendment for the single- + control HHRSH case) and [ADR-0035](0035-coherent-heating-system-synthesis.md) + (companions drag from the code). + +### Alternatives rejected + +- **409 + manual control (2401), holding to ADR-0041's default.** Rejected: HHRSH's + control is intrinsic (single option), not unobserved; the +0.7 °C manual penalty + under-credits, and 409+manual is not a real RdSAP combination. +- **Reuse an existing storage archetype (401–404).** Rejected: none carries the + HHR retention/control — the whole point of the mis-score. +- **Leave fuel unresolved (as 401–404 do).** Rejected for 409: an explicit + electricity drag keeps the system coherent if the lodged fuel is wrong; the older + archetypes' gap is a separate, out-of-scope cleanup. diff --git a/tests/domain/epc/test_main_heating_system_overlay.py b/tests/domain/epc/test_main_heating_system_overlay.py index 6e68cce03..f33efe3c6 100644 --- a/tests/domain/epc/test_main_heating_system_overlay.py +++ b/tests/domain/epc/test_main_heating_system_overlay.py @@ -98,6 +98,19 @@ def test_storage_heater_subtypes_decode_to_their_codes( assert simulation.heating.sap_main_heating_code == code +def test_high_heat_retention_storage_decodes_to_its_own_code_not_old_storage() -> None: + # Act + simulation = main_heating_overlay_for( + "Electric storage heaters, high heat retention", 0 + ) + + # Assert — HHRSH is SAP Table 4a code 409, NOT old large-volume storage (401) + # the classifier funnelled it into (ADR-0044). + assert simulation is not None + assert simulation.heating is not None + assert simulation.heating.sap_main_heating_code == 409 + + @pytest.mark.parametrize( "main_heating_value", [