fix(water): don't apply heat-pump water SCOP to a separate immersion (SAP N3.7a)

When a heat-pump cert lodges a PCDB Table 362 record, the APM override
set BOTH the space efficiency (N3.6) and the water efficiency (N3.7a)
from the heat pump unconditionally. But the PCDB η_water applies only
when the DHW is heated BY the heat pump (water-heating code "from main":
901/902/914). A separate electric immersion (WHC 903) heats the water at
100% regardless of the space system, so applying the HP's water SCOP
(187.5% × 0.6 in-use = 112.5%) under-counted the immersion's hot-water
fuel.

Gate the η_water override on the DHW-from-main codes; a separate immersion
keeps its own 100% efficiency. Space η_space still always uses the APM
value (the heat pump is the space main).

Worksheet-validated to 1e-4 on simulated case 45 (HP space + WHC-903
immersion): water fuel (62) 1893.57 -> 2130.2639, total cost (255)
619.7433, CO2 692.13 — all matching the P960 exactly; SAP 60.53 -> rounds
to the worksheet's 61. RdSAP-21.0.1 corpus unchanged (no HP+WHC903 certs
in it). Pinned in test_cert_to_inputs (immersion fuel is main-independent).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-18 13:44:51 +00:00
parent 9b0c590bf8
commit 72ef0f0e7b
2 changed files with 68 additions and 1 deletions

View file

@ -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

View file

@ -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