Slice S0380.57: Elmhurst mapper infers electricity fuel for electric SAP main heating codes

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 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-28 22:47:34 +00:00 committed by Jun-te Kim
parent 35d2648ae6
commit 358b4dcd01

View file

@ -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)