mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-27 23:35:01 +00:00
Code a Manual-Entry electric "Direct acting" boiler to SAP 191 🟩
A PasHub Manual-Entry electric boiler lodges "Direct acting" in its "Heating System (Boiler):" age-band cell. `_PASHUB_MANUAL_BOILER_AGE_BAND_TO_TABLE_4B` covers only gas/liquid Table 4b rows, so the descriptor resolved to None and the boiler stayed uncoded — taking the generic 0.80 gas-boiler seasonal efficiency AND, on a Dual meter, `_table_12a_system_for_main`'s None-fallback that bills the whole main-heating load at the off-peak LOW rate. The two errors pull opposite ways, so an identical mis-mapping over-rated one dwelling and under-rated another (fixture 497712825571 +9.5 on a Dual meter; 461386632387 -6.2 on a Single meter). A direct-acting electric "boiler with radiators" is SAP 10.2 Table 4a code 191 (direct-acting electric boiler, efficiency 1.00, §12 Rule 3 tariff). Code it directly in the manual-boiler resolver (gated on fuel Electricity + no PCDB product), so the single code value drives the efficiency table, the Table 12a pricing row and the tariff dispatch together. Both fixtures converge: 497712825571 → +0.6, 461386632387 → +0.4. LRHA within-0.5 52.4% → 53.4%, MAE 0.835. Renamed `_pashub_manual_boiler_table_4b_code` → `_pashub_manual_ boiler_code` (it now also returns the Table 4a electric code). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
398692703f
commit
0012ad1625
2 changed files with 59 additions and 13 deletions
|
|
@ -96,11 +96,17 @@ _MANIFEST_PATH = _FIXTURES_DIR / "manifest.json"
|
|||
# code-691 dwellings. Routed the resolved `_rdsap_tariff` into the secondary cost
|
||||
# path (mirroring the CO2 path, which already did this) → within-0.5 49.5% →
|
||||
# 52.4%, MAE 0.977. Storage-heater dwellings (7-hour by §12) are unchanged.
|
||||
# 2026-07-24 (accuracy tail audit): a Manual-Entry electric "Direct acting"
|
||||
# boiler was left uncoded (Table 4b covers only gas/liquid), so it took the 0.80
|
||||
# gas-boiler default AND, on a Dual meter, 100%-off-peak billing → ±6-9 mis-rate.
|
||||
# Coded it to SAP Table 4a 191 (direct-acting electric boiler, eff 1.00): fixture
|
||||
# 497712825571 +9.5 → +0.6, 461386632387 -6.2 → +0.4 → within-0.5 52.4% → 53.4%,
|
||||
# MAE 0.835.
|
||||
# Treat the constants as a regression tripwire, NOT a cohort accuracy figure;
|
||||
# re-baseline (coverage growth is not loosening) as further mapper fixes unblock
|
||||
# fixtures.
|
||||
_MIN_WITHIN_HALF: float = 0.52
|
||||
_MAX_SAP_MAE: float = 0.98
|
||||
_MIN_WITHIN_HALF: float = 0.53
|
||||
_MAX_SAP_MAE: float = 0.84
|
||||
|
||||
_KNOWN_GAP_REASON = (
|
||||
"pashub `from_site_notes` mapper does not int-code a main-heating "
|
||||
|
|
@ -161,6 +167,27 @@ def _evaluate(deal_id: str) -> Outcome:
|
|||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
"497712825571" not in _BY_ID or "461386632387" not in _BY_ID,
|
||||
reason="direct-acting electric boiler fixtures not in manifest",
|
||||
)
|
||||
@pytest.mark.parametrize("deal_id", ["497712825571", "461386632387"])
|
||||
def test_direct_acting_electric_boiler_codes_to_191(deal_id: str) -> None:
|
||||
"""Regression: a Manual-Entry electric "Direct acting" boiler is a
|
||||
direct-acting electric wet system → SAP Table 4a code 191 (efficiency 1.00,
|
||||
§12 Rule 3 tariff). Left uncoded it took the generic 0.80 gas-boiler default
|
||||
AND, on a Dual meter, billed its whole load at the off-peak low rate — two
|
||||
errors that pull opposite ways (fixture 497712825571 over-rated +9.5 on a
|
||||
Dual meter; 461386632387 under-rated -6.2 on a Single meter). Coding it 191
|
||||
fixes both (the code drives the efficiency table, the Table 12a pricing row
|
||||
and the §12 tariff together): 497712825571 +9.5 → +0.6, 461386632387
|
||||
-6.2 → +0.4. The bound is 1.0 (not 0.5) — the fix removes the ±6-9 mis-code,
|
||||
leaving only sub-point residuals within the cohort's normal spread."""
|
||||
outcome = _evaluate(deal_id)
|
||||
assert outcome.diff is not None
|
||||
assert outcome.diff < 1.0, f"direct-acting electric boiler mis-coded: diff {outcome.diff:.2f}"
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
"462024626384" not in _BY_ID, reason="fixture 462024626384 not in manifest"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8578,14 +8578,24 @@ def _pashub_main_heating_control_code(
|
|||
return code
|
||||
|
||||
|
||||
# SAP 10.2 Table 4a code 191 = "direct-acting electric boiler" (efficiency 1.00,
|
||||
# §12 Rule 3 tariff dispatch). A Manual-Entry electric boiler lodges "Direct
|
||||
# acting" in its "Heating System (Boiler):" age-band cell; Table 4b (gas/liquid
|
||||
# only) does not cover it, so without an explicit code it stayed uncoded → billed
|
||||
# at the generic 0.80 gas default AND, on a Dual meter, 100% off-peak low-rate — a
|
||||
# large mis-rate in BOTH directions (over-rate on Dual, under-rate on Single).
|
||||
_PASHUB_DIRECT_ACTING_ELECTRIC_BOILER_AGE_BAND: Final[str] = "Direct acting"
|
||||
_SAP_DIRECT_ACTING_ELECTRIC_BOILER_CODE: Final[int] = 191
|
||||
|
||||
# PasHub Manual-Entry boiler descriptor → SAP 10.2 Table 4b sub-row code
|
||||
# (`table_4b.py`), keyed on the surveyed "Heating System (Boiler):" cell
|
||||
# (`boiler_age_band`), which carries both the boiler type and the pre/post-1998
|
||||
# age band. A Manual-Entry boiler has no PCDB `product_id`, so its efficiency
|
||||
# must come from Table 4b, not the generic 0.80 gas default (#1649 A). Only the
|
||||
# cohort's observed gas/liquid descriptors are mapped; an unmapped descriptor
|
||||
# (or an electric "Direct acting" boiler, which Table 4b does not cover) resolves
|
||||
# to None, preserving the pre-existing behaviour (no block, no regression).
|
||||
# cohort's observed gas/liquid descriptors are mapped; an unmapped gas/liquid
|
||||
# descriptor resolves to None, preserving the pre-existing behaviour (no block,
|
||||
# no regression). The electric "Direct acting" boiler is handled separately
|
||||
# (Table 4a code 191, above) since it is not a Table 4b row.
|
||||
_PASHUB_MANUAL_BOILER_AGE_BAND_TO_TABLE_4B: Dict[str, int] = {
|
||||
# Gas boilers 1998 or later (Table 4b 101-109).
|
||||
"1998 or later - Condensing combi": 104, # 84/75 winter/summer
|
||||
|
|
@ -8599,14 +8609,22 @@ _PASHUB_MANUAL_BOILER_AGE_BAND_TO_TABLE_4B: Dict[str, int] = {
|
|||
}
|
||||
|
||||
|
||||
def _pashub_manual_boiler_table_4b_code(main: PasHubMainHeating) -> Optional[int]:
|
||||
def _pashub_manual_boiler_code(main: PasHubMainHeating) -> Optional[int]:
|
||||
"""Resolve a Manual-Entry boiler's surveyed descriptor to its SAP 10.2
|
||||
Table 4b code (#1649 A). Returns None for a PCDF-Search boiler (product_id
|
||||
lodged — efficiency is the SEDBUK lookup) and for any descriptor the Table
|
||||
4b map does not cover (e.g. an electric boiler), leaving the calculator's
|
||||
existing cascade untouched for those."""
|
||||
main-heating code. Returns None for a PCDF-Search boiler (product_id lodged
|
||||
— efficiency is the SEDBUK lookup) and for any gas/liquid descriptor the
|
||||
Table 4b map does not cover, leaving the calculator's existing cascade
|
||||
untouched. An electric "Direct acting" boiler is a direct-acting electric
|
||||
wet system → Table 4a code 191 (efficiency 1.00), NOT a Table 4b gas/liquid
|
||||
row; without this it stayed uncoded and was billed at the 0.80 gas default
|
||||
and (on a Dual meter) the off-peak low rate."""
|
||||
if main.product_id > 0:
|
||||
return None
|
||||
if (
|
||||
main.boiler_age_band == _PASHUB_DIRECT_ACTING_ELECTRIC_BOILER_AGE_BAND
|
||||
and main.fuel == "Electricity"
|
||||
):
|
||||
return _SAP_DIRECT_ACTING_ELECTRIC_BOILER_CODE
|
||||
return _PASHUB_MANUAL_BOILER_AGE_BAND_TO_TABLE_4B.get(main.boiler_age_band)
|
||||
|
||||
|
||||
|
|
@ -8645,9 +8663,10 @@ def _pashub_main_heating_system_code(main: PasHubMainHeating) -> Optional[int]:
|
|||
return code
|
||||
if system in _PASHUB_BOILER_SYSTEM_TYPES:
|
||||
# PCDF-Search boiler → None (SEDBUK product efficiency); Manual-Entry
|
||||
# boiler → its Table 4b code (or None for an uncovered/non-Table-4b
|
||||
# descriptor, keeping the pre-existing cascade) (#1649 A).
|
||||
return _pashub_manual_boiler_table_4b_code(main)
|
||||
# boiler → its Table 4b code, Table 4a 191 for an electric direct-acting
|
||||
# boiler, or None for an uncovered descriptor (keeping the pre-existing
|
||||
# cascade) (#1649 A).
|
||||
return _pashub_manual_boiler_code(main)
|
||||
return None
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue