mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
fix(hot-water): default present-but-unsized cylinder to Table 28 Normal 110 L
RdSAP 10 §10.5 (PDF p.55): "If the actual size is not determined, the size of a hot-water cylinder is taken as according to Table 28." When a cylinder is present (has_hot_water_cylinder) but no size descriptor resolves — the gov API lodges cylinder_size=0, or Exact with no measured volume — `_hot_water_ cylinder_volume_l` returned None, silently dropping BOTH the cylinder's storage loss and the Table 13 electric-DHW high-rate fraction, under-costing and over-rating the dwelling. Default such cylinders to the Table 28 baseline "Normal" 110 L (the value §10.7 also instantiates as the first-row default). The context-dependent Inaccessible 210/160 values are deliberately NOT applied here — they are tied to the explicit "Inaccessible" descriptor (code 5) the assessor lodges, not to an unpopulated size field. Scope: 7 of 301 cylinder certs in the corpus (2%). Correctness fix — closes a real spec gap; marginal on the headline (within-0.5 66.1% unchanged, MAE 1.128 -> 1.124) because these certs' residual is dominated by a separate HW- demand gap, not the cylinder. Worksheet harness 47/47 0 diverge (Summary certs lodge a real size, so the fallback never fires). 1 AAA test, pyright net-zero. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
bec62b9167
commit
94275d07cc
3 changed files with 46 additions and 7 deletions
|
|
@ -5223,6 +5223,15 @@ _CYLINDER_SIZE_EXACT: Final[int] = 6
|
|||
_CYLINDER_INACCESSIBLE_DUAL_IMMERSION_L: Final[float] = 210.0
|
||||
_CYLINDER_INACCESSIBLE_SOLID_FUEL_L: Final[float] = 160.0
|
||||
_CYLINDER_INACCESSIBLE_DEFAULT_L: Final[float] = 110.0
|
||||
# RdSAP 10 §10.5 (PDF p.55): "If the actual size is not determined, the size
|
||||
# of a hot-water cylinder is taken as according to Table 28." For a cylinder
|
||||
# present but with no size descriptor lodged (size code 0 / absent), the
|
||||
# baseline Table 28 default is the "Normal" row (110 L) — the same value
|
||||
# §10.7 instantiates as the first-row default. The context-dependent
|
||||
# Inaccessible 210/160 values are NOT applied here: they are tied to the
|
||||
# explicit "Inaccessible" descriptor (code 5) the assessor lodges
|
||||
# deliberately, not to a merely-unpopulated size field.
|
||||
_CYLINDER_SIZE_NOT_DETERMINED_L: Final[float] = 110.0
|
||||
|
||||
|
||||
def _cylinder_inaccessible_volume_l(epc: EpcPropertyData) -> float:
|
||||
|
|
@ -6163,11 +6172,21 @@ def _hot_water_cylinder_volume_l(epc: EpcPropertyData) -> Optional[float]:
|
|||
"""Resolve the HW cylinder volume (litres) from the cert's
|
||||
`cylinder_size` code via RdSAP 10 §10.5 Table 28 — Normal/Medium/Large
|
||||
(codes 2/3/4), Inaccessible (5, context-dependent) and Exact (6, lodged
|
||||
measured volume). Returns None when no cylinder is lodged or no size
|
||||
code resolves."""
|
||||
measured volume). Returns None only when no cylinder is lodged.
|
||||
|
||||
RdSAP 10 §10.5 (PDF p.55): "If the actual size is not determined, the
|
||||
size of a hot-water cylinder is taken as according to Table 28." When a
|
||||
cylinder IS present but no size descriptor resolves (size code 0 /
|
||||
absent, or Exact with no measured volume), fall back to the Table 28
|
||||
baseline "Normal" default (110 L). Without this the cylinder resolved
|
||||
to None, silently dropping BOTH its storage loss and the Table 13
|
||||
high-rate fraction, over-rating unsized-cylinder electric dwellings."""
|
||||
if not epc.has_hot_water_cylinder:
|
||||
return None
|
||||
return _cylinder_volume_l_from_code(epc)
|
||||
volume_l = _cylinder_volume_l_from_code(epc)
|
||||
if volume_l is not None:
|
||||
return volume_l
|
||||
return _CYLINDER_SIZE_NOT_DETERMINED_L
|
||||
|
||||
|
||||
def _immersion_is_single(epc: EpcPropertyData) -> Optional[bool]:
|
||||
|
|
|
|||
|
|
@ -561,6 +561,25 @@ def test_cylinder_size_exact_code_6_uses_lodged_measured_volume() -> None:
|
|||
assert volume_l is not None and abs(volume_l - 150.0) <= 1e-9
|
||||
|
||||
|
||||
def test_cylinder_present_but_size_not_determined_defaults_to_normal_110l() -> None:
|
||||
# Arrange — RdSAP 10 §10.5 (PDF p.55): "If the actual size is not
|
||||
# determined, the size of a hot-water cylinder is taken as according to
|
||||
# Table 28." A cylinder IS present but no size descriptor resolves (gov
|
||||
# API lodges `cylinder_size=0`) → the Table 28 baseline "Normal" default
|
||||
# of 110 L, NOT None (which silently dropped the cylinder's storage loss
|
||||
# AND the Table 13 high-rate fraction, over-rating the dwelling).
|
||||
from domain.sap10_calculator.rdsap.cert_to_inputs import (
|
||||
_hot_water_cylinder_volume_l, # pyright: ignore[reportPrivateUsage]
|
||||
)
|
||||
epc = _cylinder_epc(cylinder_size=0)
|
||||
|
||||
# Act
|
||||
volume_l = _hot_water_cylinder_volume_l(epc)
|
||||
|
||||
# Assert
|
||||
assert volume_l is not None and abs(volume_l - 110.0) <= 1e-9
|
||||
|
||||
|
||||
def test_cylinder_size_inaccessible_code_5_solid_fuel_boiler_uses_160l() -> None:
|
||||
# Arrange — RdSAP 10 §10.5 Table 28: an "Inaccessible" cylinder (code 5)
|
||||
# heated "from a solid fuel boiler" uses 160 litres.
|
||||
|
|
|
|||
|
|
@ -41,10 +41,11 @@ _CORPUS = Path(
|
|||
)
|
||||
|
||||
# Measured floors/ceilings over the fixed corpus at HEAD (1000 certs, 0 skips).
|
||||
# Current: SAP within-0.5 = 66.1%, SAP MAE = 1.128 (Table 12a Grid 1
|
||||
# integrated-storage code-408 0.20 high-rate fraction, this slice: sap408
|
||||
# over-rate +14.6/+12.9/+12.7 -> +7.1/+5.1/+3.4; prior slice was the
|
||||
# heat-network Table 4c(3) flat-rate charging factor).
|
||||
# Current: SAP within-0.5 = 66.1%, SAP MAE = 1.124 (RdSAP 10 §10.5 present-
|
||||
# but-unsized cylinder -> Table 28 Normal 110 L default, this slice — a
|
||||
# correctness fix: 7 certs that silently dropped storage loss + Table 13;
|
||||
# marginal on the headline. Prior slices: Table 12a code-408 0.20 storage
|
||||
# high-rate fraction; heat-network Table 4c(3) flat-rate charging factor).
|
||||
# CO2 MAE = 0.28 t/yr (signed +0.17 — a systematic over-estimate, see below).
|
||||
# PE MAE = 14.7 kWh/m2/yr (signed +9.1).
|
||||
#
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue