Slice S0380.101: HP SAP code 211-227/521-527 → main_heating_category=4 (SAP 10.2 Table 4a)

SAP 10.2 Table 4a (PDF p.165) lists "Heat pumps" as category 4 for
SAP main-heating codes:

    211-217 — ground/water source heat pumps
    221-227 — air source heat pumps (224 = ASHP 2013+, COP 1.70)
    521-527 — warm-air heat pumps

Cert 000565 Main 1 lodges `Main Heating SAP Code = 224` (ASHP 2013+)
with `PCDF boiler Reference = 0` — i.e. no PCDB Table 362 lookup is
possible. Pre-slice `_elmhurst_main_heating_category` returned None
on this path (the existing PCDB-Table-362-membership check failed),
falling through to the cascade's `_DEFAULT_PUMPS_FANS_KWH_PER_YR =
130` (incorrect — HP circulation pump's electricity is inside the
system COP per SAP 10.2 Table 4f line "Heat pumps", so the cascade
row is 0 kWh/year for category 4).

Single-line fix: after the existing PCDB-resolution branches, check
`mh.main_heating_sap_code in _HEAT_PUMP_SAP_MAIN_HEATING_CODES` and
return category 4 if so. New frozenset of HP codes (subset of the
existing `_ELECTRIC_SAP_MAIN_HEATING_CODES`).

Transient state at HEAD (cert 000565):
- main_heating_category: None → 4 ✓
- pumps_fans cascade: 255.0 → 125.0 kWh/yr (HP base 0 + flue 45 +
  solar HW 80; MEV +127.5 kWh still missing — wiring lands in
  S0380.102)
- sap_score (int): 29 ✓ EXACT preserved
- sap_score_continuous: 28.31 → 28.69 (transient drift +0.39 vs ws;
  the previously-cancelling +130 over-count is gone, restoring the
  MEV-under net negative — closes when S0380.102 lands)

Cohort safety: cohort certs 000474..000516 are gas-combi with
`sap_main_heating_code=None` (PCDB Table 105 boiler identified via
the index instead). No cohort cert affected. Cert 0380 + other
golden HP fixtures lodge category=4 via the API mapper, also
unaffected.

Per the spec citation in [[feedback-spec-citation-in-commits]] +
the standing TODO at mapper.py:4037-4043, this slice is the
category half of the coupled cert 000565 closure arc.

Pyright net-zero per touched file.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-30 15:32:51 +00:00 committed by Jun-te Kim
parent 61c0276599
commit 784e05ebbf
2 changed files with 53 additions and 10 deletions

View file

@ -1847,6 +1847,33 @@ def test_summary_000565_ext2_floor_routes_to_u_value_0p22_via_table_20_per_rdsap
assert bp_2.floor_insulation_thickness == "200mm"
def test_summary_000565_main_1_ashp_sap_code_224_routes_to_main_heating_category_4_per_sap_table_4a() -> None:
# Arrange — SAP 10.2 Table 4a (PDF p.165) "Main heating systems":
# the category column lists "Heat pumps" as category 4. Codes in
# rows 211-217 (ground/water source HP) and 221-227 (air source HP)
# and 521-527 (warm-air HP) all map to category 4.
#
# Cert 000565 Main 1 lodges `Main Heating SAP Code = 224` (Air
# source heat pump, 2013 or later — SAP 10.2 Appendix N efficiency
# row 224, COP 1.70). Without a PCDB Table 362 record (cert lodges
# `PCDF boiler Reference = 0`) the existing mapper's `_elmhurst_
# main_heating_category` returns None, which falls through to the
# cascade's `_DEFAULT_PUMPS_FANS_KWH_PER_YR = 130` (incorrect — HP
# circulation pump's electricity is inside the system COP per
# SAP 10.2 Table 4f, so the category 4 row is 0 kWh/year).
pages = _summary_pdf_to_textract_style_pages(_SUMMARY_000565_PDF)
site_notes = ElmhurstSiteNotesExtractor(pages).extract()
# Act
epc = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes)
# Assert
assert epc.sap_heating is not None
main_1 = epc.sap_heating.main_heating_details[0]
assert main_1.sap_main_heating_code == 224
assert main_1.main_heating_category == 4
def test_summary_000565_ext1_floor_above_partially_heated_routes_to_u_value_0p7_per_rdsap_10_section_5_14() -> None:
# Arrange — RdSAP 10 §5.14 (PDF p.47) "U-value of floor above a
# partially heated space":

View file

@ -3853,6 +3853,22 @@ _ELECTRIC_SAP_MAIN_HEATING_CODES: Final[frozenset[int]] = frozenset(
+ list(range(521, 528))
)
# SAP 10.2 Table 4a "Heat pumps" rows — codes whose `main_heating_
# category` is 4 per the table's category column. Used when the cert
# lodges a Table 4a SAP code but no PCDB Table 362 record (the
# preferred identifier per `_elmhurst_main_heating_category`). Subset
# of `_ELECTRIC_SAP_MAIN_HEATING_CODES`.
#
# 211-217 — ground/water source heat pumps
# 221-227 — air source heat pumps (224 = ASHP 2013+, the cert 000565
# Main 1 lodging)
# 521-527 — warm-air heat pumps
_HEAT_PUMP_SAP_MAIN_HEATING_CODES: Final[frozenset[int]] = frozenset(
list(range(211, 218))
+ list(range(221, 228))
+ list(range(521, 528))
)
class UnmappedElmhurstLabel(ValueError):
"""An Elmhurst Summary lodged a finite-enum label that the mapper
@ -4030,22 +4046,22 @@ def _elmhurst_main_heating_category(
lodged data. A PCDB index that resolves to a Table 362 record is a
heat pump (category 4) Table 362 lists heat pumps only, so
membership is the authoritative signal. A PCDB-referenced boiler on
mains/LPG gas is category 2 (gas-fired boilers). Other system types
fall through to None so the cascade applies its default pumps_fans
130 kWh/yr until extended.
mains/LPG gas is category 2 (gas-fired boilers).
TODO: route Table 4a HP SAP codes (211-227, 521-527) to
category=4 when no PCDB Table 362 record is lodged. Currently
deferred because the correct dispatch needs (a) Table 12a
high/low rate split for HP-on-E7 cost cascade and (b) Table 4f
MEV / flue-fan / solar HW pump components for pumps_fans
without both, naive category=4 dispatch overshoots cost by
£1.3k and undershoots pumps_fans by 252 kWh on cert 000565.
SAP 10.2 Table 4a (PDF p.165) lists "Heat pumps" as category 4 for
SAP codes 211-217 (ground/water source), 221-227 (air source) and
521-527 (warm-air). When a cert lodges one of these codes WITHOUT
a PCDB Table 362 record (e.g. cert 000565 Main 1: SAP code 224
+ `PCDF boiler Reference = 0`), category 4 still applies the
cascade's pumps_fans path then routes through the HP-specific 0
kWh/yr row (Table 4f) rather than the 130 kWh/yr default base.
"""
if pcdb_index is not None and heat_pump_record(pcdb_index) is not None:
return _ELMHURST_HEATING_CATEGORY_HEAT_PUMP
if pcdb_index is not None and mh.fuel_type in _ELMHURST_GAS_BOILER_FUEL_TYPES:
return _ELMHURST_HEATING_CATEGORY_GAS_BOILER
if mh.main_heating_sap_code in _HEAT_PUMP_SAP_MAIN_HEATING_CODES:
return _ELMHURST_HEATING_CATEGORY_HEAT_PUMP
return None