Model/docs/adr/0024-heating-eligibility-and-overlay.md
Khalim Conn-Kowlessar a61b6f90c9 docs: heating & hot-water eligibility, bundles, overlay (ADR-0024 + CONTEXT)
Designs the heating Recommendation Generator via /grill-with-docs. Load-bearing
decisions: HW + controls + fuel + meter fold into each competing whole-system
bundle (the legacy heating-vs-HW split double-counted); each bundle is a fixed,
real, contractor-installable end-state (ASHP via PCDB index, HHR storage via
sap_main_heating_code=409), Product stays cost-only; eligibility encodes only
physical/planning installability since the Optimiser owns the economics (the
legacy ASHP built-form / 120 m² rule is dropped — research found no
authoritative basis); the Simulation Overlay is the deepest surface yet,
spanning main_heating_details[0] + sap_heating + top-level EpcPropertyData +
sap_energy_source. Build order HHRSH -> ASHP -> boiler (deferred).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 18:51:33 +00:00

8.2 KiB

Heating & Hot-Water Eligibility, Bundles, and Overlay

The heating Recommendation Generator offers a whole-system replacement. Four decisions are load-bearing and non-obvious: that hot water, controls, fuel, and meter are folded into the heating bundle (not offered as separate or complementary measures), that each bundle is a fixed, real, contractor-installable end-state (not a derived ideal), that eligibility encodes only physical/planning installability (the Optimiser owns the economics — so the legacy ASHP built-form/floor-area rule is dropped), and that the Simulation Overlay is the deepest surface yet, spanning five locations across EpcPropertyData.

Decision

One "Heating & Hot Water" Recommendation, competing whole-system bundles, the Optimiser picks at most one. recommend_heating(epc, products, restrictions=PlanningRestrictions()) emits a single Recommendation whose mutually-exclusive Measure Options are competing system replacements:

  • high_heat_retention_storage_heaters
  • air_source_heat_pump
  • boiler_upgrade (deferred — see below)

measure_type keeps the legacy MEASURE_MAP["heating"] names so Funding/reporting key on them; the precise spec lives in each Option's description and overlay. The Recommendation is a free Optimiser candidate (in _candidate_recommendations), not a forced Measure Dependency — a bundle can lower SAP / raise cost for some baselines, and the Optimiser simply won't pick it.

Hot water, controls, fuel, and meter fold into each bundle — never separate competing or complementary measures. A heating system implies its hot-water arrangement: an ASHP brings a HWP cylinder + electric HW; HHR storage brings an off-peak immersion cylinder. Historically heating and hot water were assessed separately, which double-counted (recommend a heating upgrade that already changes HW, then also recommend a complementary HW upgrade). Each bundle's Option is therefore the whole system change at once: main heating + controls + fuel + meter + the implied HW. Standalone HW-only measures (cylinder thermostat, tank insulation for a dwelling whose main heating is already fine) and secondary heating (low-efficiency mounted heaters — residents resist removal, so an isolated concern) are separate future generators, not part of this bundle.

Each bundle is a fixed, real, contractor-installable end-state. Like loft_insulation=300 mm, each bundle encodes one representative target system as constants — chosen because Domna installs these specific products with its contractors, not as a theoretical ideal:

  • HHR storagesap_main_heating_code=409 (Table 4a) + control 2404 + off-peak meter + electric immersion cylinder; resolves efficiency by SAP code (no PCDB index).
  • ASHP → a fixed representative heat-pump PCDB index (101413) + main_heating_category=4 + control 2210 + HWP cylinder; the calculator resolves SCOP from the PCDB heat-pump record (there is no generic-SCOP path), so a real index is required.

Product stays cost-only — it prices the Option from the materials table; it does not carry the system identity. Promoting the catalogue to drive which product is a clean future change, not needed for the tracer.

Eligibility encodes only physical/legal installability; the Optimiser owns the economics. Because the Optimiser scores every offered bundle through SAP 10.2 and picks the most cost-effective package to the target, eligibility must not re-gate on economic proxies (double-gating drops options the Optimiser would correctly weigh). It encodes only what the Optimiser cannot see — can the system physically and legally be installed:

  • ASHP (research-grounded, replacing the legacy rule): offer ⇔ property_type ∈ {House, Bungalow} ∧ not listed ∧ not heritage ∧ not already ASHP/GSHP. Flats/maisonettes are not auto-offered an individual air-to-water ASHP (siting / lease / MCS-020 need a survey). A conservation area does not exclude ASHP (offered with a planning caveat) — unlike glazing, where it downgrades the measure. Floor area, fuel, fabric, and terraced/enclosed built form are not gates — the legacy built_form ∉ {enclosed terrace}floor_area > 120 m² rule is dropped (no authoritative basis; MCS-020 siting/noise is geometry the EPC cannot supply).
  • HHR storage: off-gas (not mains_gas) or currently electric / room heaters; not community heating, not already HHR/ASHP/GSHP. Legacy keyed these on clean_description string sections; the rebuild translates them to structured predicates (main_fuel_type, sap_main_heating_code, mains_gas), grounded against the example certs.

The Simulation Overlay is the deepest surface yet — five locations. A flat HeatingOverlay carries the target system identity; _fold_heating routes each non-None field to its home (mirroring how _fold_window writes flat fields into nested WindowTransmissionDetails). It writes absolute target states (replacing the system regardless of the before), across:

  1. main_heating_details[0]main_fuel_type, sap_main_heating_code or main_heating_index_number+main_heating_category, main_heating_control
  2. sap_heating (top-level) — water_heating_code, water_heating_fuel, cylinder_size / _insulation_type / _insulation_thickness_mm
  3. EpcPropertyData (top-level) — has_hot_water_cylinder
  4. sap_energy_sourcemeter_type, mains_gas

Only main_heating_details[0] (the primary system) is targeted; dual-heating dwellings (multiple main_heating_details) are out of scope for the tracer.

Build order: HHRSH → ASHP → boiler. HHRSH has clean before/after pairs across several base systems (electric storage / no-system / wrong-HW / room-heaters → one common HHR after) and is built first; ASHP follows; boiler_upgrade is deferred until a sound before/after example exists.

Considered options

  • Separate heating and hot-water Recommendations (legacy). Rejected: it double-counts, because a heating system change already determines the HW arrangement. HW folds into the bundle.
  • Catalogue-driven system identity (extend Product with PCDB index / SAP code / controls). Rejected for now: a large Product extension that diverges from the fixed-target pattern the other five generators use. A fixed representative product per bundle is simpler and matches how loft/glazing hardcode their targets; catalogue-driving is a clean later promotion.
  • Porting the legacy ASHP eligibility rule (built form + 120 m² floor area). Rejected: research found no authoritative basis — MCS-020 siting/noise depends on geometry the EPC cannot supply, and the rule contradicts its own cited evidence (EST "suitable for all property types"). The Optimiser now owns the economics the rule was proxying.
  • Treating the heating bundle as a forced Measure Dependency (like ventilation). Rejected: ventilation only ever costs SAP, so it is forced; a heating replacement is a genuine cost/benefit choice the Optimiser should make freely.

Consequences

  • EpcSimulation grows its 5th overlay surface, heating: Optional[HeatingOverlay], and the applicator gains _fold_heating — the first fold writing across main_heating_details[0], sap_heating, top-level EpcPropertyData, and sap_energy_source at once.
  • Heating is wired into the free candidate pool, priced in the catalogue + contingency table under the legacy MEASURE_MAP["heating"] names, and added to _GENERATOR_MEASURE_TYPES + harness/report.py::_triggers_for.
  • Validation uses real Elmhurst before/after certs as 1e-4 cascade pins; the same HHRSH overlay is pinned against several base-system befores → one common after, exercising the absolute-target design. Fuel-switching (gas/LPG → electricity) is the most error-prone part for the cascade and is the focus of the pins.
  • Deferred (named gaps): boiler_upgrade (pending a sound example); secondary heating (separate generator); standalone HW-only measures (separate generator — the "smaller alternative to a big upgrade" avenue); dual-heating dwellings; flats / communal-or-air-to-air ASHP; survey "caveat" flags on Options (conservation-area / terraced ASHP) — our Option model has no caveat field yet.