mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
S0380.225: §10.7 no-water-heating default — A-F → 12mm loose jacket
The §10.7 no-water-heating default cylinder raised UnmappedSapCode for age bands A-F (2 certs in a 2026 sample, bands B + C) because Table 29's "A to F: 12 mm loose jacket" row wasn't plumbed — the loose-jacket storage-loss branch didn't exist. S0380.224 added it, so this slice completes the Table 29 lookup. Restructure _TABLE_29_DEFAULT_CYLINDER_INSULATION_BY_AGE to carry (cylinder_insulation_type, thickness_mm) per band — A-F → (loose jacket, 12), G/H → (factory, 25), I-M → (factory, 38) per RdSAP 10 Table 29 (PDF p.56) — and have the default read both, setting the loose-jacket type for A-F instead of hardcoding factory. The strict-raise is retained only for an absent / out-of-A-M age band (no Table 29 row). Validated: certs 2211 (band B, SAP 49.8 vs lodged 52) and 3420 (band C, 11.2 vs 11) now compute. §4 + golden suite 2395 passed — the corpus "no system" cert (age G, 25 mm factory) is unchanged. cert_to_inputs.py pyright unchanged at 32; new test suppresses reportPrivateUsage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
2e351be957
commit
9c0a373f7d
2 changed files with 57 additions and 11 deletions
|
|
@ -4616,11 +4616,25 @@ _WHC_NO_WATER_HEATING_SYSTEM: Final[int] = 999
|
|||
# "Immersion Heater Type: Single" so the single-immersion path is used.
|
||||
_CYLINDER_SIZE_CODE_NORMAL_110L: Final[int] = 2
|
||||
# RdSAP 10 Table 29 (PDF p.56) "Hot water cylinder insulation if not
|
||||
# accessible" — the §10.7 default cylinder uses the age-band insulation,
|
||||
# same rule as the inaccessible-cylinder path: A-F → 12 mm loose jacket
|
||||
# (not yet plumbed — strict-raise), G/H → 25 mm foam, I-M → 38 mm foam.
|
||||
_TABLE_29_DEFAULT_CYLINDER_INSULATION_MM_BY_AGE: Final[dict[str, int]] = {
|
||||
"G": 25, "H": 25, "I": 38, "J": 38, "K": 38, "L": 38, "M": 38,
|
||||
# accessible" — the §10.7 default cylinder uses the age-band insulation:
|
||||
# "Age band of main property A to F: 12 mm loose jacket", G/H → 25 mm
|
||||
# foam, I-M → 38 mm foam. Each entry is (cylinder_insulation_type,
|
||||
# thickness_mm); the loose-jacket branch is now plumbed (S0380.224) so
|
||||
# A-F resolves instead of raising.
|
||||
_TABLE_29_DEFAULT_CYLINDER_INSULATION_BY_AGE: Final[dict[str, tuple[int, int]]] = {
|
||||
"A": (_CYLINDER_INSULATION_TYPE_LOOSE_JACKET, 12),
|
||||
"B": (_CYLINDER_INSULATION_TYPE_LOOSE_JACKET, 12),
|
||||
"C": (_CYLINDER_INSULATION_TYPE_LOOSE_JACKET, 12),
|
||||
"D": (_CYLINDER_INSULATION_TYPE_LOOSE_JACKET, 12),
|
||||
"E": (_CYLINDER_INSULATION_TYPE_LOOSE_JACKET, 12),
|
||||
"F": (_CYLINDER_INSULATION_TYPE_LOOSE_JACKET, 12),
|
||||
"G": (_CYLINDER_INSULATION_TYPE_FACTORY, 25),
|
||||
"H": (_CYLINDER_INSULATION_TYPE_FACTORY, 25),
|
||||
"I": (_CYLINDER_INSULATION_TYPE_FACTORY, 38),
|
||||
"J": (_CYLINDER_INSULATION_TYPE_FACTORY, 38),
|
||||
"K": (_CYLINDER_INSULATION_TYPE_FACTORY, 38),
|
||||
"L": (_CYLINDER_INSULATION_TYPE_FACTORY, 38),
|
||||
"M": (_CYLINDER_INSULATION_TYPE_FACTORY, 38),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -4643,9 +4657,8 @@ def _apply_rdsap_no_water_heating_system_default(
|
|||
Elmhurst engine's worksheet header for the corpus "no system" cert
|
||||
(WHS 903, Single immersion, 110 L cylinder, 25 mm foam at age G).
|
||||
|
||||
Raises `UnmappedSapCode` for age bands A-F (12 mm loose jacket) —
|
||||
no corpus member exercises that combination and the SAP 10.2 Table 2
|
||||
loss-factor dispatch only has the factory-foam path plumbed.
|
||||
Raises `UnmappedSapCode` only when the main dwelling's age band is
|
||||
absent / outside A-M (no Table 29 row to apply).
|
||||
"""
|
||||
if epc.sap_heating.water_heating_code != _WHC_NO_WATER_HEATING_SYSTEM:
|
||||
return epc
|
||||
|
|
@ -4654,17 +4667,18 @@ def _apply_rdsap_no_water_heating_system_default(
|
|||
if epc.sap_building_parts else None
|
||||
)
|
||||
band = (age_band or "")[:1].upper()
|
||||
thickness_mm = _TABLE_29_DEFAULT_CYLINDER_INSULATION_MM_BY_AGE.get(band)
|
||||
if thickness_mm is None:
|
||||
default = _TABLE_29_DEFAULT_CYLINDER_INSULATION_BY_AGE.get(band)
|
||||
if default is None:
|
||||
raise UnmappedSapCode(
|
||||
"rdsap_10_7_default_cylinder_insulation_age_band", age_band
|
||||
)
|
||||
insulation_type_code, thickness_mm = default
|
||||
sap_heating = replace(
|
||||
epc.sap_heating,
|
||||
water_heating_code=_WHC_ELECTRIC_IMMERSION,
|
||||
water_heating_fuel=_STANDARD_ELECTRICITY_FUEL_CODE,
|
||||
cylinder_size=_CYLINDER_SIZE_CODE_NORMAL_110L,
|
||||
cylinder_insulation_type=_CYLINDER_INSULATION_TYPE_FACTORY,
|
||||
cylinder_insulation_type=insulation_type_code,
|
||||
cylinder_insulation_thickness_mm=thickness_mm,
|
||||
cylinder_thermostat="Y",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4032,6 +4032,38 @@ def test_loose_jacket_cylinder_computes_storage_loss_via_table2_loose_jacket_bra
|
|||
assert got_jan_kwh > 36.9530 # loose jacket loses more than factory
|
||||
|
||||
|
||||
def test_no_water_heating_default_age_a_to_f_uses_12mm_loose_jacket_per_table_29() -> None:
|
||||
"""RdSAP 10 §10.7 + Table 29 (PDF p.55-56): when no water heating
|
||||
system is lodged, the default cylinder takes the age-band insulation,
|
||||
and "Age band of main property A to F: 12 mm loose jacket". Bands
|
||||
A-F previously raised UnmappedSapCode because the loose-jacket storage
|
||||
loss branch wasn't plumbed (now it is, S0380.224). A band-B cert must
|
||||
resolve to a 12 mm loose-jacket cylinder; band G stays 25 mm factory.
|
||||
"""
|
||||
from domain.sap10_calculator.rdsap.cert_to_inputs import _apply_rdsap_no_water_heating_system_default # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
def _no_dhw_epc(age_band: str) -> EpcPropertyData:
|
||||
return make_minimal_sap10_epc(
|
||||
total_floor_area_m2=_TYPICAL_TFA_M2,
|
||||
habitable_rooms_count=4,
|
||||
country_code="ENG",
|
||||
sap_building_parts=[make_building_part(construction_age_band=age_band)],
|
||||
sap_heating=make_sap_heating(water_heating_code=999),
|
||||
)
|
||||
|
||||
# Act — band B (A-F band) + band G (factory band, regression guard).
|
||||
band_b = _apply_rdsap_no_water_heating_system_default(_no_dhw_epc("B"))
|
||||
band_g = _apply_rdsap_no_water_heating_system_default(_no_dhw_epc("G"))
|
||||
|
||||
# Assert — band B → 12 mm loose jacket (type 2); band G → 25 mm
|
||||
# factory (type 1). Both gain the immersion + 110 L cylinder default.
|
||||
assert band_b.has_hot_water_cylinder is True
|
||||
assert band_b.sap_heating.cylinder_insulation_type == 2 # loose jacket
|
||||
assert band_b.sap_heating.cylinder_insulation_thickness_mm == 12
|
||||
assert band_g.sap_heating.cylinder_insulation_type == 1 # factory
|
||||
assert band_g.sap_heating.cylinder_insulation_thickness_mm == 25
|
||||
|
||||
|
||||
def test_cert_with_hot_water_cylinder_computes_primary_loss_59m_from_sap_table_3() -> None:
|
||||
"""SAP 10.2 §4 line 7700 + Table 3 (PDF p.159) define the primary
|
||||
circuit loss for an indirect cylinder:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue