S0380.228: electric secondary on off-peak bills at Table 12a direct-acting high rate

SAP 10.2 Table 12a Grid 1 (PDF p.191): secondary heating is a direct-
acting electric room heater (RdSAP 10 §A.2.2 default), on the "Other
systems including direct-acting electric" row — 7-hour high-rate fraction
1.00, 10-hour 0.50. A room heater runs on demand, mostly at the high
rate; it does NOT earn the 100%-low-rate of overnight storage charging.

`_secondary_fuel_cost_gbp_per_kwh` previously returned the flat off-peak
LOW rate (5.50 p, £0.0550) for every off-peak electric secondary, under-
charging by 9.79 p/kWh. New `_secondary_off_peak_rate_gbp_per_kwh` mirrors
`_space_heating_fuel_cost_gbp_per_kwh`: it blends the Table 12a high-rate
fraction (OTHER_DIRECT_ACTING_ELECTRIC) against the Table 32 high/low
rates, with the 18-/24-hour fallback to the low rate.

Simulated case 19 (electric storage main + electric secondary, Dual/7-hour
meter) is the worksheet case (242): "Space heating - secondary
(1.00*15.29 + 0.00*5.50)" → 15.29 p/kWh = £0.1529. This was the primary
cat-7-cluster cost driver: total cost 1485.68 → 1835.53 (worksheet
1816.58), SAP cont 60.11 → 50.67 (worksheet ~51.22). Remaining +19 cost
is HW/space-heating kWh (next slices).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-04 18:00:38 +00:00
parent 5d4b55d7f9
commit 4911c56200
2 changed files with 75 additions and 2 deletions

View file

@ -2262,6 +2262,35 @@ def _secondary_efficiency(
return seasonal_efficiency(code, None, None)
def _secondary_off_peak_rate_gbp_per_kwh(meter_type: object) -> float:
"""SAP 10.2 Table 12a Grid 1 (PDF p.191) blended rate for an electric
secondary heater on an off-peak tariff. The secondary is a direct-
acting electric room heater (RdSAP 10 §A.2.2 default), so it sits on
the "Other systems including direct-acting electric" row high-rate
fraction 1.00 for 7-hour, 0.50 for 10-hour. NOT the 100%-low-rate of
storage-charging: a room heater runs on demand, mostly at the high
rate. Worksheet evidence simulated case 19 (242): "Space heating -
secondary (1.00*15.29 + 0.00*5.50)" → all at the 7-hour HIGH rate.
Mirrors `_space_heating_fuel_cost_gbp_per_kwh`: the meter resolves to
a tariff (the `_is_off_peak_meter` Unknown-code-3 heuristic falls
through to 7-hour, as in `_off_peak_low_rate_gbp_per_kwh_via_meter_
heuristic`); 18-/24-hour tariffs (absent from the Grid 1 direct-acting
row) fall back to the tariff's Table 32 low rate."""
tariff = tariff_from_meter_type(meter_type)
if tariff is Tariff.STANDARD:
tariff = Tariff.SEVEN_HOUR
try:
high_frac = space_heating_high_rate_fraction(
Table12aSystem.OTHER_DIRECT_ACTING_ELECTRIC, tariff,
)
except NotImplementedError:
return _off_peak_low_rate_gbp_per_kwh(tariff)
high_rate, low_rate = _tariff_high_low_rates_p_per_kwh(tariff)
blended = high_frac * high_rate + (1.0 - high_frac) * low_rate
return blended * _PENCE_TO_GBP
def _secondary_fuel_cost_gbp_per_kwh(
sap_heating,
main: Optional[MainHeatingDetail],
@ -2277,13 +2306,13 @@ def _secondary_fuel_cost_gbp_per_kwh(
# Default to electricity since the default secondary system is
# portable electric heaters (code 693).
if _is_off_peak_meter(meter_type, fuel_is_electric=True):
return _off_peak_low_rate_gbp_per_kwh_via_meter_heuristic(meter_type)
return _secondary_off_peak_rate_gbp_per_kwh(meter_type)
return prices.standard_electricity_p_per_kwh * _PENCE_TO_GBP
# When secondary_fuel_type is electricity, apply off-peak if applicable.
if _is_electric_water(sec_fuel) and _is_off_peak_meter(
meter_type, fuel_is_electric=True
):
return _off_peak_low_rate_gbp_per_kwh_via_meter_heuristic(meter_type)
return _secondary_off_peak_rate_gbp_per_kwh(meter_type)
return prices.unit_price_p_per_kwh(sec_fuel) * _PENCE_TO_GBP

View file

@ -64,6 +64,7 @@ from domain.sap10_calculator.rdsap.cert_to_inputs import (
_pv_overshading_factor, # pyright: ignore[reportPrivateUsage]
_pv_pitch_deg, # pyright: ignore[reportPrivateUsage]
_responsiveness, # pyright: ignore[reportPrivateUsage]
_secondary_fuel_cost_gbp_per_kwh, # pyright: ignore[reportPrivateUsage]
_secondary_heating_fraction_for_category, # pyright: ignore[reportPrivateUsage]
_section_12_4_4_summer_immersion_applies, # pyright: ignore[reportPrivateUsage]
_separately_timed_dhw, # pyright: ignore[reportPrivateUsage]
@ -1912,6 +1913,49 @@ def test_separately_timed_dhw_excludes_dedicated_water_heater_per_table_2b_note_
assert separately_timed is False
def test_secondary_electric_off_peak_bills_at_table_12a_direct_acting_high_rate() -> None:
# Arrange — SAP 10.2 Table 12a Grid 1 (PDF p.191): secondary heating
# is a direct-acting electric room heater (RdSAP 10 §A.2.2 default),
# which sits on the "Other systems including direct-acting electric"
# row. For a 7-hour (Economy-7) tariff that row's high-rate fraction
# is 1.00 — ALL secondary consumption bills at the high rate, NOT the
# off-peak low rate that storage-heater charging earns. Simulated
# case 19's worksheet (242) is the evidence: "Space heating -
# secondary (1.00*15.29 + 0.00*5.50)" → 15.29 p/kWh = £0.1529. Pre-
# slice `_secondary_fuel_cost_gbp_per_kwh` returned the 7-hour low
# rate 5.50 p (£0.0550) for every off-peak electric secondary,
# under-charging by 9.79 p/kWh × the secondary kWh (~£340 on case 19).
storage_heater_main = MainHeatingDetail(
has_fghrs=False,
main_fuel_type=30, # electricity
heat_emitter_type="",
emitter_temperature=1,
main_heating_control=2402,
main_heating_category=None,
sap_main_heating_code=402, # electric storage heaters
)
dual_meter_off_peak_epc = make_minimal_sap10_epc(
total_floor_area_m2=_TYPICAL_TFA_M2,
habitable_rooms_count=4,
country_code="ENG",
sap_heating=make_sap_heating(
main_heating_details=[storage_heater_main],
# secondary_fuel_type omitted → §A.2.2 portable electric default
),
)
# Act
secondary_rate_gbp_per_kwh = _secondary_fuel_cost_gbp_per_kwh(
dual_meter_off_peak_epc.sap_heating,
storage_heater_main,
1, # Dual meter → 7-hour off-peak tariff
SAP_10_2_SPEC_PRICES,
)
# Assert — 1.00 × 15.29 p + 0.00 × 5.50 p = 15.29 p/kWh = £0.1529.
assert abs(secondary_rate_gbp_per_kwh - 0.1529) <= 1e-6
def test_water_efficiency_uses_table_4a_water_column_for_heat_pumps_per_sap_10_2() -> None:
# Arrange — SAP 10.2 Table 4a (PDF p.163-164) gives heat pumps two
# efficiency columns: "space" and "water". For low-temperature