diff --git a/domain/sap10_calculator/rdsap/cert_to_inputs.py b/domain/sap10_calculator/rdsap/cert_to_inputs.py index c8a59fcf..a271f8f5 100644 --- a/domain/sap10_calculator/rdsap/cert_to_inputs.py +++ b/domain/sap10_calculator/rdsap/cert_to_inputs.py @@ -7216,7 +7216,21 @@ def cert_to_inputs( epc=epc, ) if apm_efficiencies is not None: - eff, water_eff = apm_efficiencies + # η_space (N3.6) always replaces the Table 4a default — the heat + # pump is the space main. η_water (N3.7a) applies ONLY when the DHW + # is actually heated by that main (WHC "from main": 901/902/914). A + # separate electric immersion (WHC 903) or other independent DHW + # source keeps its own water efficiency (immersion = 100%), not the + # HP's water SCOP — otherwise a HP-space + immersion-DHW dwelling + # under-counts its hot-water fuel (case 45: water 2130 -> 1894 kWh, + # +1.5 SAP, because 187.5% × 0.6 in-use = 112.5% was applied where + # the worksheet (216) uses 100%). + eff, apm_water_eff = apm_efficiencies + if ( + epc.sap_heating.water_heating_code + in _WATER_INHERIT_FROM_MAIN_CODES + ): + water_eff = apm_water_eff if ( _is_heat_network_main(main) and epc.sap_heating.water_heating_code in _WATER_INHERIT_FROM_MAIN_CODES diff --git a/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py b/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py index 4531c08d..e3e99e9e 100644 --- a/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py +++ b/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py @@ -38,6 +38,7 @@ from datatypes.epc.domain.epc_property_data import ( from domain.sap10_ml.tests._fixtures import ( make_building_part, make_floor_dimension, + make_main_heating_detail, make_minimal_sap10_epc, make_sap_heating, make_window, @@ -7581,3 +7582,55 @@ def test_index_less_mev_applies_table_4g_note_3_default_data_iuf() -> None: # 2.5x the raw-0.8 value, not the raw default. assert fan_kwh > 0.0 assert abs(fan_kwh - expected) <= 1e-9 + + +def test_heat_pump_water_scop_not_applied_to_separate_immersion_dhw() -> None: + # Arrange — SAP 10.2 Appendix N3.7(a): a heat pump's PCDB water + # efficiency (η_water) applies to the DHW ONLY when the cylinder is + # heated BY the heat pump. A separate electric immersion (WHC 903) heats + # the water at 100% regardless of the space-heating system, so the HP's + # water SCOP must NOT leak onto it. Invariant: a WHC-903 immersion's + # hot-water fuel is INDEPENDENT of the main — a heat-pump main and a gas- + # boiler main yield the SAME immersion fuel (both 100%, no primary loss). + # Before the fix the APM override set η_water = 187.5% × 0.6 in-use = + # 112.5% on the HP cert, under-counting its immersion fuel. Worksheet- + # validated on simulated case 45: water (62) = 2130.26 kWh at η_water=100%, + # not 2130.26 / 1.125 = 1893.57. + hp_main = make_main_heating_detail( + main_fuel_type=29, # electricity + heat_emitter_type=1, + main_heating_category=4, # heat pump + main_heating_index_number=100053, # PCDB Table 362 ASHP (ECODAN 5 kW) + main_heating_data_source=1, + ) + boiler_main = make_main_heating_detail( + main_fuel_type=26, # mains gas + heat_emitter_type=1, + main_heating_category=2, # gas boiler + sap_main_heating_code=102, + ) + + def _immersion_epc(main: MainHeatingDetail) -> EpcPropertyData: + return make_minimal_sap10_epc( + total_floor_area_m2=_TYPICAL_TFA_M2, + habitable_rooms_count=4, + country_code="ENG", + has_hot_water_cylinder=True, + sap_heating=make_sap_heating( + main_heating_details=[main], + water_heating_code=903, # separate electric immersion + water_heating_fuel=30, # standard electricity + cylinder_size=2, + cylinder_insulation_type=1, + cylinder_insulation_thickness_mm=25, + ), + ) + + # Act + hp_fuel = cert_to_inputs(_immersion_epc(hp_main)).hot_water_kwh_per_yr + boiler_fuel = cert_to_inputs(_immersion_epc(boiler_main)).hot_water_kwh_per_yr + + # Assert — the immersion DHW fuel is identical whether the space main is a + # heat pump or a gas boiler (the HP water SCOP does not apply to it). + assert hp_fuel > 0.0 + assert abs(hp_fuel - boiler_fuel) <= 1e-6