Slice S0380.162: SAP 10.2 Appendix N3.1 default pump gain for electric HPs

SAP 10.2 Appendix N3.1 (PDF p.105) "Circulation pump and fan":
"For electric heat pumps: The electricity used by the water
circulation pump or fan is included within the calculated annual
space and hot water heating efficiency and is not included in
worksheet (230c). **The default heat gain from Table 5a is included
via worksheet (70).**"

This rule applies the Table 5a row "Central heating pump in heated
space" GAIN (3 / 10 / 7 W per pump-age bucket) to electric heat
pumps even though the pump ELECTRICITY is hidden in the COP and
excluded from (230c). The "Not applicable for electric heat pumps
from database" clause in Table 5a footnote a) scopes only to the
PCDB-Table-362 cascade case (Appendix N1.2.1: "For heat pumps held
in the PCDB ... a single water circulation pump serving the heat
emitters is sufficient" — pump kWh AND gain embedded in COP).

S0380.160 over-stripped the gain by zeroing pump_w for every HP
category-4 main, conflating the PCDB-Table-362 case with the Table-4a
default cascade. This slice refines the HP gate in
`_any_main_system_has_central_heating_pump`:
  - Cat 4 HP WITH `main_heating_index_number` lodged (PCDB Table
    362) → continue (skip; pump in COP per N1.2.1);
  - Cat 4 HP with SAP code in `_TABLE_4A_WARM_AIR_SAP_CODES` (Cat 5
    warm-air HPs distribute via ducted air, no water circulation
    pump; warm-air fan handled separately by Table 5a "Warm air
    heating system fans" row, S0380.161) → continue;
  - Otherwise (Cat 4 HP, Table 4a default cascade, water-emitter)
    → apply Table 5a default per Appendix N3.1.

Per-line walk on ashp (SAP code 214 air-to-water HP, Cat 4, no PCDB,
"Post 2013" pump age):
  worksheet (70)[Jan] = 3.0000 W
  cascade pre-slice    = 0.0000 W      delta = -3.000 W
The -3 W winter gain shortfall over-stated cascade (84) Total gains
by -3 W in heating months → cascade SH demand +12.27 kWh/yr
(cascade 9302 vs worksheet 9290), pushing continuous SAP down 0.024
because the cost residual was driven by the +1.5 kWh × 12 month
shortfall flowing through the £0.0741 low-rate cost.

Closures:
  ashp:  ΔSAP -0.0240 → +0.0000 EXACT, Δcost +£0.55 → +£0.00 EXACT
  gshp:  ΔSAP -0.0178 → -0.0000 EXACT, Δcost +£0.41 → -£0.00 EXACT

ΔPE +36 → +25.51 (and ΔCO2 +7.33 → +6.31) — residuals narrow to the
Elmhurst-vs-spec HW PE annual-vs-monthly Table 12e/12d quirk only
(same pattern as the 16-variant lighting-PE deferred cohort,
scaled by HW kWh = 1138 vs 2384 → 25.51 vs 48.66). Cohort
Σ |ΔSAP_c| 0.07 → 0.03; all 25 cascade-OK variants now SAP+cost EXACT.

Cohort-1 (cert 0380 et al.) golden fixtures unaffected — those certs
lodge `main_heating_index_number` (PCDB Table 362) → HP gate skips
correctly → (70) = 0 preserved. Cert 000565 (HP main 1 + gas boiler
main 2) unaffected — wet-boiler branch fires for main 2.

Verbatim spec quote (SAP 10.2 Appendix N3.1, PDF p.105):
  "For electric heat pumps: The electricity used by the water
   circulation pump or fan is included within the calculated annual
   space and hot water heating efficiency and is not included in
   worksheet (230c). The default heat gain from Table 5a is
   included via worksheet (70)."

Tests: 906 pass (+1), 0 fail. Pyright net-zero (35 → 35).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-01 23:59:29 +00:00 committed by Jun-te Kim
parent b3196cdcf5
commit 4d0e2ed6cf
3 changed files with 108 additions and 3 deletions

View file

@ -334,8 +334,24 @@ class _CorpusExpectation:
# Electric 2 SAP -0.1087 → -0.0000 EXACT; joins the lighting-PE
# deferred cohort (CO2 +11.95 / PE +48.66). Cohort Σ|ΔSAP_c|
# 0.18 → 0.07 in one slice.
#
# Slice S0380.162 closed ashp + gshp by restoring the SAP 10.2
# Appendix N3.1 (PDF p.105) "default heat gain from Table 5a is
# included via worksheet (70)" rule for electric heat pumps that DON'T
# have a PCDB Table 362 record lodged. S0380.160 had over-stripped
# the gain (zeroed for all HPs); ashp/gshp use Table 4a Cat 4 default
# cascade and worksheet (70) = 3.0000 W in heating months. Refined
# `_any_main_system_has_central_heating_pump` HP gate: PCDB-lodged
# HPs (e.g. cert 0380 cohort with Table 362 record) keep 0 W (pump
# embedded in COP per N1.2.1); Cat 5 warm-air HPs keep 0 W (no water
# circulation pump; warm-air fan handled by .161); Cat 4 HPs without
# PCDB and not warm-air → apply pump gain per N3.1. ashp/gshp ΔSAP
# -0.024/-0.018 → -0.0000 EXACT; ΔPE +36/+34 → +25.51 (residual
# narrowed to the Elmhurst-vs-spec HW PE annual-vs-monthly quirk
# only). Cohort Σ|ΔSAP_c| 0.07 → 0.03 in one slice. All 25 cascade-OK
# variants now SAP+cost EXACT.
_EXPECTATIONS: tuple[_CorpusExpectation, ...] = (
_CorpusExpectation(variant='ashp', block='11a', expected_sap_resid=-0.0240, expected_cost_resid_gbp=+0.5536, expected_co2_resid_kg=+7.3267, expected_pe_resid_kwh=+36.3435),
_CorpusExpectation(variant='ashp', block='11a', expected_sap_resid=+0.0000, expected_cost_resid_gbp=+0.0000, expected_co2_resid_kg=+6.3106, expected_pe_resid_kwh=+25.5090),
_CorpusExpectation(variant='electric 1', block='11a', expected_sap_resid=-0.0000, expected_cost_resid_gbp=-0.0000, expected_co2_resid_kg=+11.9451, expected_pe_resid_kwh=+48.6605),
_CorpusExpectation(variant='electric 2', block='11a', expected_sap_resid=-0.0000, expected_cost_resid_gbp=-0.0000, expected_co2_resid_kg=+11.9451, expected_pe_resid_kwh=+48.6604),
_CorpusExpectation(variant='electric 3', block='11a', expected_sap_resid=-0.0000, expected_cost_resid_gbp=+0.0000, expected_co2_resid_kg=+11.9451, expected_pe_resid_kwh=+48.6604),
@ -344,7 +360,7 @@ _EXPECTATIONS: tuple[_CorpusExpectation, ...] = (
_CorpusExpectation(variant='electric 7', block='11a', expected_sap_resid=-0.0000, expected_cost_resid_gbp=+0.0000, expected_co2_resid_kg=+11.9452, expected_pe_resid_kwh=+48.6605),
_CorpusExpectation(variant='electric 8', block='11a', expected_sap_resid=-0.0000, expected_cost_resid_gbp=-0.0000, expected_co2_resid_kg=+11.9451, expected_pe_resid_kwh=+48.6604),
_CorpusExpectation(variant='electric 9', block='11a', expected_sap_resid=+0.0000, expected_cost_resid_gbp=+0.0000, expected_co2_resid_kg=+11.9452, expected_pe_resid_kwh=+48.6605),
_CorpusExpectation(variant='gshp', block='11a', expected_sap_resid=-0.0178, expected_cost_resid_gbp=+0.4092, expected_co2_resid_kg=+7.0616, expected_pe_resid_kwh=+33.5171),
_CorpusExpectation(variant='gshp', block='11a', expected_sap_resid=-0.0000, expected_cost_resid_gbp=-0.0000, expected_co2_resid_kg=+6.3106, expected_pe_resid_kwh=+25.5090),
_CorpusExpectation(variant='oil 1', block='11a', expected_sap_resid=-0.0000, expected_cost_resid_gbp=-0.0000, expected_co2_resid_kg=+0.0000, expected_pe_resid_kwh=+0.0000),
_CorpusExpectation(variant='oil pcdb 1', block='11a', expected_sap_resid=+0.0000, expected_cost_resid_gbp=+0.0000, expected_co2_resid_kg=-0.0000, expected_pe_resid_kwh=+0.0000),
_CorpusExpectation(variant='oil pcdb 2', block='11a', expected_sap_resid=+0.0000, expected_cost_resid_gbp=+0.0000, expected_co2_resid_kg=-0.0000, expected_pe_resid_kwh=+0.0000),

View file

@ -714,13 +714,35 @@ def _any_main_system_has_central_heating_pump(epc: EpcPropertyData) -> bool:
Mirrors `cert_to_inputs._is_wet_boiler_main` see docstring there
for the kWh-side parallel in Table 4f.
Electric heat pump exception per SAP 10.2 Appendix N3.1 (PDF p.105):
"For electric heat pumps: The electricity used by the water
circulation pump or fan is included within the calculated annual
space and hot water heating efficiency and is not included in
worksheet (230c). **The default heat gain from Table 5a is included
via worksheet (70).**" → Cat 4 HPs WITHOUT a PCDB record (Table 4a
default cascade) get the Table 5a default pump gain. Cat 4 HPs
WITH a PCDB record (Table 362) embed the pump gain in the COP
no separate Table 5a gain. Cat 5 warm-air HPs (codes 521/523-527)
distribute via fans, not a water pump handled by the warm-air
fan row of Table 5a (see `_any_main_system_has_warm_air_distribution`).
"""
details = epc.sap_heating.main_heating_details
if not details:
return False
for d in details:
if d.main_heating_category == _HEAT_PUMP_MAIN_HEATING_CATEGORY:
continue
# PCDB Table 362 record → pump electricity AND gain are
# embedded in COP (Appendix N1.2.1); no separate gain row.
if d.main_heating_index_number is not None:
continue
# Cat 5 warm-air HP (codes 521/523-527) → no water pump.
code = d.sap_main_heating_code
if code is not None and code in _TABLE_4A_WARM_AIR_SAP_CODES:
continue
# Cat 4 HP, Table 4a default cascade → apply Table 5a
# pump gain per Appendix N3.1.
return True
code = d.sap_main_heating_code
if code is not None and any(
code in r for r in _WET_BOILER_SAP_CODE_RANGES

View file

@ -575,6 +575,73 @@ def test_internal_gains_from_cert_reproduces_000490_worksheet_end_to_end() -> No
assert result.total_internal_gains_monthly_w[m] == pytest.approx(expected_73[m], abs=1e-1), f"(73) month {m+1}"
def test_internal_gains_pumps_fans_applies_3w_pump_gain_for_cat4_hp_without_pcdb() -> None:
"""SAP 10.2 Appendix N3.1 (PDF p.105) — "For electric heat pumps:
The electricity used by the water circulation pump or fan is
included within the calculated annual space and hot water heating
efficiency and is not included in worksheet (230c). The default
heat gain from Table 5a is included via worksheet (70)."
The pump GAIN is included via Table 5a's "Central heating pump in
heated space" row even though the pump ELECTRICITY is hidden in
the COP. Worksheet evidence (controlled-variable corpus):
- ashp (Cat 4 HP, code 214 air-to-water, "Post 2013" pump,
no PCDB record): (70) = 3.0000 W heating-mask
- gshp (Cat 4 HP, code 211 ground-source, "Post 2013" pump,
no PCDB record): (70) = 3.0000 W heating-mask
PCDB Table 362 HP records embed the pump in the COP including the
gain for those certs (70) = 0 (e.g. cert 0380 cohort). Cat 5
warm-air HPs (codes 521/523-527) have no water circulation pump;
their fan gain is separately handled via the Table 5a "Warm air
heating system fans" row (S0380.161).
Pre-slice S0380.160 zeroed pump gain for all HPs; this test forces
a finer-grained gate: Cat 4 HP + no PCDB record + non-warm-air
code apply the pump gain.
"""
# Arrange — Cat 4 ASHP (code 214) without PCDB index, "Post 2013"
# pump age → 3 W per Table 5a.
sap_heating = SapHeating(
instantaneous_wwhrs=InstantaneousWwhrs(),
main_heating_details=[
MainHeatingDetail(
has_fghrs=False,
main_fuel_type=30,
heat_emitter_type=1, # radiators
emitter_temperature=1,
sap_main_heating_code=214,
main_heating_category=4, # HP
main_heating_control=2106,
central_heating_pump_age_str="Post 2013",
),
],
has_fixed_air_conditioning=False,
)
epc = make_minimal_sap10_epc(
total_floor_area_m2=90.0,
low_energy_fixed_lighting_bulbs_count=6,
sap_windows=[],
sap_heating=sap_heating,
)
# Act
result = internal_gains_from_cert(
epc=epc,
dwelling_volume_m3=227.25,
heat_gains_from_water_heating_monthly_kwh=(0.0,) * 12,
overshading=OvershadingCategory.AVERAGE,
)
# Assert — 3 W in heating months (Jan-May, Oct-Dec), 0 in summer.
expected = (3.0, 3.0, 3.0, 3.0, 3.0, 0.0, 0.0, 0.0, 0.0, 3.0, 3.0, 3.0)
for m in range(12):
assert abs(result.pumps_fans_monthly_w[m] - expected[m]) <= 1e-9, (
f"(70) month {m+1} = {result.pumps_fans_monthly_w[m]:.4f}, "
f"expected {expected[m]:.4f}"
)
def test_internal_gains_pumps_fans_adds_warm_air_fan_gain_for_cat5_hp_main() -> None:
"""SAP 10.2 Table 5a (PDF p.177) row "Warm air heating system fans a) c)"
gain = SFP × 0.04 × V (W). Default SFP = 1.5 W/(l/s) per footnote c