mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
fix(tariff): map electric boilers 191/193-196 to their Table 12a Grid 1 rows
SAP 10.2 Table 4a electric boilers (PDF p.170) split across three distinct
Table 12a Grid 1 SH rows (PDF p.191), not one "direct-acting" family as the
stale TODO in `_table_12a_system_for_main` implied:
- 191 Direct-acting electric boiler -> "Direct-acting electric boiler (a)"
row: 7-hour 0.90, 10-hour 0.50 (NOT the 1.00/0.50 "Other direct-acting
electric heating" room-heater row).
- 193/194/195/196 Electric dry core / water storage boiler -> "Electric dry
core or water storage boiler" row: 7-hour 0.00 (charged wholly off-peak =
100% low rate, identical to the None fallback).
- 192 Electric CPSU -> Appendix F; left falling through to None (off-peak
low) until the Appendix-F high-rate cascade is implemented.
The enum + fractions already existed in table_12a.py; only the code->enum
mapping was missing. Resolves the TODO and pins the spec-correct 0.00 for the
storage boilers so 195 can't be mis-"fixed" up to a direct-acting fraction.
Forward guard, 0 corpus impact: storage boilers already billed 100% low via
the None fallback, and all corpus 191 certs are on standard tariff (Table 12a
off-peak split never fires). Corpus gauge unchanged 73.3% / MAE 0.774.
Pin: test_electric_boilers_191_195_map_to_distinct_table_12a_grid1_rows.
pyright strict gate not run locally (pyright not installed in this container).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9694650abe
commit
eea5d3a5a8
2 changed files with 79 additions and 2 deletions
|
|
@ -2520,6 +2520,25 @@ _ELECTRIC_ROOM_HEATER_SAP_CODES: Final[frozenset[int]] = frozenset(
|
|||
{691, 692, 693, 694, 699}
|
||||
)
|
||||
|
||||
# SAP 10.2 Table 4a electric boilers (PDF p.170, codes 191-196) → their
|
||||
# Table 12a Grid 1 SH rows (PDF p.191). NOTE the boiler families do NOT all
|
||||
# share a row — read the spec exactly:
|
||||
# 191 Direct-acting electric boiler → "Direct-acting electric boiler
|
||||
# (a)" row: 7-hour 0.90, 10-hour 0.50 (NOT the "Other direct-acting
|
||||
# electric heating" 1.00/0.50 room-heater row).
|
||||
# 192 Electric CPSU → "Electric CPSU" row: Appendix F
|
||||
# (no flat Table 12a fraction — left as a documented gap, see below).
|
||||
# 193/194 Electric dry core storage boiler → "Electric dry core or water
|
||||
# 195/196 Electric water storage boiler storage boiler" row: 7-hour
|
||||
# 0.00 (charged wholly off-peak — identical to the 100%-low-rate the
|
||||
# None fallback already gave; mapped EXPLICITLY so the spec-correct
|
||||
# 0.00 is pinned and can't be "fixed" up to a wrong direct-acting 1.00).
|
||||
_DIRECT_ACTING_ELECTRIC_BOILER_CODES: Final[frozenset[int]] = frozenset({191})
|
||||
_ELECTRIC_STORAGE_BOILER_CODES: Final[frozenset[int]] = frozenset(
|
||||
{193, 194, 195, 196}
|
||||
)
|
||||
_ELECTRIC_CPSU_CODES: Final[frozenset[int]] = frozenset({192})
|
||||
|
||||
|
||||
def _table_12a_system_for_main(
|
||||
main: Optional[MainHeatingDetail],
|
||||
|
|
@ -2539,8 +2558,9 @@ def _table_12a_system_for_main(
|
|||
- Storage heaters (cat 7): 408 → INTEGRATED_STORAGE_DIRECT (0.20),
|
||||
all others → OTHER_STORAGE_HEATERS (0.00) — wired
|
||||
- Underfloor heating (421-422) — TODO
|
||||
- Direct-acting electric (191) / CPSU (192) / electric storage
|
||||
boiler (193, 195) — TODO
|
||||
- Direct-acting electric boiler (191) → 0.90/0.50; electric storage
|
||||
boilers (193/194/195/196) → 0.00 — wired
|
||||
- Electric CPSU (192) — Appendix F high-rate cascade still TODO
|
||||
"""
|
||||
if main is None:
|
||||
return None
|
||||
|
|
@ -2601,6 +2621,19 @@ def _table_12a_system_for_main(
|
|||
if code == 408:
|
||||
return Table12aSystem.INTEGRATED_STORAGE_DIRECT
|
||||
return Table12aSystem.OTHER_STORAGE_HEATERS
|
||||
# Electric boilers (Table 4a codes 191-196) — resolve the misleading TODO
|
||||
# that lumped them as one "direct-acting" family. They split across THREE
|
||||
# distinct Table 12a Grid 1 rows (see `_DIRECT_ACTING_ELECTRIC_BOILER_CODES`
|
||||
# et al). 191 alone is direct-acting (0.90/0.50); 193-196 are storage
|
||||
# boilers (0.00 = 100% low, the spec-correct value the None fallback gave —
|
||||
# so this is a forward guard, not a corpus mover); 192 CPSU needs Appendix F
|
||||
# and is left to fall through to None (the off-peak-low fallback) until the
|
||||
# Appendix-F high-rate cascade is implemented.
|
||||
if code is not None and _is_electric_main(main):
|
||||
if code in _DIRECT_ACTING_ELECTRIC_BOILER_CODES:
|
||||
return Table12aSystem.DIRECT_ACTING_ELECTRIC_BOILER
|
||||
if code in _ELECTRIC_STORAGE_BOILER_CODES:
|
||||
return Table12aSystem.ELECTRIC_DRY_CORE_OR_WATER_STORAGE
|
||||
return None
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ from domain.sap10_calculator.rdsap.cert_to_inputs import (
|
|||
_TABLE_4G_DEFAULT_MEV_SFP_W_PER_L_PER_S, # pyright: ignore[reportPrivateUsage]
|
||||
dimensions_from_cert,
|
||||
_table_12_factor_fuel_code, # pyright: ignore[reportPrivateUsage]
|
||||
_table_12a_system_for_main, # pyright: ignore[reportPrivateUsage]
|
||||
_is_electric_main, # pyright: ignore[reportPrivateUsage]
|
||||
_is_heat_network_electric_main, # pyright: ignore[reportPrivateUsage]
|
||||
_is_electric_water, # pyright: ignore[reportPrivateUsage]
|
||||
|
|
@ -911,6 +912,49 @@ def test_no_system_electric_heaters_assumed_code_699_bills_direct_acting_split()
|
|||
assert abs(inputs.space_heating_fuel_cost_gbp_per_kwh - 0.1109) <= 1e-9
|
||||
|
||||
|
||||
def test_electric_boilers_191_195_map_to_distinct_table_12a_grid1_rows() -> None:
|
||||
# Arrange — SAP 10.2 Table 4a electric boilers (PDF p.170) do NOT share
|
||||
# one Table 12a Grid 1 row (PDF p.191). Read exactly: code 191
|
||||
# "Direct-acting electric boiler (a)" → 7-hour 0.90 / 10-hour 0.50 (its
|
||||
# OWN row, NOT the 1.00/0.50 "Other direct-acting electric heating"
|
||||
# room-heater row); code 195 "Electric water storage boiler" → the
|
||||
# "Electric dry core or water storage boiler" row → 7-hour 0.00 (charged
|
||||
# wholly off-peak = 100% low rate, identical to the None fallback). This
|
||||
# pins the spec-correct 0.00 so 195 can't be mis-"fixed" up to a wrong
|
||||
# direct-acting fraction.
|
||||
from domain.sap10_calculator.tables.table_12a import (
|
||||
Table12aSystem,
|
||||
Tariff,
|
||||
space_heating_high_rate_fraction,
|
||||
)
|
||||
|
||||
def _electric_boiler_main(code: int) -> MainHeatingDetail:
|
||||
return MainHeatingDetail(
|
||||
has_fghrs=False,
|
||||
main_fuel_type=29, # electricity
|
||||
heat_emitter_type=0,
|
||||
emitter_temperature=1,
|
||||
main_heating_control=2106,
|
||||
main_heating_category=2, # boiler
|
||||
sap_main_heating_code=code,
|
||||
)
|
||||
|
||||
# Act
|
||||
direct_acting = _table_12a_system_for_main(_electric_boiler_main(191))
|
||||
storage = _table_12a_system_for_main(_electric_boiler_main(195))
|
||||
|
||||
# Assert — distinct rows with their published fractions.
|
||||
assert direct_acting is Table12aSystem.DIRECT_ACTING_ELECTRIC_BOILER
|
||||
assert storage is Table12aSystem.ELECTRIC_DRY_CORE_OR_WATER_STORAGE
|
||||
assert (
|
||||
space_heating_high_rate_fraction(direct_acting, Tariff.SEVEN_HOUR) == 0.90
|
||||
)
|
||||
assert (
|
||||
space_heating_high_rate_fraction(direct_acting, Tariff.TEN_HOUR) == 0.50
|
||||
)
|
||||
assert space_heating_high_rate_fraction(storage, Tariff.SEVEN_HOUR) == 0.00
|
||||
|
||||
|
||||
def test_heat_network_distribution_electricity_per_sap_10_2_appendix_c_3_2() -> None:
|
||||
# Arrange — heat-network main (Table 4a code 301 = community heating,
|
||||
# category 6). SAP 10.2 Appendix C §C3.2 (PDF p.51): distribution
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue