Slice S0380.150: SAP 10.2 §12 / Appendix F2 — 18-hour high-rate for pumps + lighting

SAP 10.2 §12 (PDF p.45 lines 2280-2283):

  "The 18-hour tariff is only for use with electric CPSUs with
   sufficient energy storage to provide space (and possibly water)
   heating requirements for 2 hours. Electricity at the low-rate price
   is available for 18 hours per day, with interruptions totalling 6
   hours per day, with the proviso that no interruption will exceed 2
   hours. The low-rate price applies to space and water heating, while
   electricity for all other purposes is at the high-rate price."

SAP 10.2 Appendix F2 (PDF p.63 lines 3809-3812):

  "F2 Electric CPSUs using 18-hour electricity tariff. The 18-hour
   low rate applies to all space heating and water heating provided
   by the CPSU. The CPSU must have sufficient energy stored to provide
   heating during a 2-hour shut-off period. The 18-hour high rate
   applies to all other electricity uses."

Table 12a Grid 2 omits 18-hour / 24-hour from its 7-hour / 10-hour
table; pre-slice the cascade's `_other_fuel_cost_gbp_per_kwh` fell
through Grid 2's `NotImplementedError` to
`prices.standard_electricity_p_per_kwh` (Table 32 code 30 = 13.19
p/kWh). Per §12 + Appendix F2 the 18-hour rule is explicit fraction =
1.0 at the high rate — pumps, fans, and lighting bill at the 18-hour
high rate (Table 32 code 38 = 13.67 p/kWh).

All 41 heating-systems corpus variants lodge `meter_type='18 Hour'`,
so this gap was cohort-wide. Pre-slice the cascade undercounted
pumps + lighting cost by (13.67 − 13.19) × kWh on every variant:

  oil 1            Δcost -£9.31 → -£6.69   (closed £2.62, pumps 265 +
                                            lighting 282 × £0.0048)
  oil pcdb 1/2     Δcost -£8.32 → -£6.29   (closed £2.03)
  oil pcdb 3       Δcost -£8.91 → -£6.29   (closed £2.62)
  pcdb 1           Δcost -£11.10 → -£9.07  (closed £2.03)
  ashp             Δcost -£5.57 → -£4.22   (closed £1.35, lighting only)
  electric 1..9    Δcost shift ~ -£1.35..+£1.35  (lighting only;
                                                  storage / room-heater
                                                  certs carry pumps_fans
                                                  = 0)
  solid fuel 4..11 Δcost ~ -£1.55 (lighting only)
  gshp             Δcost -£26.48 → -£25.12 (closed £1.35)

Pyright net-zero (43 → 43). Extended handover suite: 892 → 893 pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-01 09:34:09 +00:00
parent f20d96369f
commit a658f73613
3 changed files with 80 additions and 26 deletions

View file

@ -219,21 +219,21 @@ class _CorpusExpectation:
# the SH+Sec demand mismatch for electric 3/6/7 (Table 11 fraction # the SH+Sec demand mismatch for electric 3/6/7 (Table 11 fraction
# for codes 401/402) remains the open driver of those SAP residuals. # for codes 401/402) remains the open driver of those SAP residuals.
_EXPECTATIONS: tuple[_CorpusExpectation, ...] = ( _EXPECTATIONS: tuple[_CorpusExpectation, ...] = (
_CorpusExpectation(variant='ashp', block='11a', expected_sap_resid=+0.2418, expected_cost_resid_gbp=-5.5706, expected_co2_resid_kg=-1.4283, expected_pe_resid_kwh=-11.8017), _CorpusExpectation(variant='ashp', block='11a', expected_sap_resid=+0.1830, expected_cost_resid_gbp=-4.2166, expected_co2_resid_kg=-1.4283, expected_pe_resid_kwh=-11.8017),
_CorpusExpectation(variant='electric 1', block='11a', expected_sap_resid=+0.4522, expected_cost_resid_gbp=-10.4203, expected_co2_resid_kg=-4.3334, expected_pe_resid_kwh=-40.1603), _CorpusExpectation(variant='electric 1', block='11a', expected_sap_resid=+0.3849, expected_cost_resid_gbp=-8.8694, expected_co2_resid_kg=-4.3334, expected_pe_resid_kwh=-40.1603),
_CorpusExpectation(variant='electric 2', block='11a', expected_sap_resid=-0.1842, expected_cost_resid_gbp=+4.2439, expected_co2_resid_kg=+38.7768, expected_pe_resid_kwh=+392.8379), _CorpusExpectation(variant='electric 2', block='11a', expected_sap_resid=-0.2430, expected_cost_resid_gbp=+5.5979, expected_co2_resid_kg=+38.7768, expected_pe_resid_kwh=+392.8379),
_CorpusExpectation(variant='electric 3', block='11a', expected_sap_resid=+0.6568, expected_cost_resid_gbp=-15.1334, expected_co2_resid_kg=-13.8238, expected_pe_resid_kwh=-114.7533), _CorpusExpectation(variant='electric 3', block='11a', expected_sap_resid=+0.5980, expected_cost_resid_gbp=-13.7793, expected_co2_resid_kg=-13.8238, expected_pe_resid_kwh=-114.7533),
_CorpusExpectation(variant='electric 5', block='11a', expected_sap_resid=-0.6813, expected_cost_resid_gbp=+15.6982, expected_co2_resid_kg=+43.9325, expected_pe_resid_kwh=+338.5315), _CorpusExpectation(variant='electric 5', block='11a', expected_sap_resid=-0.7401, expected_cost_resid_gbp=+17.0523, expected_co2_resid_kg=+43.9325, expected_pe_resid_kwh=+338.5315),
_CorpusExpectation(variant='electric 6', block='11a', expected_sap_resid=+0.5744, expected_cost_resid_gbp=-13.2352, expected_co2_resid_kg=-10.1354, expected_pe_resid_kwh=-93.1997), _CorpusExpectation(variant='electric 6', block='11a', expected_sap_resid=+0.5156, expected_cost_resid_gbp=-11.8811, expected_co2_resid_kg=-10.1354, expected_pe_resid_kwh=-93.1997),
_CorpusExpectation(variant='electric 7', block='11a', expected_sap_resid=+0.5398, expected_cost_resid_gbp=-12.4372, expected_co2_resid_kg=-8.3964, expected_pe_resid_kwh=-83.9576), _CorpusExpectation(variant='electric 7', block='11a', expected_sap_resid=+0.4810, expected_cost_resid_gbp=-11.0832, expected_co2_resid_kg=-8.3964, expected_pe_resid_kwh=-83.9576),
_CorpusExpectation(variant='electric 8', block='11a', expected_sap_resid=+0.4874, expected_cost_resid_gbp=-11.2307, expected_co2_resid_kg=-6.4095, expected_pe_resid_kwh=-70.5744), _CorpusExpectation(variant='electric 8', block='11a', expected_sap_resid=+0.4286, expected_cost_resid_gbp=-9.8766, expected_co2_resid_kg=-6.4095, expected_pe_resid_kwh=-70.5744),
_CorpusExpectation(variant='electric 9', block='11a', expected_sap_resid=+0.6261, expected_cost_resid_gbp=-14.4253, expected_co2_resid_kg=-12.3507, expected_pe_resid_kwh=-105.2495), _CorpusExpectation(variant='electric 9', block='11a', expected_sap_resid=+0.5673, expected_cost_resid_gbp=-13.0713, expected_co2_resid_kg=-12.3507, expected_pe_resid_kwh=-105.2495),
_CorpusExpectation(variant='gshp', block='11a', expected_sap_resid=+1.1491, expected_cost_resid_gbp=-26.4775, expected_co2_resid_kg=-41.4461, expected_pe_resid_kwh=-454.5023), _CorpusExpectation(variant='gshp', block='11a', expected_sap_resid=+1.0903, expected_cost_resid_gbp=-25.1234, expected_co2_resid_kg=-41.4461, expected_pe_resid_kwh=-454.5023),
_CorpusExpectation(variant='oil 1', block='11a', expected_sap_resid=+0.4042, expected_cost_resid_gbp=-9.3142, expected_co2_resid_kg=-36.6371, expected_pe_resid_kwh=-71.2875), _CorpusExpectation(variant='oil 1', block='11a', expected_sap_resid=+0.2902, expected_cost_resid_gbp=-6.6882, expected_co2_resid_kg=-36.6371, expected_pe_resid_kwh=-71.2875),
_CorpusExpectation(variant='oil pcdb 1', block='11a', expected_sap_resid=+0.3609, expected_cost_resid_gbp=-8.3159, expected_co2_resid_kg=-34.4292, expected_pe_resid_kwh=-67.1831), _CorpusExpectation(variant='oil pcdb 1', block='11a', expected_sap_resid=+0.2728, expected_cost_resid_gbp=-6.2850, expected_co2_resid_kg=-34.4292, expected_pe_resid_kwh=-67.1831),
_CorpusExpectation(variant='oil pcdb 2', block='11a', expected_sap_resid=+0.3609, expected_cost_resid_gbp=-8.3159, expected_co2_resid_kg=-34.4292, expected_pe_resid_kwh=-67.1831), _CorpusExpectation(variant='oil pcdb 2', block='11a', expected_sap_resid=+0.2728, expected_cost_resid_gbp=-6.2850, expected_co2_resid_kg=-34.4292, expected_pe_resid_kwh=-67.1831),
_CorpusExpectation(variant='oil pcdb 3', block='11a', expected_sap_resid=+0.3869, expected_cost_resid_gbp=-8.9139, expected_co2_resid_kg=-34.4447, expected_pe_resid_kwh=-67.2071), _CorpusExpectation(variant='oil pcdb 3', block='11a', expected_sap_resid=+0.2729, expected_cost_resid_gbp=-6.2879, expected_co2_resid_kg=-34.4447, expected_pe_resid_kwh=-67.2071),
_CorpusExpectation(variant='pcdb 1', block='11a', expected_sap_resid=+0.5018, expected_cost_resid_gbp=-11.0973, expected_co2_resid_kg=-49.6654, expected_pe_resid_kwh=-92.8147), _CorpusExpectation(variant='pcdb 1', block='11a', expected_sap_resid=+0.4096, expected_cost_resid_gbp=-9.0664, expected_co2_resid_kg=-49.6654, expected_pe_resid_kwh=-92.8147),
# Slice S0380.133 unblocked 10 solid-fuel variants by routing the # Slice S0380.133 unblocked 10 solid-fuel variants by routing the
# Elmhurst §14.0 "Main Heating EES Code" through the new # Elmhurst §14.0 "Main Heating EES Code" through the new
# `_ELMHURST_MAIN_HEATING_EES_TO_FUEL_CODE` dict. Pre-slice the # `_ELMHURST_MAIN_HEATING_EES_TO_FUEL_CODE` dict. Pre-slice the
@ -241,16 +241,16 @@ _EXPECTATIONS: tuple[_CorpusExpectation, ...] = (
# cost / CO2 / PE all route via the correct Table 32 fuel code. # cost / CO2 / PE all route via the correct Table 32 fuel code.
# Remaining residuals are likely heating-system efficiency or # Remaining residuals are likely heating-system efficiency or
# control-type gaps — separate slices. # control-type gaps — separate slices.
_CorpusExpectation(variant='solid fuel 2', block='11a', expected_sap_resid=+3.1478, expected_cost_resid_gbp=-72.5305, expected_co2_resid_kg=+41.5584, expected_pe_resid_kwh=-1346.0016), _CorpusExpectation(variant='solid fuel 2', block='11a', expected_sap_resid=+3.0805, expected_cost_resid_gbp=-70.9797, expected_co2_resid_kg=+41.5584, expected_pe_resid_kwh=-1346.0016),
_CorpusExpectation(variant='solid fuel 3', block='11a', expected_sap_resid=+1.8310, expected_cost_resid_gbp=-42.1903, expected_co2_resid_kg=-441.0048, expected_pe_resid_kwh=-1069.2375), _CorpusExpectation(variant='solid fuel 3', block='11a', expected_sap_resid=+1.7637, expected_cost_resid_gbp=-40.6395, expected_co2_resid_kg=-441.0048, expected_pe_resid_kwh=-1069.2375),
_CorpusExpectation(variant='solid fuel 4', block='11a', expected_sap_resid=+0.4523, expected_cost_resid_gbp=-10.4208, expected_co2_resid_kg=-86.4442, expected_pe_resid_kwh=-106.8858), _CorpusExpectation(variant='solid fuel 4', block='11a', expected_sap_resid=+0.3935, expected_cost_resid_gbp=-9.0668, expected_co2_resid_kg=-86.4442, expected_pe_resid_kwh=-106.8858),
_CorpusExpectation(variant='solid fuel 5', block='11a', expected_sap_resid=+0.3440, expected_cost_resid_gbp=-7.9255, expected_co2_resid_kg=-56.6651, expected_pe_resid_kwh=-41.8008), _CorpusExpectation(variant='solid fuel 5', block='11a', expected_sap_resid=+0.2767, expected_cost_resid_gbp=-6.3746, expected_co2_resid_kg=-56.6651, expected_pe_resid_kwh=-41.8008),
_CorpusExpectation(variant='solid fuel 6', block='11a', expected_sap_resid=+0.5376, expected_cost_resid_gbp=-12.3864, expected_co2_resid_kg=-11.6812, expected_pe_resid_kwh=-89.8541), _CorpusExpectation(variant='solid fuel 6', block='11a', expected_sap_resid=+0.4703, expected_cost_resid_gbp=-10.8355, expected_co2_resid_kg=-11.6812, expected_pe_resid_kwh=-89.8541),
_CorpusExpectation(variant='solid fuel 7', block='11a', expected_sap_resid=+0.6029, expected_cost_resid_gbp=-14.0701, expected_co2_resid_kg=-87.4488, expected_pe_resid_kwh=-117.8475), _CorpusExpectation(variant='solid fuel 7', block='11a', expected_sap_resid=+0.5361, expected_cost_resid_gbp=-12.5193, expected_co2_resid_kg=-87.4488, expected_pe_resid_kwh=-117.8475),
_CorpusExpectation(variant='solid fuel 8', block='11a', expected_sap_resid=+0.4291, expected_cost_resid_gbp=-9.8880, expected_co2_resid_kg=+5.6990, expected_pe_resid_kwh=-89.4580), _CorpusExpectation(variant='solid fuel 8', block='11a', expected_sap_resid=+0.3618, expected_cost_resid_gbp=-8.3371, expected_co2_resid_kg=+5.6990, expected_pe_resid_kwh=-89.4580),
_CorpusExpectation(variant='solid fuel 9', block='11a', expected_sap_resid=+0.5486, expected_cost_resid_gbp=-12.6405, expected_co2_resid_kg=+1.6494, expected_pe_resid_kwh=-103.7659), _CorpusExpectation(variant='solid fuel 9', block='11a', expected_sap_resid=+0.4898, expected_cost_resid_gbp=-11.2865, expected_co2_resid_kg=+1.6494, expected_pe_resid_kwh=-103.7659),
_CorpusExpectation(variant='solid fuel 10', block='11a', expected_sap_resid=+0.5837, expected_cost_resid_gbp=-13.4482, expected_co2_resid_kg=-0.2410, expected_pe_resid_kwh=-130.1413), _CorpusExpectation(variant='solid fuel 10', block='11a', expected_sap_resid=+0.5249, expected_cost_resid_gbp=-12.0942, expected_co2_resid_kg=-0.2410, expected_pe_resid_kwh=-130.1413),
_CorpusExpectation(variant='solid fuel 11', block='11a', expected_sap_resid=+0.4809, expected_cost_resid_gbp=-11.0799, expected_co2_resid_kg=+5.5072, expected_pe_resid_kwh=-92.4917), _CorpusExpectation(variant='solid fuel 11', block='11a', expected_sap_resid=+0.4221, expected_cost_resid_gbp=-9.7259, expected_co2_resid_kg=+5.5072, expected_pe_resid_kwh=-92.4917),
) )

View file

@ -1985,7 +1985,17 @@ def _other_fuel_cost_gbp_per_kwh(
is on an off-peak tariff, applies the Table 12a Grid 2 is on an off-peak tariff, applies the Table 12a Grid 2
ALL_OTHER_USES high-rate fraction blended Table 32 rate. Standard ALL_OTHER_USES high-rate fraction blended Table 32 rate. Standard
tariff bypasses to the prices table's flat scalar (preserves the tariff bypasses to the prices table's flat scalar (preserves the
cohort fixture cost cascade at 1e-4).""" cohort fixture cost cascade at 1e-4).
SAP 10.2 §12 (PDF p.45) + Appendix F2 (PDF p.63) for the 18-hour
tariff, "the 18-hour high rate applies to all other electricity
uses" (i.e. fraction = 1.0 at the high rate). Table 12a Grid 2 omits
18-hour and 24-hour from its 7-hour/10-hour table; for 18-hour the
spec rule is explicit (fraction 1.0 at the high rate per Appendix
F2), so route directly to the 18-hour high rate (Table 32 code 38 =
13.67 p/kWh). 24-hour heating tariff is a heating-only single-rate
tariff (Table 32 code 35 = 6.61 p/kWh) non-heating uses fall back
to the standard electricity rate."""
if tariff is Tariff.STANDARD: if tariff is Tariff.STANDARD:
return prices.standard_electricity_p_per_kwh * _PENCE_TO_GBP return prices.standard_electricity_p_per_kwh * _PENCE_TO_GBP
try: try:
@ -1993,6 +2003,9 @@ def _other_fuel_cost_gbp_per_kwh(
OtherUse.ALL_OTHER_USES, tariff, OtherUse.ALL_OTHER_USES, tariff,
) )
except NotImplementedError: except NotImplementedError:
if tariff is Tariff.EIGHTEEN_HOUR:
high_rate, _low = _tariff_high_low_rates_p_per_kwh(tariff)
return high_rate * _PENCE_TO_GBP
return prices.standard_electricity_p_per_kwh * _PENCE_TO_GBP return prices.standard_electricity_p_per_kwh * _PENCE_TO_GBP
high_rate, low_rate = _tariff_high_low_rates_p_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 blended = high_frac * high_rate + (1.0 - high_frac) * low_rate

View file

@ -45,6 +45,7 @@ from domain.sap10_calculator.rdsap.cert_to_inputs import (
_is_electric_water, # pyright: ignore[reportPrivateUsage] _is_electric_water, # pyright: ignore[reportPrivateUsage]
_is_off_peak_meter, # pyright: ignore[reportPrivateUsage] _is_off_peak_meter, # pyright: ignore[reportPrivateUsage]
_main_floor_u_value, # pyright: ignore[reportPrivateUsage] _main_floor_u_value, # pyright: ignore[reportPrivateUsage]
_other_fuel_cost_gbp_per_kwh, # pyright: ignore[reportPrivateUsage]
_pv_overshading_factor, # pyright: ignore[reportPrivateUsage] _pv_overshading_factor, # pyright: ignore[reportPrivateUsage]
_pv_pitch_deg, # pyright: ignore[reportPrivateUsage] _pv_pitch_deg, # pyright: ignore[reportPrivateUsage]
_responsiveness, # pyright: ignore[reportPrivateUsage] _responsiveness, # pyright: ignore[reportPrivateUsage]
@ -1398,6 +1399,46 @@ def test_tariff_high_low_rates_full_dispatch_coverage() -> None:
assert excinfo.value.field == "tariff_high_low_rates" assert excinfo.value.field == "tariff_high_low_rates"
def test_other_fuel_cost_for_18_hour_tariff_uses_18_hour_high_rate() -> None:
# Arrange — SAP 10.2 §12 (PDF p.45) and Appendix F2 (PDF p.63) both
# specify that for the 18-hour tariff "the 18-hour high rate applies
# to all other electricity uses" (pumps, fans, lighting). Table 12a
# Grid 2 only lists 7-hour / 10-hour fractions; for 18-hour the spec
# rule is implicit fraction = 1.0 at the high rate per Appendix F2.
#
# Pre-slice the cascade fell through Grid 2's NotImplementedError
# to `prices.standard_electricity_p_per_kwh` (Table 32 code 30 =
# 13.19 p/kWh), under-counting pumps + lighting cost on every
# 18-hour cert by (13.67 13.19) × kWh. The 41-variant heating-
# systems corpus all lodges `meter_type='18 Hour'`, so this gap is
# cohort-wide.
#
# Spec verbatim, SAP 10.2 §12 (lines 2280-2283):
# "The 18-hour tariff is only for use with electric CPSUs ...
# The low-rate price applies to space and water heating, while
# electricity for all other purposes is at the high-rate price."
#
# Spec verbatim, SAP 10.2 Appendix F2 (lines 3809-3812):
# "F2 Electric CPSUs using 18-hour electricity tariff. The 18-hour
# low rate applies to all space heating and water heating
# provided by the CPSU. ... The 18-hour high rate applies to all
# other electricity uses."
#
# Table 32 code 38 (18-hour high rate) = 13.67 p/kWh =
# 0.1367 £/kWh.
from domain.sap10_calculator.tables.table_12a import Tariff
# Act
rate_18h = _other_fuel_cost_gbp_per_kwh(Tariff.EIGHTEEN_HOUR, SAP_10_2_SPEC_PRICES)
# Assert
assert abs(rate_18h - 0.1367) <= 1e-6, (
f"18-hour tariff other-uses rate {rate_18h:.6f} £/kWh "
f"should equal Table 32 code 38 high rate 0.1367 £/kWh per "
f"SAP 10.2 §12 / Appendix F2"
)
def test_is_off_peak_meter_recognises_bare_18_hour_lodging() -> None: def test_is_off_peak_meter_recognises_bare_18_hour_lodging() -> None:
# Arrange — RdSAP 10 §17 page 85 row 10-2 lodges 18-hour meter # Arrange — RdSAP 10 §17 page 85 row 10-2 lodges 18-hour meter
# as the bare "18-hour" or "18 Hour" form (Elmhurst Summary §14.2 # as the bare "18-hour" or "18 Hour" form (Elmhurst Summary §14.2