From 3b61ca8cecb539740889c5cf841ac770ff105a35 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 28 May 2026 22:47:34 +0000 Subject: [PATCH] Slice S0380.57: Elmhurst mapper infers electricity fuel for electric SAP main heating codes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Elmhurst §14.0 leaves "Fuel Type" empty for electric main heating systems (heat pumps, electric boilers, storage heaters, electric underfloor, warm-air HPs) — the SAP code identifies the carrier directly. The mapper was reading the empty string via `_elmhurst_main_fuel_int(mh.fuel_type)` → None, and downstream `_main_fuel_code` returned None, so Table 32 unit-price lookups defaulted to mains gas. Cert 000565 (HP Main 1, SAP code 224) was being charged 29,353 kWh/yr of electricity at the gas tariff — £0.0364/kWh instead of £0.165/kWh. New `_ELECTRIC_SAP_MAIN_HEATING_CODES` frozen set covers the Table 4a electric carrier rows: 191-196 Electric boilers 211-217, 221-227 Heat pumps (224 = ASHP 2013+, 1.70 COP) 401-409 Electric storage heaters 421-425 Electric underfloor heating 521-527 Warm-air heat pumps Inference fires in both Main 1 (`_map_elmhurst_sap_heating`) and Main 2 (`_map_elmhurst_main_heating_2`) construction paths — when `_elmhurst_main_fuel_int(fuel_type)` returns None AND the SAP code is in the electric set, fall back to `_STANDARD_ELECTRICITY_FUEL_ CODE = 30` (Table 12 row "Electricity, standard tariff"). Cert 000565 cascade impact (compounding with S0380.56): - sap_score: 71 → 30 (target 29 → Δ +1.7; was Δ +44) - sap_score_continuous: 71.42 → 30.21 (target 28.51 → Δ +1.70; was Δ +42.91) - ecf: 2.05 → 5.22 (target 5.39 → Δ −0.17; was Δ −3.34) - total_fuel_cost_gbp: 1,423.80 → 3,624.64 (target 4,680.26 → Δ −1,055.62; was Δ −3,256.46) - co2_kg_per_yr: 7,181.62 → 5,009.47 (target 6,447.63 → Δ −1,438.16; was Δ +733.99) (now undershooting — independent cascade gap around Table 12d monthly electric CO2 factor interpolation; separate slice) Single-main non-HP certs: no behavioural change (`fuel_type` lodged explicitly for gas/oil boilers → `_elmhurst_main_fuel_int` returns non-None → inference branch not entered). Cohort regression check: 472 pass + 10 expected 000565 fails — no regression. Spec source: SAP 10.2 Table 4a main heating SAP codes + Table 12 fuel codes (electricity, standard tariff = 30). Heat-pump cohort efficiency values cross-referenced in `domain/sap10_ml/sap_efficiencies.py:42-44`. Pyright net-zero on mapper.py (32 / 32). Co-Authored-By: Claude Opus 4.7 --- datatypes/epc/domain/mapper.py | 46 ++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 7f268796..adfffdf2 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -3618,6 +3618,36 @@ _ELMHURST_GAS_BOILER_FUEL_TYPES: frozenset[str] = frozenset({ "LPG special condition", }) +# Standard-electricity Table 32 fuel code. SAP 10.2 Table 12 lists +# this as code 30 = "Electricity, standard tariff" — the fuel-cost / +# CO2 / PE lookup key for any electric main heating system. +_STANDARD_ELECTRICITY_FUEL_CODE: Final[int] = 30 + +# SAP 10.2 Table 4a main-heating SAP codes whose fuel is electricity. +# Elmhurst §14.0 leaves "Fuel Type" empty for these systems (the SAP +# code identifies the carrier), so the mapper must infer the fuel +# from the SAP code rather than the empty string. First needed by +# cert 000565 Main 1 (SAP code 224 = "Air source heat pump, 2013 or +# later") — without inference, `_main_fuel_code` returns None and the +# Table 32 price lookup defaults to mains gas, charging the HP cert's +# electricity kWh at the gas tariff. +# +# Coverage as Elmhurst-only fixtures land: +# 191-196 — electric boilers (Table 4a "Electric boilers" rows) +# 211-217, 221-227 — heat pumps (Table 4a "Heat pumps" rows; 224 is +# the ASHP 2013+ entry, 1.70 COP — `sap_efficiencies.py:44`) +# 401-409 — electric storage heaters (Table 4a "Storage heaters") +# 421-425 — electric underfloor (Table 4a "Underfloor heating") +# 521-527 — warm-air heat pumps (Table 4a "Warm-air heat pumps") +_ELECTRIC_SAP_MAIN_HEATING_CODES: Final[frozenset[int]] = frozenset( + list(range(191, 197)) + + list(range(211, 218)) + + list(range(221, 228)) + + list(range(401, 410)) + + list(range(421, 426)) + + list(range(521, 528)) +) + class UnmappedElmhurstLabel(ValueError): """An Elmhurst Summary lodged a finite-enum label that the mapper @@ -3835,6 +3865,13 @@ def _map_elmhurst_main_heating_2( ), ) main_fuel_int = _elmhurst_main_fuel_int(mh2.fuel_type) + # Same electric-SAP-code fuel inference as Main 1 — see + # `_ELECTRIC_SAP_MAIN_HEATING_CODES` docstring. + if ( + main_fuel_int is None + and mh2.main_heating_sap_code in _ELECTRIC_SAP_MAIN_HEATING_CODES + ): + main_fuel_int = _STANDARD_ELECTRICITY_FUEL_CODE category: Optional[int] = None if pcdb_index is not None and heat_pump_record(pcdb_index) is not None: category = _ELMHURST_HEATING_CATEGORY_HEAT_PUMP @@ -3882,6 +3919,15 @@ def _map_elmhurst_sap_heating(survey: ElmhurstSiteNotes) -> SapHeating: ) pcdb_index = _elmhurst_pcdb_boiler_index(mh.pcdf_boiler_reference) main_fuel_int = _elmhurst_main_fuel_int(mh.fuel_type) + # Elmhurst §14.0 leaves "Fuel Type" empty for electric main heating + # systems (HP / electric boiler / storage / underfloor); the SAP + # code identifies the carrier. Infer electricity (Table 32 code 30) + # when the mapper can't read it from the `fuel_type` string. + if ( + main_fuel_int is None + and mh.main_heating_sap_code in _ELECTRIC_SAP_MAIN_HEATING_CODES + ): + main_fuel_int = _STANDARD_ELECTRICITY_FUEL_CODE heat_emitter_int = _elmhurst_heat_emitter_int(mh.heat_emitter) sap_control_int = _elmhurst_sap_control_code(sap_control) main_heating_category = _elmhurst_main_heating_category(mh, pcdb_index)