Accrue Table 3 primary-circuit loss for water-only heat-network cylinders 🟥

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Daniel Roth 2026-07-15 10:27:54 +00:00
parent 5f3e9f893f
commit d10b5210c6

View file

@ -1561,6 +1561,56 @@ def test_water_only_heat_network_dhw_fuel_independent_of_plant_type() -> None:
assert heat_pump == pytest.approx(boilers, abs=1e-6)
def test_water_only_heat_network_cylinder_accrues_table_3_primary_loss() -> None:
# Arrange — SAP 10.2 §4.3 (PDF p.24) scopes itself "Where hot water is
# provided by a heat network", then requires: "Primary circuit loss for
# insulated pipework and cylinderstat should be included (see Table 3)."
# A water-ONLY network (WHC 950) feeds the dwelling-side cylinder via
# primary pipework from the network connection, so the loss applies on
# the DHW source alone — whatever the SPACE main is.
#
# The space main here is warm air (Table 4a code 502, category 9),
# mirroring cert 9093 (whc 950 + warm-air main). That main is not a
# boiler, so no boiler-category branch can accrue the loss on its
# behalf: without a WHC-keyed gate the cylinder silently gets (59)m = 0.
part = make_building_part(construction_age_band="E")
warm_air_main = MainHeatingDetail(
has_fghrs=False,
main_fuel_type=26,
heat_emitter_type=1,
emitter_temperature=1,
main_heating_control=2106,
main_heating_category=9,
sap_main_heating_code=502,
)
epc = make_minimal_sap10_epc(
total_floor_area_m2=_TYPICAL_TFA_M2,
habitable_rooms_count=4,
country_code="ENG",
has_hot_water_cylinder=True,
sap_building_parts=[part],
sap_heating=make_sap_heating(
main_heating_details=[warm_air_main],
water_heating_code=950, # hot-water-only heat network
cylinder_size=2, # Normal / 110 L
cylinder_thermostat="Y",
),
)
# Act
wh_result, _ = _water_heating_worksheet_and_gains(
epc=epc,
water_efficiency_pct=100.0,
is_instantaneous=False,
primary_age="E",
pcdb_record=None,
)
# Assert — the primary circuit runs, so (59)m is billed every month.
assert wh_result is not None
assert all(m > 0.0 for m in wh_result.primary_loss_monthly_kwh)
def test_gas_boiler_main_efficiency_unchanged_by_dlf_override() -> None:
# Arrange — regression check: the DLF override only fires for heat-
# network main heating. A standard gas boiler (cat=2, code=102) must