diff --git a/domain/sap10_calculator/rdsap/cert_to_inputs.py b/domain/sap10_calculator/rdsap/cert_to_inputs.py index 7afbd5d4..f39b006d 100644 --- a/domain/sap10_calculator/rdsap/cert_to_inputs.py +++ b/domain/sap10_calculator/rdsap/cert_to_inputs.py @@ -663,6 +663,11 @@ _INSTANTANEOUS_WATER_CODES: Final[frozenset[int]] = frozenset({907, 909}) # zero-loss list, so primary loss is zero whenever this code is lodged. _WHC_ELECTRIC_IMMERSION: Final[int] = 903 +# SAP 10.2 Table 4a "direct-acting electric boiler" (RdSAP 10 §12 p.62). +# Named in the SAP 10.2 Table 3 (PDF p.160) primary-loss zero list, so a +# 191 main feeding a cylinder incurs no primary circuit loss. +_DIRECT_ACTING_ELECTRIC_BOILER_CODE: Final[int] = 191 + # Water-heating codes for a dedicated "boiler/circulator for water # heating only" — SAP 10.2 Table 4a hot-water section (PDF p.166): # 911 gas, 912 liquid fuel, 913 solid fuel boiler/circulator; 921-931 @@ -5307,6 +5312,16 @@ def _primary_loss_applies( # kWh/yr — zero before this branch. if water_heating_code in _WATER_HEATING_BOILER_CIRCULATOR_CODES: return True + # SAP 10.2 Table 3 (PDF p.160) zero-loss list names "Direct-acting + # electric boiler" verbatim. RdSAP 10 §12 (p.62) classifies SAP code + # 191 as the direct-acting electric boiler: its cylinder is immersion- + # heated with no primary pipework, so no primary loss — even though it + # lodges as main_heating_category 2 ("Boiler and radiators, electric") + # and would otherwise hit the cat-{1,2} boiler branch below. Checked + # before that branch so the electric-flat segment (cert 2474: WHC 901 + # + code 191 + cylinder) no longer accrues ~1177 kWh/yr phantom loss. + if main.sap_main_heating_code == _DIRECT_ACTING_ELECTRIC_BOILER_CODE: + return False if main.main_heating_category == 4: if hp_record is None: # No PCDB record → assume separate-vessel (conservative; the 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 a21ee098..4b2d13c3 100644 --- a/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py +++ b/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py @@ -2164,6 +2164,36 @@ def test_secondary_electric_off_peak_bills_at_table_12a_direct_acting_high_rate( assert abs(secondary_rate_gbp_per_kwh - 0.1529) <= 1e-6 +def test_sap_table_3_primary_loss_zero_for_direct_acting_electric_boiler() -> None: + # Arrange — SAP 10.2 Table 3 (PDF p.160) names "Direct-acting electric + # boiler" verbatim in the primary-loss zero list (alongside electric + # immersion, combi, CPSU, integral-vessel heat pump). RdSAP 10 §12 + # (p.62) classifies SAP code 191 as the "direct-acting electric + # boiler", so a 191 main feeding a cylinder (WHC 901, "from main + # system") incurs NO primary circuit loss — the DHW is immersion- + # heated, with no primary pipework. The cat-{1,2} branch in + # `_primary_loss_applies` mis-fires here (main_heating_category=2), + # returning True and adding ~1177 kWh/yr of phantom primary loss to + # the cat-2 electric-flat segment (cert 2474 worksheet (59) = 0). + electric_boiler_main = MainHeatingDetail( + has_fghrs=False, + main_fuel_type=29, # electricity + heat_emitter_type=1, + emitter_temperature="NA", + main_heating_control=2106, + main_heating_category=2, # "Boiler and radiators, electric" + sap_main_heating_code=191, # direct-acting electric boiler + ) + + # Act — cylinder present, WHC 901 (HW from the electric boiler). + applies = _primary_loss_applies( + electric_boiler_main, True, None, water_heating_code=901, + ) + + # Assert — direct-acting electric boiler → Table 3 zero list → no loss. + assert applies is False + + def test_sap_table_3_primary_loss_applies_to_dedicated_water_heating_boiler_circulator() -> None: # Arrange — SAP 10.2 Table 3 (PDF p.160) row 1: primary circuit loss # applies when "hot water is heated by a heat generator (e.g. boiler)