mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
fix(elmhurst): map secondary room-heater SAP codes to Table 4a fuel category
Completes `_elmhurst_secondary_fuel_from_sap_code` per SAP 10.2 §12 (PDF p.34: "Secondary heating systems and applicable fuel types are taken from the room heaters section of Table 4a") + RdSAP 10 §10.4.1. Each Table 4a room-heater code now resolves to its fuel CATEGORY's modal fuel: - gas room heaters 601-613 → mains gas (26 → Table 32 1, 3.48 p/kWh) - liquid room heaters 621-625 → heating oil (28 → Table 32 4, 5.44 p/kWh) - solid room heaters 631-636 → house coal (11 → Table 32 11, 3.67 p/kWh) - electric room htrs 691-694/699/701 → None (cascade electricity default) Previously only the gas (601-613→26) and solid (631-634→11) blocks were mapped; liquid heaters (621-625) and 635-636 fell through to None → silently billed as electricity (13.19 p/kWh), a large mis-price for an oil/solid heater. The prior slice raised on those; this maps them to the correct category fuel instead, and keeps the raise ONLY for codes inside the room-heater range (601-701) that are not a recognised Table 4a row. The specific sub-fuel within a category (mains gas vs LPG vs biogas) is a SEPARATE lodgement per §10.4.1 and is NOT exported in the Summary, so the gas block stays the modal mains gas — worksheet "simulated case 37" lodged its 605 live-effect fire on biogas (7.60 p/kWh), unrecoverable from the Summary code alone (this is the entire +7 SAP case-37 gap: secondary energy £131 + a separate biogas standing charge £70; every other line matches the worksheet exactly, incl. (206) main efficiency 61%). 5 AAA tests, harness 47/47 (0 raised), pyright net-zero, regression clean, corpus gauge unchanged (Elmhurst-path only). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
9830ea2110
commit
0fae84d2b6
2 changed files with 90 additions and 41 deletions
|
|
@ -5332,6 +5332,35 @@ def _elmhurst_pump_age_int(age_str: Optional[str]) -> Optional[int]:
|
|||
return 2
|
||||
|
||||
|
||||
# SAP 10.2 Table 4a "Room heaters" section — the secondary-heating SAP
|
||||
# code → FUEL CATEGORY map. Per RdSAP 10 §10.4.1 + SAP 10.2 §12 (PDF
|
||||
# p.34, "Secondary heating systems and applicable fuel types are taken
|
||||
# from the room heaters section of Table 4a") each code carries an
|
||||
# appliance type whose APPLICABLE FUELS are a category (gas / liquid /
|
||||
# solid / electric); the SPECIFIC sub-fuel within that category (mains
|
||||
# gas vs LPG vs biogas) is lodged SEPARATELY and is NOT recoverable from
|
||||
# the Summary, which exports only the code. We therefore resolve each
|
||||
# code to its category's MODAL fuel. Codes mirror the Table 4a efficiency
|
||||
# rows in `domain.sap10_ml.sap_efficiencies` (601-613 gas, 621-625
|
||||
# liquid, 631-636 solid, 691-694/699/701 electric).
|
||||
_ELMHURST_SECONDARY_GAS_CODES: Final[frozenset[int]] = frozenset(
|
||||
{601, 602, 603, 604, 605, 606, 607, 609, 610, 611, 612, 613}
|
||||
)
|
||||
_ELMHURST_SECONDARY_LIQUID_CODES: Final[frozenset[int]] = frozenset(
|
||||
{621, 622, 623, 624, 625}
|
||||
)
|
||||
_ELMHURST_SECONDARY_SOLID_CODES: Final[frozenset[int]] = frozenset(
|
||||
{631, 632, 633, 634, 635, 636}
|
||||
)
|
||||
_ELMHURST_SECONDARY_ELECTRIC_CODES: Final[frozenset[int]] = frozenset(
|
||||
{691, 692, 693, 694, 699, 701}
|
||||
)
|
||||
# Modal Table 32 / `_ELMHURST_MAIN_FUEL_TO_SAP10` fuel code per category.
|
||||
_SECONDARY_FUEL_MAINS_GAS: Final[int] = 26 # Table 32 code 1, 3.48 p/kWh
|
||||
_SECONDARY_FUEL_HEATING_OIL: Final[int] = 28 # → Table 32 code 4, 5.44 p/kWh
|
||||
_SECONDARY_FUEL_HOUSE_COAL: Final[int] = 11 # Table 32 code 11, 3.67 p/kWh
|
||||
|
||||
|
||||
def _elmhurst_secondary_fuel_from_sap_code(
|
||||
sap_code: Optional[int],
|
||||
) -> Optional[int]:
|
||||
|
|
@ -5342,49 +5371,43 @@ def _elmhurst_secondary_fuel_from_sap_code(
|
|||
electricity when `secondary_fuel_type` is None — correct for the
|
||||
portable-electric default but wrong for fuel-fired room heaters.
|
||||
|
||||
SAP 10.2 Table 4a Category 10 ("Room heaters") code blocks:
|
||||
601-613: Gas (mains gas / LPG / biogas) — column A is mains gas;
|
||||
column B for LPG. Cohort default is mains gas
|
||||
(`_ELMHURST_MAIN_FUEL_TO_SAP10["Mains gas"] = 26`).
|
||||
621-625: Liquid fuel room heaters (oil / bioethanol). Cohort
|
||||
not yet exercised — NOT mapped, so they RAISE (see below).
|
||||
631-634: Solid fuel room heaters (open fire, closed room
|
||||
heater with/without boiler). House coal is the modal
|
||||
default per Table 12 secondary rate (3.67 p/kWh).
|
||||
691-699: Electric room heaters. Cascade default (None) routes
|
||||
to standard electricity (13.19 p/kWh).
|
||||
Each code resolves to its SAP 10.2 Table 4a fuel CATEGORY's modal
|
||||
fuel (per RdSAP 10 §10.4.1 — the specific sub-fuel is a separate
|
||||
lodgement the Summary omits):
|
||||
- gas room heaters (601-613) → mains gas (26)
|
||||
- liquid room heaters (621-625) → heating oil (28)
|
||||
- solid room heaters (631-636) → house coal (11)
|
||||
- electric room heaters (691-699, → None (cascade electricity
|
||||
701) default; electricity IS the fuel)
|
||||
|
||||
A code in the room-heater range (601-701) that is NOT a recognised
|
||||
Table 4a row RAISES `UnmappedElmhurstLabel` rather than silently
|
||||
mis-fuelling — per the strict-raise pattern. Codes outside the range
|
||||
(e.g. None / no secondary) return None.
|
||||
|
||||
Cohort cert 2102-3018-0205-7886-5204 surfaces the 631 ("Open fire
|
||||
in grate") path — pre-slice the cascade defaulted to electricity
|
||||
at 13.19 p/kWh, over-charging secondary by ~£340/yr and pushing
|
||||
SAP -15.81 below the worksheet's 63.87.
|
||||
in grate") path — pre-fix the cascade defaulted to electricity at
|
||||
13.19 p/kWh, over-charging secondary by ~£340/yr and pushing SAP
|
||||
-15.81 below the worksheet's 63.87.
|
||||
|
||||
RAISE-don't-guess: a Category-10 room-heater code (601-699) that is
|
||||
fuel-fired but NOT in the mapped gas/solid blocks (e.g. 621-625 liquid
|
||||
fuel) used to return None → the cascade silently billed it as
|
||||
electricity (13.19 p/kWh), a large mis-price for an oil/LPG heater.
|
||||
Per the `UnmappedElmhurstLabel` strict-raise pattern these now raise
|
||||
so the gap surfaces instead of producing a wrong SAP. Electric room
|
||||
heaters (691-699) keep returning None — electricity IS their fuel.
|
||||
|
||||
NOTE the gas block 601-613 still resolves to the MODAL default mains
|
||||
gas: the Summary lodges only the SAP code, never the gas sub-fuel, so
|
||||
an LPG or biogas live-effect fire (worksheet "simulated case 37" used
|
||||
biogas at 7.60 p/kWh) is indistinguishable here from mains gas — the
|
||||
cohort default is correct for the common case and the rarer sub-fuels
|
||||
are not recoverable from the Summary export.
|
||||
SUB-FUEL CAVEAT: the gas block 601-613 resolves to the modal mains
|
||||
gas; an LPG or biogas live-effect fire (worksheet "simulated case 37"
|
||||
lodged biogas at 7.60 p/kWh vs mains gas 3.48 p/kWh) is
|
||||
indistinguishable here — the sub-fuel is not in the Summary export.
|
||||
"""
|
||||
if sap_code is None:
|
||||
return None
|
||||
if 601 <= sap_code <= 613:
|
||||
return 26 # Mains gas, matching `_ELMHURST_MAIN_FUEL_TO_SAP10`
|
||||
if 631 <= sap_code <= 634:
|
||||
return 11 # House coal (Coal in `_ELMHURST_MAIN_FUEL_TO_SAP10`)
|
||||
if 691 <= sap_code <= 699:
|
||||
if sap_code in _ELMHURST_SECONDARY_GAS_CODES:
|
||||
return _SECONDARY_FUEL_MAINS_GAS
|
||||
if sap_code in _ELMHURST_SECONDARY_LIQUID_CODES:
|
||||
return _SECONDARY_FUEL_HEATING_OIL
|
||||
if sap_code in _ELMHURST_SECONDARY_SOLID_CODES:
|
||||
return _SECONDARY_FUEL_HOUSE_COAL
|
||||
if sap_code in _ELMHURST_SECONDARY_ELECTRIC_CODES:
|
||||
return None # Electric room heaters → cascade electricity default
|
||||
if 601 <= sap_code <= 699:
|
||||
# Fuel-fired Category-10 room heater we do not yet map — raise
|
||||
# rather than let the cascade silently bill it as electricity.
|
||||
if 601 <= sap_code <= 701:
|
||||
# A room-heater-range code we don't recognise — raise rather than
|
||||
# let the cascade silently bill it as electricity.
|
||||
raise UnmappedElmhurstLabel("secondary_heating.sap_code", str(sap_code))
|
||||
return None
|
||||
|
||||
|
|
|
|||
|
|
@ -750,10 +750,36 @@ class TestElmhurstSecondaryFuelFromSapCode:
|
|||
# Assert
|
||||
assert fuel is None
|
||||
|
||||
def test_unmapped_liquid_fuel_room_heater_raises(self) -> None:
|
||||
# Arrange — SAP code 621 (liquid-fuel room heater) is fuel-fired but
|
||||
# not in the mapped gas/solid blocks; returning None would silently
|
||||
# bill it as electricity. It must raise instead.
|
||||
def test_liquid_fuel_room_heater_resolves_to_heating_oil(self) -> None:
|
||||
# Arrange — SAP code 621 (liquid-fuel room heater) → its Table 4a
|
||||
# category's modal fuel, heating oil (28 → Table 32 code 4), NOT a
|
||||
# silent electricity fallback.
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_elmhurst_secondary_fuel_from_sap_code, # pyright: ignore[reportPrivateUsage]
|
||||
)
|
||||
|
||||
# Act
|
||||
fuel = _elmhurst_secondary_fuel_from_sap_code(621)
|
||||
|
||||
# Assert
|
||||
assert fuel == 28
|
||||
|
||||
def test_solid_fuel_room_heater_resolves_to_house_coal(self) -> None:
|
||||
# Arrange — SAP code 631 (open fire in grate) → house coal (11).
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_elmhurst_secondary_fuel_from_sap_code, # pyright: ignore[reportPrivateUsage]
|
||||
)
|
||||
|
||||
# Act
|
||||
fuel = _elmhurst_secondary_fuel_from_sap_code(631)
|
||||
|
||||
# Assert
|
||||
assert fuel == 11
|
||||
|
||||
def test_unrecognised_room_heater_range_code_raises(self) -> None:
|
||||
# Arrange — SAP code 620 sits in the room-heater range (601-701) but
|
||||
# is not a recognised Table 4a row; rather than silently mis-fuel it
|
||||
# must raise.
|
||||
from datatypes.epc.domain.mapper import (
|
||||
UnmappedElmhurstLabel,
|
||||
_elmhurst_secondary_fuel_from_sap_code, # pyright: ignore[reportPrivateUsage]
|
||||
|
|
@ -761,4 +787,4 @@ class TestElmhurstSecondaryFuelFromSapCode:
|
|||
|
||||
# Act / Assert
|
||||
with pytest.raises(UnmappedElmhurstLabel):
|
||||
_elmhurst_secondary_fuel_from_sap_code(621)
|
||||
_elmhurst_secondary_fuel_from_sap_code(620)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue