From 19e23d0c316e2d81e50b230982a6a8f56e19ecba Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 27 May 2026 17:56:30 +0000 Subject: [PATCH] Slice S0380.2: surface main_heating_category=4 for PCDB heat-pump indices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../tests/test_summary_pdf_mapper_chain.py | 22 +++++++++++++++ datatypes/epc/domain/mapper.py | 28 ++++++++++++------- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py b/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py index 557ea037..a4fb4229 100644 --- a/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py +++ b/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py @@ -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 diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 2be18112..05af513f 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -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