Treat water-only heat-network primary pipework as insulated 🟥

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Daniel Roth 2026-07-15 10:29:12 +00:00
parent ef44a744a3
commit 15b78ba312

View file

@ -1611,6 +1611,74 @@ def test_water_only_heat_network_cylinder_accrues_table_3_primary_loss() -> None
assert all(m > 0.0 for m in wh_result.primary_loss_monthly_kwh)
def test_water_only_heat_network_primary_loss_matches_heat_network_main() -> None:
# Arrange — SAP 10.2 Table 3 (PDF p.160): "For heat networks apply the
# formula above with p = 1.0 and h = 3 for all months", and §4.3 (p.24)
# pins the pipework literal: "Primary circuit loss for INSULATED
# pipework". Both scope on the DHW source, so the two ways a dwelling
# can take DHW from a heat network must accrue the SAME (59)m:
#
# - WHC 901 "from main" where the space main IS the network, and
# - WHC 950, a water-only network behind a warm-air space main.
#
# Without this the water-only cert falls back to the RdSAP §3 age-band
# default (band E → p = 0.0, uninsulated), billing a heat network for
# bare primary pipework the spec says is insulated.
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,
)
heat_network_main = MainHeatingDetail(
has_fghrs=False,
main_fuel_type=20,
heat_emitter_type=1,
emitter_temperature=1,
main_heating_control=2106,
main_heating_category=6,
sap_main_heating_code=301,
)
def primary_loss_for(
main: MainHeatingDetail, water_heating_code: int
) -> tuple[float, ...]:
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=[main],
water_heating_code=water_heating_code,
cylinder_size=2, # Normal / 110 L
cylinder_thermostat="Y",
),
)
wh_result, _ = _water_heating_worksheet_and_gains(
epc=epc,
water_efficiency_pct=100.0,
is_instantaneous=False,
primary_age="E",
pcdb_record=None,
)
assert wh_result is not None
return wh_result.primary_loss_monthly_kwh
# Act — DHW from a water-only network vs DHW from a network space main.
water_only = primary_loss_for(warm_air_main, 950)
from_network_main = primary_loss_for(heat_network_main, 901)
# Assert — Table 3's heat-network row keys on the DHW source, not the
# space main, so both bill the same insulated p=1.0 / h=3 loss.
assert water_only == pytest.approx(from_network_main, abs=1e-6)
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