From 1a6996abbbb2e231c3cd8844be8aa5eb62ec3438 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 18 May 2026 15:48:42 +0000 Subject: [PATCH] slice S-B12: water-heating eff inherits main_heating_category cascade MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The legacy water_heating_efficiency(901, main_code) returns 0.80 (gas boiler default) when sap_main_heating_code is None — even if the main system is a heat pump (category=4, efficiency 2.30). For "from main system" water codes (901/902/914), we must inherit through the FULL main-heating cascade including the category fallback. Discovered by hand-tracing cert 0320-2850 (Semi-detached bungalow, heat-pump main with no SAP code lodged, actual SAP 70, predicted 49). HW was being charged at 0.80 eff for a 2.30-eff dwelling — 2.9× too much HW fuel. 100-cert parity probe: MAE 4.66 → 4.53 (-0.13) RMSE 6.27 → 5.96 bias -0.70 → -0.57 within ±10: 94% → 95% Co-Authored-By: Claude Opus 4.7 --- .../src/domain/sap/rdsap/cert_to_inputs.py | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 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 cb8032bd..690d2368 100644 --- a/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py +++ b/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py @@ -50,7 +50,7 @@ from datatypes.epc.domain.epc_property_data import ( from domain.ml.demand import predicted_hot_water_kwh, predicted_lighting_kwh from domain.ml.sap_efficiencies import ( seasonal_efficiency, - water_heating_efficiency, + water_heating_efficiency as _legacy_water_heating_efficiency, ) from domain.sap.calculator import CalculatorInputs, WindowInput from domain.sap.tables.table_12 import co2_factor_kg_per_kwh, unit_price_p_per_kwh @@ -411,6 +411,35 @@ def _other_fuel_cost_gbp_per_kwh(prices: PriceTable) -> float: return prices.standard_electricity_p_per_kwh * _PENCE_TO_GBP +# Water-heating codes that say "inherit from the main system" — the +# `seasonal_efficiency` cascade returns 0 as a sentinel for these in the +# legacy `domain.ml.sap_efficiencies` module. We need to inherit through +# the SAME cascade the main heating uses, including the main_heating_ +# category fallback (e.g. heat pumps return 2.30 via category 4). +_WATER_INHERIT_FROM_MAIN_CODES: Final[frozenset[int]] = frozenset({901, 902, 914}) + + +def _water_efficiency_with_category_inherit( + *, + water_heating_code: Optional[int], + main_code: Optional[int], + main_category: Optional[int], + main_fuel: Optional[int], +) -> float: + """When the cert says "hot water comes from the main system" (codes + 901 / 902 / 914), inherit the main system's efficiency — and crucially + inherit the cascade that maps `main_heating_category` to a default + when `sap_main_heating_code` is None. The legacy water_heating_efficiency + only passes main_code through and so collapses heat pumps (cat 4) + + no-code lodgements into the 0.80 gas-boiler default. + """ + if water_heating_code is None: + return _legacy_water_heating_efficiency(None, main_code) + if water_heating_code in _WATER_INHERIT_FROM_MAIN_CODES: + return seasonal_efficiency(main_code, main_category, main_fuel) + return _legacy_water_heating_efficiency(water_heating_code, main_code) + + def _co2_factor_kg_per_kwh(main: Optional[MainHeatingDetail]) -> float: @@ -499,7 +528,12 @@ def cert_to_inputs( ) eff = seasonal_efficiency(main_code, main_category, main_fuel) - water_eff = water_heating_efficiency(epc.sap_heating.water_heating_code, main_code) + water_eff = _water_efficiency_with_category_inherit( + water_heating_code=epc.sap_heating.water_heating_code, + main_code=main_code, + main_category=main_category, + main_fuel=main_fuel, + ) hw_kwh = predicted_hot_water_kwh( total_floor_area_m2=epc.total_floor_area_m2, seasonal_efficiency_water=water_eff,