Resolve a high-heat-retention storage override to SAP code 409 🟥

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-07-01 14:26:25 +00:00
parent 6819ca7d89
commit 79f48c0564
2 changed files with 107 additions and 0 deletions

View file

@ -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
401409, 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 401404).
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 401404 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 (401404).** Rejected: none carries the
HHR retention/control — the whole point of the mis-score.
- **Leave fuel unresolved (as 401404 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.

View file

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