fix(water-heating): 18-/24-hour immersion DHW bills 100% low-rate (Table 12a scope)

SAP 10.2 Table 12a (PDF p.191) is titled "High-rate fractions for systems
using 7-hour and 10-hour tariffs"; its "Immersion water heater" row lists
the tariff as "7-hour or 10-hour" only, routing to Table 13. An 18-hour or
24-hour tariff is OUTSIDE the table's scope — it provides at least 18
hours/day at the low rate, more than enough to heat any immersion cylinder
off-peak, so the high-rate fraction is 0 (all DHW billed at the low rate).

`electric_dhw_high_rate_fraction` previously mapped 18-/24-hour to the
10-hour equations (returning ~0.10 for a 110 L dual immersion) on an
over-literal reading of Table 13 Note 1 ("at least 10 hours"). The Elmhurst
dr87 worksheet for solid fuel 5 (cert 001431: 18-hour meter, 110 L dual
immersion, WHC 903) refutes that: HW (245) high-rate = 0.0 kWh, (246)
low-rate = 100%. Table 12a's title bounds the table to the two named
tariffs; 18-/24-hour fall outside it.

Resolves the Table-13 blocker on the immersion-extractor fix: once the
Summary extractor captures the dual immersion, the 18-hour solid-fuel
corpus certs stay at high_frac=0 (matching their worksheets) instead of
regressing to the 10-hour-column 0.10.

API SAP eval unchanged: 57.6% within 0.5, mean|err| 1.185, signed -0.165
(the cached sample has no 18-hour WHC-903 certs; one 24-hour cert shifts
sub-threshold). Regression gate green (3 pre-existing fails unrelated).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-10 22:01:35 +00:00
parent 020ac6f220
commit 0202b045de
2 changed files with 44 additions and 19 deletions

View file

@ -22,12 +22,24 @@ is needed:
Single: [(14530 - 762 N) / (1.5 V) - 80 + 10 N] / 100
where V is the cylinder volume (litres) and N is the assumed occupancy
(Appendix J Table 1b). Per Note 2 the result is clamped to [0, 1]. Per
Note 1 the 10-hour equations apply to any tariff providing at least 10
hours/day at the low rate (so 18-hour and 24-hour use the 10-hour
column). Heat pumps providing water heating only are treated as dual
immersion (Note 1) out of scope of this helper (callers route those
via Table 12a).
(Appendix J Table 1b). Per Note 2 the result is clamped to [0, 1].
Table 12a (PDF p.191) whose title reads "High-rate fractions for
systems using 7-hour and 10-hour tariffs" — routes the "Immersion water
heater" row to Table 13 for the tariff "7-hour or 10-hour" ONLY. An
18-hour or 24-hour tariff is outside Table 12a/13's scope: it provides
at least 18 hours/day at the low rate, more than enough to heat any
immersion cylinder off-peak, so the high-rate fraction is 0 (all DHW
billed at the low rate). The Elmhurst dr87 worksheet for solid fuel 5
(cert 001431: 18-hour meter, 110 L dual immersion, WHC 903) confirms
this HW (245) high-rate = 0.0 kWh, (246) low-rate = 100%. (An earlier
reading mapped 18-/24-hour to the 10-hour column via Note 1's "at least
10 hours"; the worksheet refutes it — Table 12a's title bounds the table
to the two named tariffs.)
Heat pumps providing water heating only are treated as dual immersion
(Note 1) out of scope of this helper (callers route those via Table
12a).
"""
from __future__ import annotations
@ -47,13 +59,19 @@ def electric_dhw_high_rate_fraction(
`single_immersion` selects the single- vs dual-immersion equation
(RdSAP10 §10.5 p.54: an immersion is assumed dual on a dual meter).
The 7-hour tariff uses the 7-hour equations; every other off-peak
tariff (10/18/24-hour, all >= 10 hours low-rate per Note 1) uses the
10-hour equations. STANDARD has no off-peak split and is rejected
callers must early-return before this fires.
The 7-hour tariff uses the 7-hour equations; the 10-hour tariff uses
the 10-hour equations. The 18-hour and 24-hour tariffs are outside
Table 12a/13's "7-hour and 10-hour" scope (PDF p.191 title) — they
provide >= 18 hours/day at the low rate, so the high-rate fraction is
0. STANDARD has no off-peak split and is rejected callers must
early-return before this fires.
"""
if tariff is Tariff.STANDARD:
raise ValueError("Table 13 high-rate fraction is undefined for STANDARD")
if tariff in (Tariff.EIGHTEEN_HOUR, Tariff.TWENTY_FOUR_HOUR):
# Outside Table 12a's 7-hour/10-hour scope — >= 18 h/day low rate
# heats the cylinder entirely off-peak (high-rate fraction 0).
return 0.0
v = cylinder_volume_l
n = occupancy_n
if tariff is Tariff.SEVEN_HOUR:
@ -62,7 +80,7 @@ def electric_dhw_high_rate_fraction(
else:
fraction = ((6.8 - 0.024 * v) * n + 14 - 0.07 * v) / 100
else:
# >= 10 hours/day at the low rate (10/18/24-hour) — Note 1.
# 10-hour tariff (18-/24-hour handled above as out-of-scope).
if single_immersion:
fraction = ((14530 - 762 * n) / (1.5 * v) - 80 + 10 * n) / 100
else:

View file

@ -83,21 +83,28 @@ def test_table_13_large_cylinder_single_immersion_clamps_to_zero() -> None:
assert fraction == 0.0
def test_table_13_eighteen_hour_uses_ten_hour_column() -> None:
# Arrange — SAP 10.2 Table 13 Note 1 (PDF p.197): the table applies
# "for tariffs providing at least 10 hours ... at the low rate", so an
# 18-hour tariff resolves to the 10-hour equations, not a separate
# column.
def test_table_13_eighteen_and_twenty_four_hour_bill_full_low_rate() -> None:
# Arrange — SAP 10.2 Table 12a (PDF p.191) is titled "High-rate
# fractions for systems using 7-hour and 10-hour tariffs"; its
# "Immersion water heater" row lists the tariff as "7-hour or 10-hour"
# only, routing to Table 13. An 18-hour / 24-hour tariff is OUTSIDE the
# table's scope: it provides at least 18 hours/day at the low rate, more
# than enough to heat any immersion cylinder off-peak, so the high-rate
# fraction is 0 (100% billed at the low rate). The Elmhurst dr87
# worksheet for solid fuel 5 (cert 001431: 18-hour meter, 110 L dual
# immersion, WHC 903) bills HW (245) high-rate = 0.0 kWh, (246) low-rate
# = 100% — confirming high_frac = 0 for an 18-hour immersion DHW.
# Act
eighteen = electric_dhw_high_rate_fraction(
cylinder_volume_l=110.0, occupancy_n=_N_AT_TFA_100,
single_immersion=False, tariff=Tariff.EIGHTEEN_HOUR,
)
ten = electric_dhw_high_rate_fraction(
twenty_four = electric_dhw_high_rate_fraction(
cylinder_volume_l=110.0, occupancy_n=_N_AT_TFA_100,
single_immersion=False, tariff=Tariff.TEN_HOUR,
single_immersion=False, tariff=Tariff.TWENTY_FOUR_HOUR,
)
# Assert
assert abs(eighteen - ten) <= 1e-9
assert eighteen == 0.0
assert twenty_four == 0.0