S0380.229: primary loss applies for a dedicated water-heating boiler/circulator (WHS 911-931)

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) connected to a
hot water storage vessel via insulated or uninsulated pipes". The Table
4a hot-water-only codes (PDF p.166) 911 gas / 912 liquid / 913 solid
boiler-circulator + 921-931 range cooker with boiler are each a heat
generator feeding the cylinder through a primary loop.

`_primary_loss_applies` keyed only off the resolved DHW `main` — but for
these certs `_water_heating_main` returns the SPACE main (e.g. electric
storage heaters, SAP code 402, which has no primary loop), so every
boiler branch missed the gas water-boiler's primary circuit and (59)m
went to zero. New branch keys off `water_heating_code` ∈
`_WATER_HEATING_BOILER_CIRCULATOR_CODES`. 941 (electric HP for water
only) is excluded — HP DHW vessels follow the Table 3 integral-vessel
rules.

Simulated case 19 (electric storage main + WHS 911 + 210 L cylinder):
(62)m total HW demand 2493.30 → 3169.98 kWh/yr, matching the worksheet
(the missing 676.68 kWh/yr = the worksheet's (59) primary-loss annual
sum, h=5/p=0). The remaining (64)/(219) gap is the PV diverter (63b),
deferred to its own slice.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-04 18:35:12 +00:00
parent 4911c56200
commit 0f6b402345
2 changed files with 66 additions and 0 deletions

View file

@ -659,6 +659,19 @@ _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
# 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
# range cooker with boiler for water heating only. Each is a heat
# generator feeding the cylinder through a primary loop, so SAP 10.2
# Table 3 (PDF p.160) row 1 primary circuit loss applies — independent
# of the space-heating system (which for these certs is a separate main,
# e.g. electric storage heaters). 941 (electric HP for water only) is
# excluded: HP DHW vessels follow the Table 3 integral-vessel rules.
_WATER_HEATING_BOILER_CIRCULATOR_CODES: Final[frozenset[int]] = frozenset(
{911, 912, 913} | set(range(921, 932))
)
# SAP 10.2 Appendix M equation (M1): EPV = 0.8 × kWp × S × ZPV, summed
# per array. The module efficiency constant (0.8), orientation-dependent
@ -5110,6 +5123,18 @@ def _primary_loss_applies(
# kWh/yr primary loss to a system with no primary circuit at all.
if water_heating_code == _WHC_ELECTRIC_IMMERSION:
return False
# SAP 10.2 Table 3 (PDF p.160) row 1 — a dedicated "boiler/circulator
# for water heating only" (WHC 911 gas / 912 liquid / 913 solid /
# 921-931 range cooker with boiler) is a heat generator feeding the
# cylinder through a primary loop, so the loss applies regardless of
# the space-heating main. Checked off `water_heating_code` (not
# `main`) because for these certs the resolved DHW `main` is the
# SPACE main (e.g. an electric storage heater, SAP code 402) — the
# gas/oil water boiler isn't a `main_heating_detail`. Simulated case
# 19 (storage main + WHS 911 + 210 L cylinder): worksheet (59) = 676.68
# kWh/yr — zero before this branch.
if water_heating_code in _WATER_HEATING_BOILER_CIRCULATOR_CODES:
return True
if main.main_heating_category == 4:
if hp_record is None:
# No PCDB record → assume separate-vessel (conservative; the

View file

@ -60,6 +60,7 @@ from domain.sap10_calculator.rdsap.cert_to_inputs import (
_is_off_peak_meter, # pyright: ignore[reportPrivateUsage]
_main_floor_u_value, # pyright: ignore[reportPrivateUsage]
_other_fuel_cost_gbp_per_kwh, # pyright: ignore[reportPrivateUsage]
_primary_loss_applies, # pyright: ignore[reportPrivateUsage]
_rdsap_extract_fans_default, # pyright: ignore[reportPrivateUsage]
_pv_overshading_factor, # pyright: ignore[reportPrivateUsage]
_pv_pitch_deg, # pyright: ignore[reportPrivateUsage]
@ -1956,6 +1957,46 @@ 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_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)
# connected to a hot water storage vessel via insulated or
# uninsulated pipes". The dedicated "boiler/circulator for water
# heating only" water-heating codes (Table 4a hot-water section, PDF
# p.166): 911 gas, 912 liquid fuel, 913 solid fuel, 921-931 range
# cooker with boiler — each is a boiler feeding the cylinder through a
# primary loop, so the loss applies regardless of what the SPACE
# heating system is. Simulated case 19 pairs electric storage heaters
# (SAP code 402) for space with a WHS 911 gas boiler/circulator for
# water: `_water_heating_main` resolves to the code-402 storage main
# (electric, no primary loop), so before this slice every dedicated-
# boiler branch missed the cylinder's primary circuit and (59)m went
# to zero — dropping the worksheet's 676.68 kWh/yr (59) and inflating
# HW fuel (219).
storage_heater_main = MainHeatingDetail(
has_fghrs=False,
main_fuel_type=30, # electricity
heat_emitter_type="",
emitter_temperature=1,
main_heating_control=2402,
main_heating_category=None,
sap_main_heating_code=402, # electric storage heaters (space)
)
# Act
applies_with_cylinder = _primary_loss_applies(
storage_heater_main, True, None, water_heating_code=911,
)
applies_without_cylinder = _primary_loss_applies(
storage_heater_main, False, None, water_heating_code=911,
)
# Assert — WHS 911 + cylinder → primary loss applies; no cylinder →
# no primary circuit, no loss.
assert applies_with_cylinder is True
assert applies_without_cylinder is False
def test_water_efficiency_uses_table_4a_water_column_for_heat_pumps_per_sap_10_2() -> None:
# Arrange — SAP 10.2 Table 4a (PDF p.163-164) gives heat pumps two
# efficiency columns: "space" and "water". For low-temperature