Model/tests/domain/sap10_calculator/test_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

110 lines
4.3 KiB
Python

"""SAP 10.2 Table 13 — high-rate fraction for electric DHW heating.
Locks `electric_dhw_high_rate_fraction` against the published table grid
and the Note-2 clamp at
`domain/sap10_calculator/docs/specs/sap-10-2-full-specification-2025-03-14.pdf`,
page 197. Table 12a's "Immersion water heater" row (PDF p.191) routes
electric immersion DHW here.
The helper evaluates the Note-2 equations, which the spec offers as an
exact alternative to the rounded grid — the pins below check that the
equations reproduce the published 2-dp cells.
"""
from __future__ import annotations
from domain.sap10_calculator.tables.table_12a import Tariff
from domain.sap10_calculator.tables.table_13 import (
electric_dhw_high_rate_fraction,
)
# Appendix J Table 1b occupancy N at a few total floor areas (m²) — the
# anchor for the V/N grid cells below. Computed from the same piecewise
# formula the §4 worksheet uses (water_heating.assumed_occupancy).
_N_AT_TFA_100 = 2.7395 # N(100 m²)
_N_AT_TFA_60 = 1.9816 # N(60 m²)
# Table 13 high-rate-fraction grid cells (PDF p.197), keyed by (floor
# area row, cylinder litres column, tariff, single?) → published value.
_GRID_TOL = 0.005 # the published grid is rounded to 2 dp
def test_table_13_dual_immersion_matches_published_grid() -> None:
# Arrange — SAP 10.2 Table 13 (PDF p.197), 110 L cylinder, dual
# immersion. Floor area 100 m² row: 7-hour = 0.18, 10-hour = 0.10.
# Act
seven = electric_dhw_high_rate_fraction(
cylinder_volume_l=110.0, occupancy_n=_N_AT_TFA_100,
single_immersion=False, tariff=Tariff.SEVEN_HOUR,
)
ten = electric_dhw_high_rate_fraction(
cylinder_volume_l=110.0, occupancy_n=_N_AT_TFA_100,
single_immersion=False, tariff=Tariff.TEN_HOUR,
)
# Assert
assert abs(seven - 0.18) <= _GRID_TOL
assert abs(ten - 0.10) <= _GRID_TOL
def test_table_13_single_immersion_matches_published_grid() -> None:
# Arrange — SAP 10.2 Table 13 (PDF p.197), 110 L cylinder, single
# immersion. Floor area 100 m² row: 7-hour = 0.61, 10-hour = 0.23.
# Single immersion carries a much larger high-rate fraction than dual.
# Act
seven = electric_dhw_high_rate_fraction(
cylinder_volume_l=110.0, occupancy_n=_N_AT_TFA_100,
single_immersion=True, tariff=Tariff.SEVEN_HOUR,
)
ten = electric_dhw_high_rate_fraction(
cylinder_volume_l=110.0, occupancy_n=_N_AT_TFA_100,
single_immersion=True, tariff=Tariff.TEN_HOUR,
)
# Assert
assert abs(seven - 0.61) <= _GRID_TOL
assert abs(ten - 0.23) <= _GRID_TOL
def test_table_13_large_cylinder_single_immersion_clamps_to_zero() -> None:
# Arrange — SAP 10.2 Table 13 Note 2 (PDF p.197): "If these formulae
# give a value less than zero, set the high-rate fraction to zero." A
# 210 L cylinder with single immersion on a 10-hour tariff falls below
# zero (the published 210 L 10-hour column is 0), so the helper clamps.
# Act
fraction = electric_dhw_high_rate_fraction(
cylinder_volume_l=210.0, occupancy_n=_N_AT_TFA_60,
single_immersion=True, tariff=Tariff.TEN_HOUR,
)
# Assert
assert fraction == 0.0
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,
)
twenty_four = electric_dhw_high_rate_fraction(
cylinder_volume_l=110.0, occupancy_n=_N_AT_TFA_100,
single_immersion=False, tariff=Tariff.TWENTY_FOUR_HOUR,
)
# Assert
assert eighteen == 0.0
assert twenty_four == 0.0