From d004dc3f5cc21afc594f942d71ae7e209f9e4fe3 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 18 May 2026 16:02:51 +0000 Subject: [PATCH] slice S-B13: skip cylinder + primary HW losses for instantaneous systems MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Water heating codes 907 (single-point gas) and 909 (electric instantaneous) describe no-cylinder, point-of-use systems with no primary circuit. The predicted_hot_water_kwh model was adding 366 kWh cylinder-storage loss + 245 kWh primary-pipework loss on top of useful demand for these certs — over-counting HW by 600+ kWh. Discovered hand-tracing cert 2903-8339 (11m² Top-floor flat studio, water_heating_code=909, actual SAP 75, predicted 55). 100-cert parity probe: MAE 4.53 → 4.48 (-0.05) RMSE 5.96 → 5.81 bias -0.57 → -0.52 Smaller MAE delta than S-B12 because instantaneous-HW certs are a smaller subset, but the affected dwellings are exactly the worst- residual tail. Co-Authored-By: Claude Opus 4.7 --- .../src/domain/sap/rdsap/cert_to_inputs.py | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py b/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py index 690d2368..4a13fa6a 100644 --- a/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py +++ b/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py @@ -128,6 +128,12 @@ _DEFAULT_THERMAL_MASS_PARAMETER_KJ_PER_M2_K: Final[float] = 250.0 _DEFAULT_PUMPS_FANS_KWH_PER_YR: Final[float] = 130.0 +# Water-heating codes for instantaneous (no-cylinder) systems — SAP §4 +# Appendix J skips cylinder-storage + primary-pipework losses for these +# because there's no cylinder and no primary circuit. +_INSTANTANEOUS_WATER_CODES: Final[frozenset[int]] = frozenset({907, 909}) + + @dataclass(frozen=True) class PriceTable: """Seam between the spec-correct SAP 10.2/10.3 Table 12 prices and @@ -534,13 +540,22 @@ def cert_to_inputs( main_category=main_category, main_fuel=main_fuel, ) + is_instantaneous = epc.sap_heating.water_heating_code in _INSTANTANEOUS_WATER_CODES hw_kwh = predicted_hot_water_kwh( total_floor_area_m2=epc.total_floor_area_m2, seasonal_efficiency_water=water_eff, - cylinder_size=_int_or_none(epc.sap_heating.cylinder_size), - cylinder_insulation_thickness_mm=epc.sap_heating.cylinder_insulation_thickness_mm, - cylinder_insulation_type=_int_or_none(epc.sap_heating.cylinder_insulation_type), - age_band=primary_age, + # Instantaneous HW systems (codes 907/909) have no cylinder and + # no primary circuit — pass None for both to suppress storage + # and primary losses in the demand model. + cylinder_size=None if is_instantaneous else _int_or_none(epc.sap_heating.cylinder_size), + cylinder_insulation_thickness_mm=( + None if is_instantaneous else epc.sap_heating.cylinder_insulation_thickness_mm + ), + cylinder_insulation_type=( + None if is_instantaneous + else _int_or_none(epc.sap_heating.cylinder_insulation_type) + ), + age_band=None if is_instantaneous else primary_age, has_wwhrs=False, has_solar_water_heating=epc.solar_water_heating, )