Model/domain/sap10_calculator/tables/table_13.py
Khalim Conn-Kowlessar 0202b045de 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>
2026-06-10 22:01:35 +00:00

88 lines
3.9 KiB
Python

"""SAP 10.2 Table 13 — high-rate fraction for electric DHW heating.
Sourced verbatim from `domain/sap10_calculator/docs/specs/sap-10-2-full-
specification-2025-03-14.pdf`, page 197 (Table 13). RdSAP10 §10.5 (PDF
p.54) routes electric immersion water heating here via Table 12a's
"Immersion water heater" row, whose Water-heating column reads "Fraction
from Table 13".
The table gives the fraction of DHW electricity consumed at the HIGH
rate for a cylinder with a single or dual immersion heater on an
off-peak tariff; the remainder is at the low rate. Note 2 of the table
supplies exact equations equivalent to the tabulated (rounded) grid —
this module evaluates those equations, so no floor-area interpolation
is needed:
7-hour tariff (>= 7 hours/day at the low rate)
Dual: [(6.8 - 0.024 V) N + 14 - 0.07 V] / 100
Single: [(14530 - 762 N) / V - 80 + 10 N] / 100
10-hour tariff (>= 10 hours/day at the low rate)
Dual: [(6.8 - 0.036 V) N + 14 - 0.105 V] / 100
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].
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
from domain.sap10_calculator.tables.table_12a import Tariff
def electric_dhw_high_rate_fraction(
*,
cylinder_volume_l: float,
occupancy_n: float,
single_immersion: bool,
tariff: Tariff,
) -> float:
"""SAP 10.2 Table 13 (PDF p.197) high-rate fraction for an electric
immersion DHW cylinder on an off-peak tariff.
`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; 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:
if single_immersion:
fraction = ((14530 - 762 * n) / v - 80 + 10 * n) / 100
else:
fraction = ((6.8 - 0.024 * v) * n + 14 - 0.07 * v) / 100
else:
# 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:
fraction = ((6.8 - 0.036 * v) * n + 14 - 0.105 * v) / 100
return max(0.0, min(1.0, fraction))