fix(hw): direct-acting electric boiler (191) → zero primary circuit loss

SAP 10.2 Table 3 (PDF p.160) names "Direct-acting electric boiler"
verbatim in the primary-loss zero list (alongside electric immersion,
combi, CPSU, integral-vessel heat pump). RdSAP 10 §12 (p.62) classifies
SAP code 191 as the direct-acting electric boiler. Its cylinder is
immersion-heated with no primary pipework, so no primary circuit loss
applies — but `_primary_loss_applies` had no 191 branch, so a 191 main
(main_heating_category 2, "Boiler and radiators, electric") fell through
to the cat-{1,2} boiler branch and accrued ~1177 kWh/yr of phantom
primary loss on the electric-flat segment.

Validated against the cert-2474 worksheet: §4 (59) primary loss = 0,
(64) HW output 1760 (cylinder) + (64a) shower 581. Cert 2474 HW kWh
3585 → 2408; SAP 64.66 → 70.35 (the residual to the lodged 78 is an
Unknown-meter data-fidelity artifact — the register recorded meter_type=3
"Unknown" but the lodged rating used an 18-hour off-peak meter, per RdSAP
§12 / the example worksheets).

Eval mean|err| 1.720 → 1.708 (headline 45.0%, flat ±1 cert — the
electric-flat segment is dominated by the meter data-fidelity artifact).
Regression green (2448 pass incl. golden 6035 + ASHP cohort 1e-4);
pyright net-zero.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-06 21:31:27 +00:00
parent 2bc73fb08d
commit 449d8c5b95
2 changed files with 45 additions and 0 deletions

View file

@ -663,6 +663,11 @@ _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
# SAP 10.2 Table 4a "direct-acting electric boiler" (RdSAP 10 §12 p.62).
# Named in the SAP 10.2 Table 3 (PDF p.160) primary-loss zero list, so a
# 191 main feeding a cylinder incurs no primary circuit loss.
_DIRECT_ACTING_ELECTRIC_BOILER_CODE: Final[int] = 191
# 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
@ -5307,6 +5312,16 @@ def _primary_loss_applies(
# kWh/yr — zero before this branch.
if water_heating_code in _WATER_HEATING_BOILER_CIRCULATOR_CODES:
return True
# SAP 10.2 Table 3 (PDF p.160) zero-loss list names "Direct-acting
# electric boiler" verbatim. RdSAP 10 §12 (p.62) classifies SAP code
# 191 as the direct-acting electric boiler: its cylinder is immersion-
# heated with no primary pipework, so no primary loss — even though it
# lodges as main_heating_category 2 ("Boiler and radiators, electric")
# and would otherwise hit the cat-{1,2} boiler branch below. Checked
# before that branch so the electric-flat segment (cert 2474: WHC 901
# + code 191 + cylinder) no longer accrues ~1177 kWh/yr phantom loss.
if main.sap_main_heating_code == _DIRECT_ACTING_ELECTRIC_BOILER_CODE:
return False
if main.main_heating_category == 4:
if hp_record is None:
# No PCDB record → assume separate-vessel (conservative; the

View file

@ -2164,6 +2164,36 @@ 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_zero_for_direct_acting_electric_boiler() -> None:
# Arrange — SAP 10.2 Table 3 (PDF p.160) names "Direct-acting electric
# boiler" verbatim in the primary-loss zero list (alongside electric
# immersion, combi, CPSU, integral-vessel heat pump). RdSAP 10 §12
# (p.62) classifies SAP code 191 as the "direct-acting electric
# boiler", so a 191 main feeding a cylinder (WHC 901, "from main
# system") incurs NO primary circuit loss — the DHW is immersion-
# heated, with no primary pipework. The cat-{1,2} branch in
# `_primary_loss_applies` mis-fires here (main_heating_category=2),
# returning True and adding ~1177 kWh/yr of phantom primary loss to
# the cat-2 electric-flat segment (cert 2474 worksheet (59) = 0).
electric_boiler_main = MainHeatingDetail(
has_fghrs=False,
main_fuel_type=29, # electricity
heat_emitter_type=1,
emitter_temperature="NA",
main_heating_control=2106,
main_heating_category=2, # "Boiler and radiators, electric"
sap_main_heating_code=191, # direct-acting electric boiler
)
# Act — cylinder present, WHC 901 (HW from the electric boiler).
applies = _primary_loss_applies(
electric_boiler_main, True, None, water_heating_code=901,
)
# Assert — direct-acting electric boiler → Table 3 zero list → no loss.
assert applies is False
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)