Model/domain/sap10_calculator/tables/table_13.py
Khalim Conn-Kowlessar 43d4c67d12 fix(hw-cost): WHC-903 immersion off-peak HW bills at Table 13 high-rate fraction
Electric immersion water heating (WHC 903) on an off-peak tariff billed
100% at the low rate, under-costing the dwelling and over-rating it
(median +0.98 SAP across the off-peak WHC-903 API cohort, n=57).

SAP 10.2 Table 12a "Immersion water heater" row (PDF p.191) routes the
water-heating column to Table 13 (PDF p.197): the high-rate fraction is
a function of cylinder volume V, assumed occupancy N (Appendix J Table
1b) and single-/dual-immersion. The remainder bills at the low rate.
Table 13 Note 2 supplies exact equations equivalent to the rounded grid;
`electric_dhw_high_rate_fraction` evaluates them (validated against the
published 110 L grid cells). Per Note 1 the 10-hour equations cover any
tariff with >=10 hours/day low-rate (so 18-/24-hour use that column).

Immersion code mapping CONFIRMED 1=dual, 2=single via RdSAP 10 §10.5
(PDF p.54 — an immersion is "assumed dual" on a dual/off-peak meter)
cross-checked against the API cohort (code 1 sits 3.6:1 on dual meters;
code 2 on single meters). This INVERTS an earlier handover's unverified
"1=single, 2=dual" note — the dual code carries Table 13's small
fraction, matching the cohort over-rating direction; the single mapping
overshot in a prototype.

API SAP eval: 47.6% -> 48.6% within 0.5; <1.0 62.6% -> 63.8%;
mean|err| 1.586 -> 1.561; 909 computed, 0 raises.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 15:45:41 +00:00

70 lines
2.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]. 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).
"""
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; 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.
"""
if tariff is Tariff.STANDARD:
raise ValueError("Table 13 high-rate fraction is undefined for STANDARD")
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 hours/day at the low rate (10/18/24-hour) — Note 1.
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))