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, )