From 9d761f6ad191bbb2a11b79c01278ef7d72a0be8d Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 16 Jul 2026 15:54:36 +0000 Subject: [PATCH] =?UTF-8?q?Score=20heat-pump=20dwellings=20via=20Table=204?= =?UTF-8?q?e=20Group=202=20controls=20and=20Appendix=20N=20efficiency=20?= =?UTF-8?q?=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #1635: 'Programmer, room thermostat and TRVs' on a heat pump → 2210 (PCDB heat_pump_controls), and the lodgement now carries Table 4a category 4 — without it the calculator's Appendix N3.6/N3.7 gate never fired and the PCDB heat pump was billed as 100%-efficient direct electric (6.0/18.5 vs pre 62/57). With the category threaded the two LRHA WAVE 3 fixtures score 52.1/56.9 (the latter within 0.5). LRHA 59→61 computed, within-0.5 42.6%, MAE 2.830 — ratchets hold. Co-Authored-By: Claude Fable 5 --- datatypes/epc/domain/mapper.py | 42 ++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 31be5a966..9458e337e 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -6491,6 +6491,11 @@ def _map_sap_heating( has_fghrs=main.flue_gas_heat_recovery_system, main_fuel_type=main_fuel, sap_main_heating_code=sap_main_heating_code, + main_heating_category=( + _PASHUB_HEATING_CATEGORY_HEAT_PUMP + if main.system_type.lower() in _PASHUB_HEAT_PUMP_SYSTEM_TYPES + else None + ), heat_emitter_type=_pashub_heat_emitter_code(main.emitter), emitter_temperature=main.emitter_temperature, fan_flue_present=main.fan_assist, @@ -7941,6 +7946,29 @@ _PASHUB_HEAT_NETWORK_CONTROL_TO_SAP10: Dict[str, int] = { } +# PasHub `system_type` labels that are SAP 10.2 Table 4e GROUP 2: HEAT PUMPS +# WITH RADIATORS OR UNDERFLOOR HEATING (spec PDF p.172). Their control labels +# carry 22xx codes; the system's efficiency comes from the lodged PCDB product +# (`main_heating_index_number`), not a Table 4a rd row, so +# `_pashub_main_heating_system_code` correctly leaves the code None. +_PASHUB_HEAT_PUMP_SYSTEM_TYPES: frozenset[str] = frozenset({ + "heat pump with radiators or underfloor heating", +}) +# Table 4a main-heating category 4 (heat pumps) — the calculator's Appendix +# N3.6/N3.7 efficiency, extended-heating and §4f pumps+fans paths all gate on +# `main_heating_category == 4` (same category the Elmhurst mapper lodges via +# `_ELMHURST_HEATING_CATEGORY_HEAT_PUMP`). Without it a PCDB heat pump is +# billed as 100%-efficient direct electric. +_PASHUB_HEATING_CATEGORY_HEAT_PUMP: Final[int] = 4 + + +# PasHub surveyed heat-pump control labels → their SAP 10.2 Table 4e Group 2 +# code (PCDB `heat_pump_controls`, control type 2, temp adjustment 0.0). +_PASHUB_HEAT_PUMP_CONTROL_TO_SAP10: Dict[str, int] = { + "Programmer, room thermostat and TRVs": 2210, +} + + # PasHub `system_type` labels that are SAP 10.2 Table 4e GROUP 4: ELECTRIC # STORAGE SYSTEMS (spec PDF p.173). A distinct group from boiler/heat-network, # so its control labels carry their own 24xx codes (see the boiler comment on @@ -8013,9 +8041,10 @@ def _pashub_main_heating_control_code( The resolution is system-aware: the same control label recurs across Table 4e's system groups under different 2xxx codes, so the code is chosen by the system's group — Group 1 boiler (`_PASHUB_MAIN_HEATING_CONTROL_TO_SAP10`), - Group 3 heat network (`_PASHUB_HEAT_NETWORK_CONTROL_TO_SAP10`), Group 4 - electric storage (`_PASHUB_STORAGE_HEATER_CONTROL_TO_SAP10`) or Group 6 room - heater (`_PASHUB_ROOM_HEATER_CONTROL_TO_SAP10`). Any other system group, or a + Group 2 heat pump (`_PASHUB_HEAT_PUMP_CONTROL_TO_SAP10`), Group 3 heat + network (`_PASHUB_HEAT_NETWORK_CONTROL_TO_SAP10`), Group 4 electric storage + (`_PASHUB_STORAGE_HEATER_CONTROL_TO_SAP10`) or Group 6 room heater + (`_PASHUB_ROOM_HEATER_CONTROL_TO_SAP10`). Any other system group, or a label the group's lookup does not cover, strict-raises `UnmappedPasHubLabel` (naming the label and system) rather than silently taking a wrong-group code — the gap is fixed here, never mis-rated or strict-raised deep in the @@ -8025,6 +8054,8 @@ def _pashub_main_heating_control_code( system = system_type.lower() if system in _PASHUB_BOILER_SYSTEM_TYPES: group_map = _PASHUB_MAIN_HEATING_CONTROL_TO_SAP10 + elif system in _PASHUB_HEAT_PUMP_SYSTEM_TYPES: + group_map = _PASHUB_HEAT_PUMP_CONTROL_TO_SAP10 elif system in _PASHUB_HEAT_NETWORK_SYSTEM_TYPES: group_map = _PASHUB_HEAT_NETWORK_CONTROL_TO_SAP10 elif system in _PASHUB_STORAGE_HEATER_SYSTEM_TYPES: @@ -8035,8 +8066,9 @@ def _pashub_main_heating_control_code( raise UnmappedPasHubLabel( "main heating control", f"{control_label!r} on unsupported system {system_type!r} " - f"(only Table 4e Group 1 boiler, Group 3 heat-network, Group 4 " - f"electric-storage + Group 6 room-heater codes are mapped)", + f"(only Table 4e Group 1 boiler, Group 2 heat-pump, Group 3 " + f"heat-network, Group 4 electric-storage + Group 6 room-heater " + f"codes are mapped)", ) code = group_map.get(control_label) if code is None: