Price water-only heat-network DHW without a plant-efficiency divisor 🟩

SAP 10.2 §4.3 (p.24) puts the water-heating efficiency in the Table 12
heat price, and C4 (p.53) scopes the plant efficiency to CO2/PE only, so
worksheet (310) = (64) × (305a) × (306) carries no efficiency term. The
dwelling-side divisor was applying it a second time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Daniel Roth 2026-07-15 10:24:34 +00:00
parent bf4af89b3a
commit 353ebe5556
2 changed files with 46 additions and 19 deletions

View file

@ -1187,6 +1187,13 @@ _HEAT_NETWORK_DHW_CHARGING_FACTOR_BY_CODE: Final[dict[int, float]] = {
2306: 1.00, 2308: 1.00, 2309: 1.00, 2310: 1.00, 2312: 1.00, 2314: 1.00,
}
# SAP 10.2 Table 4c(3) (PDF p.169) worksheet (305a), "DHW-only system,
# flat rate charging" row — the RdSAP 10 (spec p.59) default for a
# water-ONLY heat network (WHC 950/951/952), whose own charging method is
# never lodged: the dwelling's heating-control code describes the SPACE
# system's charging, not this network's.
_TABLE_4C3_FLAT_RATE_CHARGING_FACTOR: Final[float] = 1.05
def _heat_network_space_charging_factor(main: Optional[MainHeatingDetail]) -> float:
"""SAP 10.2 Table 4c(3) (PDF p.169) worksheet (305) — heat-network
@ -8001,13 +8008,28 @@ def cert_to_inputs(
* _heat_network_dhw_charging_factor(main)
)
elif epc.sap_heating.water_heating_code in _WATER_HEAT_NETWORK_ONLY_CODES:
# HW-only heat network (whc 950/951/952): the Table 4a plant
# efficiency is already in `water_eff`; apply the Table 12c
# distribution loss on top per RdSAP 10 §10 (spec p.36 "water
# heating only ... distribution loss"). q_generated = q_useful ×
# DLF, so delivered-per-fuel efficiency = plant_eff / DLF. Fires
# on the WHC alone — the HW network is independent of the main.
water_eff = water_eff / _heat_network_dlf(primary_age)
# HW-only heat network (whc 950/951/952). SAP 10.2 §4.3 (p.24):
# "The efficiency for water heating is incorporated in the price of
# heat from a heat network in Table 12" — the dwelling buys
# DELIVERED heat (fuel 51), so the Table 4a plant efficiency must
# NOT divide here as well. C4 (p.53) scopes it: "For heat networks
# not listed in the PCDB, for the calculation of CO2 emissions and
# Primary Energy, the efficiency to be used is that of the heat
# generator" — CO2/PE only, never delivered energy.
#
# C6 "Heat network providing DHW only" (p.54): "Allow for the
# control factor from Table 4c(3), for the distribution loss factor
# as in C3.1" — i.e. worksheet (310) = (64) × (305a) × (306), with
# no efficiency term. Same shape as the WHC-901/902/914 branch
# above, differing only in that (305a) takes the Table 4c(3)
# DHW-only flat-rate default rather than the space system's lodged
# charging code. Fires on the WHC alone — §4.3 keys on the DHW
# source ("Where hot water is provided by a heat network"), not on
# the space main.
water_eff = 1.0 / (
_heat_network_dlf(primary_age)
* _TABLE_4C3_FLAT_RATE_CHARGING_FACTOR
)
is_instantaneous = epc.sap_heating.water_heating_code in _INSTANTANEOUS_WATER_CODES
# §9a Table 11 secondary fraction — pulled forward of §4 so the
# post-§8 Equation D1 cascade can derive Q_space = (98c)m × (204)

View file

@ -1399,17 +1399,22 @@ def test_heat_network_main_with_hw_from_main_dlf_scales_hot_water_kwh() -> None:
assert abs(hn_hw_fuel / wh_result.output_kwh_per_yr - 1.41) <= 1e-6
def test_hot_water_only_heat_network_whc_950_applies_table12c_dlf() -> None:
def test_hot_water_only_heat_network_whc_950_costs_more_fuel_than_boiler_dhw() -> None:
# Arrange — water_heating_code 950 = "hot-water-only heat network
# (boilers)" (SAP 10.2 Table 4a HW section, plant eff 0.80). RdSAP 10
# §10 (spec p.36) requires the Table 12c distribution loss applied on
# top of the plant efficiency for water-heating-only heat networks, so
# the delivered HW fuel = q_useful × DLF / 0.80. The DLF must fire on
# the WHC ALONE — independent of the main, which here is an ordinary
# gas boiler (NOT a heat network), mirroring cert 9093 (whc 950 + a
# warm-air main). Compare against a non-heat-network baseline at the
# same 0.80 water efficiency (whc 901 from the same gas main): the 950
# HW fuel must exceed it by exactly the DLF (age E → 1.41).
# (boilers)". The Table 12c distribution loss must fire on the WHC
# ALONE — independent of the main, which here is an ordinary gas boiler
# (NOT a heat network), mirroring cert 9093 (whc 950 + a warm-air main).
# Compared against the SAME dwelling taking DHW from that gas boiler
# (whc 901, Table 4a eff 0.80), the water-only network bills:
#
# 950: q_useful × (306) DLF 1.41 × (305a) 1.05 = q × 1.4805
# 901: q_useful ÷ 0.80 = q × 1.25
#
# → the network's delivered fuel is 1.4805/1.25 ≈ 1.18× the boiler's.
# The network pays a distribution loss and a flat-rate charging penalty
# but NO plant-efficiency divisor (SAP 10.2 §4.3 p.24: that efficiency
# is in the Table 12 heat price) — see the absolute-efficiency pin in
# `test_water_only_heat_network_dhw_prices_heat_without_plant_efficiency`.
part = make_building_part(construction_age_band="E")
gas_main = MainHeatingDetail(
has_fghrs=False,
@ -1445,8 +1450,8 @@ def test_hot_water_only_heat_network_whc_950_applies_table12c_dlf() -> None:
hw_network: float = cert_to_inputs(hw_network_epc).hot_water_kwh_per_yr
baseline: float = cert_to_inputs(baseline_epc).hot_water_kwh_per_yr
# Assert — the HW-only-heat-network fuel is scaled up by the age-E DLF.
assert abs(hw_network / baseline - 1.41) <= 0.02
# Assert — DLF × flat-rate charging, against the boiler's 1/0.80.
assert abs(hw_network / baseline - (1.41 * 1.05) * 0.80) <= 0.02
def test_water_only_heat_network_dhw_prices_heat_without_plant_efficiency() -> None: