Slice S0380.2: surface main_heating_category=4 for PCDB heat-pump indices

Extends `_elmhurst_main_heating_category` in
`datatypes/epc/domain/mapper.py` so a PCDB index that resolves to a
Table 362 record (heat pumps only) yields category 4 — the SAP 10.2
Table 4a code that gates the Appendix N3.6/N3.7 heat-pump cascade
(`cert_to_inputs.py` lines 1896, 2005, 2057, 2104 all branch on
`main_heating_category == 4`).

Authoritative signal: PCDB Table 362 is heat-pumps-only, so
membership IS the heat-pump answer. `heat_pump_record(pcdb_id)`
(introduced for the API path's cohort closure) returns the typed
record or None; a non-None return is sufficient. No fuel-type
belt-and-braces is needed — Table 362 membership is unambiguous,
unlike the gas-boiler branch which uses fuel type to disambiguate
PCDB Table 105 records.

Forcing function (Slice S0380.1): cert 0380 Summary cascade SAP
moves from 33.7920 (Δ -54.7184) to 81.7528 (Δ -6.7576) — closes
~88% of the gap. Remaining -6.76 SAP is the next workstream:
cylinder / HW cascade, PV array surfacing, secondary-heating routing
(per HANDOVER_CERT_0380_SUMMARY_PATH.md debug order steps 3–4).

Added focused unit test
`test_summary_0380_main_heating_category_is_heat_pump` that pins the
contract at the mapper boundary (idx 104568 → category 4), so future
debuggers can localise regressions before walking the full chain.

Architectural note: introduces the first
`datatypes/epc/domain/mapper.py → domain/sap10_calculator/tables/pcdb`
import. PCDB is BRE reference data shared by both layers; treating it
as importable shared reference is the lighter alternative to either
(a) duplicating an HP-PCDB-IDs frozenset in the mapper or (b) hoisting
PCDB into a new shared package.

Pyright baseline preserved:
  datatypes/epc/domain/mapper.py: 32 errors (no new errors introduced)
  backend/documents_parser/tests/test_summary_pdf_mapper_chain.py: 0 errors

Regression suite: 670 pass + 11 fail (vs handover baseline 669 + 10 —
net +1 pass for the new GREEN unit test, +1 fail still being the
Slice 1 chain test that this slice does not yet fully close).

Spec refs:
- SAP 10.2 Table 4a (main heating category codes — code 4 = heat pump)
- SAP 10.2 Appendix N3.6/N3.7 (heat-pump space-heating efficiency
  with PSR interpolation, routed via the category-4 gate)
- BRE PCDB Table 362 (heat-pump records — pcdb_id 104568 = Mitsubishi
  Ecodan PUZ-WM50VHA, the cert 0380 main heating appliance)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-27 17:56:30 +00:00 committed by Jun-te Kim
parent 2828bf988d
commit 19e23d0c31
2 changed files with 40 additions and 10 deletions

View file

@ -492,6 +492,28 @@ def test_summary_0330_full_chain_sap_matches_worksheet_pdf_exactly() -> None:
assert abs(result.sap_score_continuous - worksheet_unrounded_sap) < 1e-4
def test_summary_0380_main_heating_category_is_heat_pump() -> None:
# Arrange — cert 0380's Summary lodges main heating as a PCDB-
# indexed Mitsubishi PUZ-WM50VHA (idx 104568), which lives in
# PCDB Table 362 (heat pumps only). The Elmhurst mapper must
# surface `main_heating_category=4` so the cascade routes the
# cert through the Appendix N3.6/N3.7 heat-pump path instead of
# falling through to the default boiler-ish branches that key off
# `main_heating_category in {1, 2}`. Spec ref: SAP 10.2 Table 4a
# (main heating category code 4 = heat pump).
pages = _summary_pdf_to_textract_style_pages(_SUMMARY_000899_PDF)
site_notes = ElmhurstSiteNotesExtractor(pages).extract()
# Act
epc = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes)
# Assert
assert epc.sap_heating.main_heating_details, "no main heating details surfaced"
main = epc.sap_heating.main_heating_details[0]
assert main.main_heating_index_number == 104568
assert main.main_heating_category == 4
def test_summary_0380_full_chain_sap_matches_worksheet_pdf_exactly() -> None:
# Arrange — cert 0380-2471-3250-2596-8761 (Summary_000899.pdf /
# dr87-0001-000899.pdf) is the first heat-pump cert under per-cert

View file

@ -61,6 +61,7 @@ from datatypes.epc.schema.rdsap_schema_21_0_1 import (
RdSapSchema21_0_1,
EnergyElement as EnergyElement_21_0_1,
)
from domain.sap10_calculator.tables.pcdb import heat_pump_record
from datatypes.epc.surveys.elmhurst_site_notes import (
AlternativeWall as ElmhurstAlternativeWall,
BuildingPartDimensions as ElmhurstBuildingPartDimensions,
@ -3332,14 +3333,16 @@ def _elmhurst_sap_control_code(sap_control: str) -> Optional[int]:
return int(m.group(1)) if m else None
# SAP10.2 Table 4a main-heating-category codes. Currently only the
# gas-fired-boiler branch is exercised by the Elmhurst cohort — the
# cascade reads `main_heating_category` to key the §4f pumps+fans table
# (160 kWh/yr for cat 2 = 115 central heating pump + 45 flue fan) and to
# detect heat-network mains (cat 6). Other categories (heat pumps,
# warm-air, electric storage, oil/biomass) are deferred until a fixture
# exercises them.
# SAP10.2 Table 4a main-heating-category codes. The cascade reads
# `main_heating_category` to key the §4f pumps+fans table (160 kWh/yr
# for cat 2 = 115 central heating pump + 45 flue fan), to detect
# heat-network mains (cat 6), and to gate the Appendix N3.6/N3.7
# heat-pump path (cat 4 — `cert_to_inputs.py` line 1896/2005/2057/
# 2104 all branch on `main_heating_category == 4`). Other categories
# (warm-air, electric storage, oil/biomass) are deferred until a
# fixture exercises them.
_ELMHURST_HEATING_CATEGORY_GAS_BOILER: Final[int] = 2
_ELMHURST_HEATING_CATEGORY_HEAT_PUMP: Final[int] = 4
_ELMHURST_GAS_BOILER_FUEL_TYPES: frozenset[str] = frozenset({
"Mains gas",
"LPG bottled",
@ -3352,9 +3355,14 @@ def _elmhurst_main_heating_category(
mh: ElmhurstMainHeating, pcdb_index: Optional[int]
) -> Optional[int]:
"""Derive the SAP10.2 Table 4a main-heating-category from Elmhurst-
lodged data. 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."""
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."""
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
return None