From c11eb46b8ab5409ad340552783ad6d3e6d06edce Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 15 Jun 2026 06:53:14 +0000 Subject: [PATCH] fix(modelling): HHR overlay sets off-peak immersion type so HW Table 13 applies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The HHR-storage HeatingOverlay (ADR-0024) added an off-peak electric immersion cylinder but never set `immersion_heating_type`, so the overlaid cert left it None. The calculator then could not resolve `immersion_single` for the SAP 10.2 Table 13 HW high-rate split and billed hot water 100% at the off-peak low rate — £127.41 vs the relodged after-cert's £169.39, overstating the overlay's SAP by +1.26 (CO2/PE matched, isolating it to the HW cost path). Add `immersion_heating_type` to HeatingOverlay, route it through `_fold_heating` (it lives on `sap_heating`), and set it to 1 (single off-peak immersion) on the HHR overlay to match the relodged reference. Closes both `test_hhr_storage_overlay_reproduces_the_relodged_after_*` cascade pins (electric-storage and no-system befores share the after). Pre-existing failure (present before this branch's recent commits), outside the handover regression gate. Full modelling suite 220 pass, pyright net- zero. Co-Authored-By: Claude Opus 4.8 --- domain/modelling/generators/heating_recommendation.py | 4 ++++ domain/modelling/scoring/overlay_applicator.py | 1 + domain/modelling/simulation.py | 5 +++++ tests/domain/modelling/test_heating_recommendation.py | 1 + 4 files changed, 11 insertions(+) diff --git a/domain/modelling/generators/heating_recommendation.py b/domain/modelling/generators/heating_recommendation.py index 410eb5e6..b10e1b76 100644 --- a/domain/modelling/generators/heating_recommendation.py +++ b/domain/modelling/generators/heating_recommendation.py @@ -64,6 +64,10 @@ _HHR_STORAGE_OVERLAY = HeatingOverlay( cylinder_insulation_type=1, cylinder_insulation_thickness_mm=120, cylinder_thermostat="Y", + # Single off-peak electric immersion — drives the SAP 10.2 Table 13 HW + # high-rate split (matches the relodged after-cert; without it the HW + # bills 100% at the low rate, +1.26 SAP over the reference). + immersion_heating_type=1, has_hot_water_cylinder=True, meter_type="Dual", ) diff --git a/domain/modelling/scoring/overlay_applicator.py b/domain/modelling/scoring/overlay_applicator.py index c11285ca..d47d84a7 100644 --- a/domain/modelling/scoring/overlay_applicator.py +++ b/domain/modelling/scoring/overlay_applicator.py @@ -98,6 +98,7 @@ _SAP_HEATING_FIELDS: tuple[str, ...] = ( "cylinder_insulation_type", "cylinder_insulation_thickness_mm", "cylinder_thermostat", + "immersion_heating_type", ) _ENERGY_SOURCE_FIELDS: tuple[str, ...] = ("meter_type", "mains_gas") diff --git a/domain/modelling/simulation.py b/domain/modelling/simulation.py index 083f8898..7d951ac5 100644 --- a/domain/modelling/simulation.py +++ b/domain/modelling/simulation.py @@ -133,6 +133,11 @@ class HeatingOverlay: cylinder_insulation_type: Optional[Union[int, str]] = None cylinder_insulation_thickness_mm: Optional[int] = None cylinder_thermostat: Optional[str] = None + # The cylinder's immersion-heater arrangement (e.g. single off-peak + # immersion = 1). Drives the SAP 10.2 Table 13 HW high-rate fraction on + # off-peak tariffs — without it the calculator cannot resolve + # `immersion_single` and bills HW 100% at the low rate. + immersion_heating_type: Optional[Union[int, str]] = None # EpcPropertyData (top-level) has_hot_water_cylinder: Optional[bool] = None # sap_energy_source diff --git a/tests/domain/modelling/test_heating_recommendation.py b/tests/domain/modelling/test_heating_recommendation.py index 5e8e1576..81b9f692 100644 --- a/tests/domain/modelling/test_heating_recommendation.py +++ b/tests/domain/modelling/test_heating_recommendation.py @@ -67,6 +67,7 @@ def test_electric_storage_dwelling_yields_an_hhr_storage_bundle() -> None: cylinder_insulation_type=1, cylinder_insulation_thickness_mm=120, cylinder_thermostat="Y", + immersion_heating_type=1, has_hot_water_cylinder=True, meter_type="Dual", )