Model/docs/adr/0028-secondary-heating-removal.md
Khalim Conn-Kowlessar f9a89a8e11 docs(adr): secondary heating removal — ADR-0028 + CONTEXT term
Records the four load-bearing design decisions from the grill-with-docs session:
standalone co-selectable rec; eligibility = lodged-only (no effectiveness gate,
electric-storage §A.2.2 no-op is the Optimiser's call); dedicated clearing
SecondaryHeatingOverlay; flat per-dwelling cost (a lodged secondary is fixed per
RdSAP, so a real decommission job, not room-scaled).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 15:32:14 +00:00

33 lines
4.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Secondary Heating Removal — standalone recommendation, eligibility, overlay, costing
We model "removal of secondary heating" — stripping a dwelling's lodged secondary heating system so the main system serves 100% of space heating — as a **standalone, co-selectable Recommendation** (`MeasureType.SECONDARY_HEATING_REMOVAL`), built API-inputs-first. This records the four load-bearing, non-obvious choices made designing it.
## Status
Accepted.
## Decisions
### 1. Standalone Recommendation, not an Option in the Heating & Hot Water rec
The heating expansion (ADR-0024) consolidated heating into one rec whose Options are **mutually-exclusive** (the Optimiser picks ≤1) because they are whole-system replacements you would never combine. Secondary-heating removal is different in kind: it is independent of, and freely combinable with, a tune-up or a boiler upgrade (you can remove a panel heater *and* upgrade the controls). Making it an Option would falsely force mutual exclusivity with the partial heating upgrades. As a standalone rec it composes additively like loft/cavity/glazing.
The redundancy risk with a whole-system bundle is self-resolving: an ASHP is calculator-category 4 → secondary fraction is already 0.00, so removal adds **zero marginal SAP** on top of it, and a SAP-maximising Optimiser never pays for a zero-gain measure. The two overlays touch disjoint fields, so there is no double-*SAP*-counting either.
### 2. Eligibility is physical only — offer iff a secondary is lodged; no effectiveness gate
Offered **iff `sap_heating.secondary_heating_type is not None`** (a surveyor-lodged secondary exists to remove). Per ADR-0024's principle — *eligibility encodes only physical/planning installability; the Optimiser owns the economics* — we deliberately do **not** gate out the cases where removal cannot move SAP.
The surprising case: on an **electric-storage main** (SAP codes 401407/409/421), RdSAP §A.2.2 **forces a default secondary (693, portable electric) back** even after the lodged one is removed, so removal is a guaranteed zero-SAP no-op. That is an *effectiveness* fact, not an installability one — so eligibility still offers it, and the Optimiser de-selects it (zero gain, real cost). This is why our only example cert (001431, main 402) shows F35→F35 unchanged, matching Elmhurst exactly.
### 3. A dedicated `SecondaryHeatingOverlay` that *clears* fields
Every other Simulation Overlay obeys "a `None` field means leave the baseline unchanged" and writes **target states**. Removal is the opposite — it must set `secondary_heating_type` and `secondary_fuel_type` *to* `None`, which that convention structurally cannot express. Rather than wart the absolute-target-state `HeatingOverlay` with a remove-flag, removal gets its own minimal overlay surface (`SecondaryHeatingOverlay`, with an explicit `remove` flag) + `EpcSimulation` slot + `_fold_secondary_heating`, mirroring the one-overlay-per-measure-family pattern. It is the one overlay that sets a value to *absent*.
### 4. Flat per-dwelling decommission cost, not room-scaled
Legacy `recommendations/SecondaryHeating.py` scaled cost by a room count (`habitable heated`). We price a **flat per-dwelling scalar (~£250, contingency 0.25)** instead. Two reasons: (a) the EPC lodges a *single* secondary system with **no heater count**, so the legacy room proxy is unfounded; (b) because RdSAP only records a secondary when a **fixed** emitter is present (portable plug-in heaters are ignored), a lodged secondary is by definition a fixed unit — its removal is a real but roughly-fixed job (one electrician visit to disconnect/isolate a hard-wired panel/convector/radiant heater + a blanking plate + localised making-good of the wall). The contingency absorbs the unknown count / hard-wire status / repaint extent. No composite `Products` machinery and no `rates.json` entry — a future data-only upgrade if a real per-unit rate sheet arrives.
## Validation
The before/after example (cert 001431, main 402) is a forced-secondary delta-0 case, and shares the boiler-fixtures' Summary-path roof-fidelity gap, so the cascade pin asserts the **field-delta** (`score(before + remove-overlay) == score(before)` at delta 0), proving the overlay clears the fields and the calculator correctly re-forces the §A.2.2 default. A **synthetic unit test** recasts 001431's main to a non-forced gas-boiler code and asserts removal yields a *positive* SAP delta (Table 11 fraction → 0), exercising the value path without a second real cert.