S0380.230: electric room heaters (cat 10) on off-peak bill at Table 12a direct-acting high rate

SAP 10.2 Table 12a Grid 1 (PDF p.191): an electric room heater (RdSAP
main_heating_category 10, e.g. SAP code 691) is direct-acting electric,
so it sits on the "Other systems including direct-acting electric" row —
7-hour high-rate fraction 1.00, 10-hour 0.50. It runs on demand, mostly
at the HIGH rate; it does NOT earn the 100%-low-rate of overnight storage
charging (which is category 7).

`_table_12a_system_for_main` only mapped ASHP, so an electric room heater
fell through to the "100% low-rate" fallback (5.50 p, £0.0550), under-
charging space heating by ~9.79 p/kWh and systematically OVER-rating the
cluster. Now maps electric cat-10 mains to OTHER_DIRECT_ACTING_ELECTRIC
(gated on `_is_electric_main`, so gas/solid-fuel cat-10 room heaters are
excluded). The same Table 12a fraction flows through cost, CO2 (Table
12d) and PE (Table 12e) — all three callers already pre-gate on electric.
Mirror of S0380.228 (same fallback bug for electric SECONDARY heating).

1,000-cert 2026 API sample (no worksheet for this cluster — ±0.5-vs-lodged
fallback bar): cat-10 mean |err| 9.49 → 7.11, %<0.5 10.4% → 16.7%;
headline %<0.5 42.5% → 42.9%, overall mean |err| 2.29 → 2.16. cat-7
(storage) and cat-2 (gas) unchanged. Full §4 suite green (2405 passed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-04 20:39:15 +00:00
parent 86a725224b
commit 0476b4b235
2 changed files with 59 additions and 0 deletions

View file

@ -2138,6 +2138,16 @@ def _table_12a_system_for_main(
main.main_heating_index_number is not None
and heat_pump_record(main.main_heating_index_number) is not None
)
# Electric room heaters (RdSAP main_heating_category 10) are direct-
# acting electric → SAP 10.2 Table 12a Grid 1 (PDF p.191) "Other
# systems including direct-acting electric" row (7-hour high-rate
# fraction 1.00, 10-hour 0.50). Distinct from electric STORAGE
# heaters (category 7), which charge off-peak and correctly fall
# through to None here (→ 100% low rate). Gated on `_is_electric_main`
# so a non-electric room heater (gas / solid-fuel cat 10) is excluded;
# all callers already pre-gate on electric, this is belt-and-braces.
if main.main_heating_category == 10 and _is_electric_main(main):
return Table12aSystem.OTHER_DIRECT_ACTING_ELECTRIC
# ASHP — Table 4a rows 211-217 (earlier generations) + 221-227
# (2013+) cover the air-source space. Warm-air ASHPs are 521-524.
if code is not None and (

View file

@ -2938,6 +2938,55 @@ def test_space_heating_off_peak_fallback_uses_actual_tariff_low_rate_not_e7() ->
assert abs(cost_eighteen_hour - 0.0741) <= 1e-6
def test_space_heating_electric_room_heater_off_peak_bills_at_direct_acting_high_rate() -> None:
# Arrange — an ELECTRIC room heater (RdSAP main_heating_category 10,
# e.g. SAP code 691) is direct-acting electric, so SAP 10.2 Table 12a
# Grid 1 (PDF p.191) puts it on the "Other systems including direct-
# acting electric" row: 7-hour high-rate fraction 1.00, 10-hour 0.50.
# Unlike STORAGE heaters (category 7), which charge off-peak and so
# correctly bill 100% at the low rate, a room heater runs on demand —
# mostly at the HIGH rate. `_table_12a_system_for_main` only mapped
# ASHP, so a room heater fell through to the "100% low-rate" fallback
# (5.50 p, £0.0550), under-charging space heating by ~9.79 p/kWh and
# systematically OVER-rating the cat-10 cluster (1,000-cert API sample:
# 48 certs, mean |err| 9.49, signed +5.08). The fix maps electric
# cat-10 mains to OTHER_DIRECT_ACTING_ELECTRIC. Mirror of S0380.228
# (which fixed the same fallback for electric SECONDARY heating).
from domain.sap10_calculator.tables.table_12a import Tariff
electric_room_heater_main = MainHeatingDetail(
has_fghrs=False,
main_fuel_type=30, # standard electricity
heat_emitter_type=2,
emitter_temperature=1,
main_heating_control=2602,
main_heating_category=10, # electric room heaters
sap_main_heating_code=691,
)
gas_room_heater_main = MainHeatingDetail(
has_fghrs=False,
main_fuel_type=1, # mains gas — NOT electric
heat_emitter_type=2,
emitter_temperature=1,
main_heating_control=2602,
main_heating_category=10, # gas room heater (also cat 10)
sap_main_heating_code=631,
)
# Act — 7-hour off-peak tariff.
electric_rate = _space_heating_fuel_cost_gbp_per_kwh(
electric_room_heater_main, Tariff.SEVEN_HOUR, prices=SAP_10_2_SPEC_PRICES,
)
gas_rate = _space_heating_fuel_cost_gbp_per_kwh(
gas_room_heater_main, Tariff.SEVEN_HOUR, prices=SAP_10_2_SPEC_PRICES,
)
# Assert — electric room heater: 1.00 × 15.29 p = £0.1529 (high rate).
# Gas room heater is unaffected (non-electric → single Table 32 rate,
# not the off-peak electric split).
assert abs(electric_rate - 0.1529) <= 1e-6
assert abs(gas_rate - 0.0550) > 1e-6
def test_heat_network_dlf_full_table_12c_age_band_coverage() -> None:
# Arrange — SAP 10.2 Table 12c (page 193) heat-network Distribution
# Loss Factor by dwelling age band A..M. None → K-or-newer