diff --git a/CONTEXT.md b/CONTEXT.md index 01623858a..85e65f000 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -159,8 +159,12 @@ _Avoid_: roof warning (unspecific), re-roof recommendation (there is no engine m Resolving a **Landlord Description** (unbounded free-text a landlord supplies for one component — "CWI" / "Cav filled" / "cavity insulated" all name one thing) onto a **Recognised Internal Description** via an LLM classifier, persisted in the `landlord_*_overrides` table (`source=classifier`) as a reviewed cache. Four vocabularies are kept **distinct** and must not be conflated: a **Landlord Description** (unbounded input); a **Recognised Internal Description** (the closed target taxonomy — e.g. a `MainHeatingSystemType` archetype — each binding to a Simulation Overlay); a **Lodged Description** (the gov-EPC `main_heating[].description` rendering, e.g. "Room heaters, electric" — only an example of which system *types* occur, never a map key); and the **SAP main heating code** (Table 4a/4b, what the calculator consumes). The classifier maps Landlord → Recognised Internal → SAP code. When it cannot confidently place the text it emits **`None`** (no overlay → the lodged EPC stands, surfaced to the user as "no suitable match"), **never the nearest wrong archetype** — the target taxonomy must be complete enough that a real system always has a correct home, so the classifier never overflows into a garbage-drawer archetype (ADR-0041). **Heating Companion Set**: -The coherent SAP inputs a **main heating system** override drags alongside the **SAP main heating code** so the **Effective EPC** reads as one internally-consistent system, not a hybrid of the new code and the *replaced* system's leftovers — heating **category** (SAP Table 4a group: gas→2, heat pump→4, network→6, storage→7, electric underfloor→8, warm air→9, room heater→10), **charge/heating control** (Table 4e group), **natural fuel**, and **meter** (see **Off-Peak Meter**). Companions divide by how firmly the archetype fixes them (ADR-0048): **category and control are archetype-forced** — always written, never inherited, because a wrong value silently mis-scores or mis-bills (a leftover storage **category 7** makes the Table 12a resolver bill peaky room heaters at the all-night rate; a leftover storage **control 2401** adds a +0.7 °C Table-4e penalty a room heater should not carry). **Fuel and meter defer** only where multiple values are genuinely coherent — fuel for an ambiguous carrier (community heating: gas CHP / biomass / waste heat), meter for a meter-flexible system (room heaters run on either; screed underfloor per ADR-0046) — while a meter-*locked* system still forces (storage/CPSU/HHRSH→Dual, non-electric→Single, ADR-0035). Because the overlay composes last-wins, an **unset (`None`)** forced companion is *inherited from the replaced system*, not cleared — so an archetype the overlay cannot yet fully companion (heat pump / community / underfloor / electric boiler control defaults are unmapped) is **logged as an error and continues** (log-not-raise, matching `flag_fuel_mismatch`), making the gap visible for review rather than shipping a silently-incoherent cert; the intended end-state is to fill each and flip the log to a raise. -_Avoid_: coherent companions (informal), dragged fields, heating defaults +The coherent SAP inputs a **main heating system** override drags alongside the **SAP main heating code** so the **Effective EPC** reads as one internally-consistent system, not a hybrid of the new code and the *replaced* system's leftovers — heating **category** (SAP Table 4a group: gas→2, heat pump→4, network→6, storage→7, electric underfloor→8, warm air→9, room heater→10), **charge/heating control** (Table 4e group), **natural fuel**, and **meter** (see **Off-Peak Meter**). Companions divide by how firmly the archetype fixes them (ADR-0048): **category and control are archetype-forced** — always written, never inherited, because a wrong value silently mis-scores or mis-bills (a leftover storage **category 7** makes the Table 12a resolver bill peaky room heaters at the all-night rate; a leftover storage **control 2401** adds a +0.7 °C Table-4e penalty a room heater should not carry). **Fuel and meter defer** only where multiple values are genuinely coherent — fuel for an ambiguous carrier (community heating: gas CHP / biomass / waste heat), meter for a meter-flexible system (room heaters run on either; screed underfloor per ADR-0046) — while a meter-*locked* system still forces (storage/CPSU/HHRSH→Dual, non-electric→Single, ADR-0035). Because the overlay composes last-wins, an **unset (`None`)** forced companion is *inherited from the replaced system*, not cleared — so an archetype the overlay cannot yet fully companion (heat pump / community / underfloor / electric boiler control defaults are unmapped) is **logged as an error and continues** (log-not-raise, matching `flag_fuel_mismatch`), making the gap visible for review rather than shipping a silently-incoherent cert; the intended end-state is to fill each and flip the log to a raise. A **fifth** archetype-forced companion — **boiler efficiency**, keyed by the **Boiler Efficiency Band** — is implemented per ADR-0068 (the override wins over the cert's efficiency, as a replacement makes the lodged value stale). +_Avoid_: coherent companions (informal), dragged fields, heating defaults; treating efficiency as a *deferring* companion (it is forced — the override is the surveyed current boiler) + +**Boiler Efficiency Band**: +The SEDBUK **A–G** rating a landlord carries in the **Landlord Description** for a boiler override (`Boiler: C rated Combi`, `Boiler: D rated Regular Boiler`) — an *attribute* of the override, **not** a **Recognised Internal Description** archetype. It names *how well the boiler burns*, orthogonal to the `MainHeatingSystemType` archetype that names the *system*; it must never mint per-band archetypes because that enum is **FE-owned** ([[main-heating-system-pgenum-is-fe-owned]]). It resolves to a **`(winter, summer)`** seasonal-efficiency pair — the fifth **Heating Companion** (ADR-0068) — that the calculator consumes ahead of the Table 4b code default (as a PCDB `winter_efficiency_pct` already does). C-and-below reuse accredited Table 4b code values (D→80 %, E→74 %, F→70 %, G→66 % winter); **A/B exceed Table 4b's 84 % gas ceiling** and take SEDBUK-derived values validated against an accredited Elmhurst build. Dropping the band (the pre-ADR-0068 overlay mapped every band to the flat condensing 84 %) over-credits D–G and under-credits A/B. It applies to the **SEDBUK-rated wet boilers only — gas/LPG/biogas *and* oil** (both were inflated to their condensing code); **solid-fuel (Table 4a 75 %), electric (100 %) and CPSU** boilers carry an *intrinsic* fixed efficiency and take no band. Stored as its own `boiler_efficiency_band` override row (A–G); absence is modelled as no value (no slot → the code's Table 4b default stands). +_Avoid_: "SEDBUK band" as a synonym for the **SAP main heating code** (the band picks the efficiency, not the code); conflating with the gov-EPC `mainheat_energy_eff` element rating (an A–G *presentation* band on the cert, not this override attribute); "boiler rating" (ambiguous with the EPC band) _Avoid_: "the LLM mapper is unreliable" (the failure mode is a too-small target taxonomy, not LLM language ability); conflating the landlord input vocabulary with the gov-EPC lodged rendering or the RdSAP entry-tool catalogue; treating a deterministic dict as a *replacement* for the LLM rather than a reviewed cache of its output **Lodgement Sentinels (ND / NI / AB)**: diff --git a/docs/adr/0068-boiler-efficiency-band-is-the-fifth-heating-companion.md b/docs/adr/0068-boiler-efficiency-band-is-the-fifth-heating-companion.md new file mode 100644 index 000000000..e71e92e7c --- /dev/null +++ b/docs/adr/0068-boiler-efficiency-band-is-the-fifth-heating-companion.md @@ -0,0 +1,157 @@ +# Boiler efficiency band is the fifth Heating Companion + +## Status + +accepted — implemented (band parser + overlay `(winter, summer)` slot + +calculator precedence + classifier/backfill; C-and-below live, A/B pending the +Elmhurst validation below) + +## Context + +A `main_heating_system` **Landlord Override** maps a **Recognised Internal +Description** archetype to a **SAP main heating code** and drags a **Heating +Companion Set** (ADR-0048): category and control are *archetype-forced*, fuel +and meter *defer*. The overlay +(`domain/epc/property_overlays/main_heating_system_overlay.py`) maps every gas +boiler to a single modern/condensing Table 4b code — `Gas boiler, regular` → 102, +`Gas boiler, combi` → 104, both **84 % winter / 74–75 % summer**. + +The Landlord spreadsheet's **SEDBUK A–G efficiency band** — carried in the +**Landlord Description** as `Boiler: C rated Combi`, `Boiler: D rated Regular +Boiler` — is **dropped at classification**: the `override_value` collapses to the +bare archetype (`Gas boiler, combi`) and the band survives only in +`property_overrides.original_spreadsheet_description`. So the companion set has +**no efficiency dimension** (the overlay docstring already flags "no efficiency +slot on the overlay/`MainHeatingDetail`"), and *every* overridden gas boiler is +modelled at the condensing efficiency regardless of its real band. + +Portfolio-796 audit evidence (2026-07-28), of 24 555 overridden gas boilers: + +| SEDBUK band | count | SEDBUK eff | modelled at 84 % is… | +|---|---:|---|---| +| A | 14 590 | ≥ 90 % | **under**-credited | +| B | 260 | 86–90 % | slightly under | +| C | 6 251 | 82–86 % | ~right | +| D | 1 691 | 78–82 % | over | +| E | 1 196 | 74–78 % | over | +| F | 206 | 70–74 % | over | +| G | 360 | < 70 % | over-credited | + +This is a **code-fidelity** gap (the overlay throws away information the landlord +gave us), distinct from the sibling **fuel-override data-quality** question +(#1701 — whether a landlord's `main_fuel` assertion is itself correct). + +## Decision + +1. **Efficiency becomes the fifth Heating Companion, in the *archetype-forced* + tier** (alongside category and control). When the override's band-derived + efficiency disagrees with the lodged cert, **the override wins** — a + system-replacing override is the landlord telling us the current truth, so the + cert efficiency is treated as stale. + +2. **The SEDBUK band is a new *attribute*, not a new archetype.** The + `MainHeatingSystemType` value stays `Gas boiler, combi`; a separate + **Boiler Efficiency Band** field rides alongside it. The archetype describes + the *system*, the band describes *how well it burns* — orthogonal. Crucially, + the `MainHeatingSystemType` PG enum is **FE-owned** + ([[main-heating-system-pgenum-is-fe-owned]]); minting `Gas-boiler-combi-D-rated` + archetypes would explode a shared cross-team contract 7×. + +3. **Full fidelity (option a), symmetric — not a downward-only cap.** The + companion carries every band's efficiency: A/B rise **above** 84 %, D–G fall. + "Override wins" is symmetric by construction; a one-sided clamp would encode a + policy ("we only correct downward") that a later reader would silently "fix" + into (a) anyway. + +4. **Mechanism: `(archetype + fuel) → base code`, then `band → (winter, summer) + efficiency slot` that overrides the code's default.** The **archetype and its + resolved fuel** pick the *base* `sap_main_heating_code` — this is already + ADR-0067's "resolve fuel at resolution" (`Gas boiler, regular` + an oil + `main_fuel` → `Oil boiler, regular` → an oil code), and the code still drives + system-type behaviour (combi vs regular hot water, etc.). The **band** then + sets a `(winter, summer)` seasonal-efficiency pair on the overlay / + `MainHeatingDetail` that the calculator consumes **ahead of** the code's + Table 4b default — mirroring the existing PCDB precedence + (`pcdb_main.winter_efficiency_pct`, `cert_to_inputs.py:2103`). So efficiency is + looked up by **(fuel family, boiler type, band)**, never a single annual number. + +5. **The band mechanism covers exactly the SEDBUK-rated wet boilers — gas and + oil — and both are currently inflated.** Mains gas, LPG and biogas **share** the + gas codes **101–119**; oil is the separate liquid-fuel range **124–132**. The + overlay today defaults **both** to their condensing/modern variant — gas 102/104 + (84 %) *and* oil 127/130 (84 %/82 %) — so the D–G over-credit exists for oil as + well as gas. Every other boiler archetype we hold has **no SEDBUK band** and is + already modelled at a sensible, non-inflated fixed efficiency, so it needs no + band and is recorded here as *intrinsic-efficiency*: + + | Boiler archetype | Base code | Efficiency | Treatment | + |---|---|---|---| + | Gas boiler, regular / combi | 102 / 104 | Table 4b, condensing | **SEDBUK band** (below) | + | Oil boiler, regular / combi | 127 / 130 | Table 4b, condensing | **SEDBUK band** (below) | + | Solid fuel boiler | 151 | Table 4a **75 %** (`cert_to_inputs.py:2309`) | intrinsic — SEDBUK doesn't rate solid fuel; a PCDB Table-122 product index refines it | + | Electric boiler | 191 | Table 4a **100 %** (direct-acting) | intrinsic — electric | + | Electric CPSU | 192 | Table 4a, electric | intrinsic — electric | + | Gas CPSU | 120 | Table 4b 74 % (non-condensing default) | intrinsic — not inflated; CPSU isn't SEDBUK-banded | + + CPSU (120–123) and range cookers (133–141) are distinct archetypes, not the + regular/combi boiler a band describes. **Solid-fuel efficiency *granularity* + (manual vs auto-feed / pellet) is an archetype question, not a band one** — the + single `Solid fuel boiler` member (all coal/wood/dual collapse to it, ADR-0067) + is a separate follow-up if finer solid-fuel efficiency is wanted. + + The per-fuel band → efficiency lookup for the two banded fuels, `(winter, summer)`: + + | Band | Gas regular / combi | Oil regular / combi | Source | + |---|---|---|---| + | A | **slot** ~(90, 80) | **slot** ~(88, 78) | SEDBUK ≥ 90 %, **needs oracle** | + | B | **slot** ~(88, 78) | **slot** ~(87, 77) | SEDBUK 86–90 %, **needs oracle** | + | C | 102 (84,74) / 104 (84,75) | 127 (84,72) / 130 (82,73) | Table 4b (accredited) | + | D | 106 (80,70) / 108 (80,71) | 126 (80,68) / 129 (77,68) | Table 4b | + | E | 101 (74,64) / 103 (74,65) | 125 (71,59) / 129 (77,68) ⚠ | Table 4b | + | F | 105 (70,60) / 107 (70,61) | 125 (71,59) / 128 (71,62) | Table 4b | + | G | 115 (66,56) / 118 (66,57) | 124 (66,54) / 128 (71,62) ⚠ | Table 4b | + + **C-and-below reuse accredited Table 4b code values** — no oracle needed. + **A/B exceed Table 4b's 84 % ceiling** (gas) / 82–84 % (oil), so they *must* + use the slot with SEDBUK-derived values — the only judgement calls, carrying + the validation obligation below. ⚠ **Oil's code set is sparser** than gas and + does not line up cleanly with SEDBUK bands (no oil code in the 74–78 % E range + for regular; nothing below ~66/71 % for G-combi), so oil D–G are *approximate* + nearest-fit — a follow-up may extend the slot to more oil bands rather than + borrow a mismatched code. + +6. **Data / migration.** Add a `boiler_efficiency_band` to the override + representation; **backfill** it for already-ingested overrides by re-parsing + `original_spreadsheet_description`, and extend the **Landlord Description → + Recognised Internal Description** classifier to capture the band going forward. + Then **re-model** the affected overridden-boiler population. + +## Considered options + +- **Code-only (reuse existing Table 4b codes per band), no slot.** Rejected as + the *sole* mechanism: Table 4b's non-PCDB gas codes cap at 84 %, so A/B cannot + rise — that silently delivers the downward-only cap, not full fidelity. Retained + for C-and-below, where accredited codes exist and no slot is required. +- **Downward-only clamp (ship "cap the over-credit" first).** Rejected: "override + wins" is symmetric; a clamp encodes a temporary policy that reads as a bug later. +- **A new archetype per band.** Rejected: explodes the FE-owned + `MainHeatingSystemType` enum. +- **Defer to the cert efficiency when it "looks fine".** Rejected: the divergent + case *is* the entire bug, and a replacement override makes the cert efficiency + stale — so the check only spares the cases that were never wrong. + +## Consequences + +- **Positive.** Over-credited D–G baselines corrected down to published Table 4b + values; A/B corrected up; the override finally encodes the efficiency the + landlord surveyed. +- **Cost / risk — the A/B upside.** It moves ~14 850 accredited baselines **up** + (~+1–3 SAP each, small per home but **systematic**); some cross a band boundary, + rippling into **Recommendations** and **funding eligibility**. Therefore the + A/B `(winter, summer)` numbers (rows A, B only) **must be validated against an + accredited Elmhurst SAP 10.2 build** before the upside ships. The C-and-below + downside uses published Table 4b values and needs no oracle. +- **Migration + re-model** of the overridden-boiler population; the stored + baselines change, so this is an operational re-base, not a silent code swap. +- Sibling data-quality reconciliation: the cheaper-direction `main_fuel` + overrides (#1701).