Cylinder with unknown insulation defaults per RdSAP Table 29 age band 🟥

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jun-te Kim 2026-07-02 16:09:46 +00:00
parent 233f085fb3
commit 6accf365d7

View file

@ -6523,6 +6523,79 @@ def test_elmhurst_no_insulation_cylinder_thickness_zero_takes_uninsulated_loss()
assert abs(got_jan_kwh - expected_jan_kwh) < 1e-4
def test_cylinder_with_unknown_insulation_defaults_per_table_29_age_band() -> None:
"""RdSAP 10 Table 29 (PDF p.56) "Hot water cylinder insulation if not
accessible": when a cylinder is lodged but its insulation is unknown
(type absent AND no thickness ADR-0028 records ~308/1000 17.0 certs
omitting `cylinder_insulation_type`), the age-band default applies:
bands A-F 12 mm loose jacket, G/H 25 mm factory, I-M 38 mm
factory. This is the same Table 29 row the §10.7 no-water-heating
default already uses. Previously the cascade returned None (zero
storage loss), silently treating an unknown cylinder as a combi.
Distinct from the uninsulated case: a lodged 0 mm means NO insulation
(t = 0), an absent thickness means NOT ACCESSIBLE (Table 29).
"""
# Arrange — same dwelling, insulation fields both absent; the
# fixture's building part lodges construction_age_band "B" → the
# Table 29 A-F row (12 mm loose jacket).
from domain.sap10_calculator.worksheet.water_heating import (
cylinder_storage_loss_factor_table_2,
cylinder_temperature_factor_table_2b,
cylinder_volume_factor_table_2a,
)
hp_main = MainHeatingDetail(
has_fghrs=False,
main_fuel_type=29,
heat_emitter_type=1,
emitter_temperature=1,
main_heating_control=2206,
main_heating_category=4,
sap_main_heating_code=None,
)
epc = make_minimal_sap10_epc(
total_floor_area_m2=_TYPICAL_TFA_M2,
habitable_rooms_count=4,
country_code="ENG",
has_hot_water_cylinder=True,
sap_building_parts=[make_building_part()], # age band B
sap_heating=make_sap_heating(
main_heating_details=[hp_main],
water_heating_code=901,
cylinder_size=3, # Medium → 160 L
cylinder_insulation_type=None, # not lodged
cylinder_insulation_thickness_mm=None, # not lodged
cylinder_thermostat="Y",
),
)
expected_jan_kwh = (
160.0
* cylinder_storage_loss_factor_table_2(
insulation_type="loose_jacket", thickness_mm=12.0
)
* cylinder_volume_factor_table_2a(160.0)
* cylinder_temperature_factor_table_2b(
has_cylinder_thermostat=True, separately_timed_dhw=True
)
* 31
)
# Act
wh_result, _ = _water_heating_worksheet_and_gains(
epc=epc,
water_efficiency_pct=1.7,
is_instantaneous=False,
primary_age="D",
pcdb_record=None,
)
# Assert — the Table 29 band-B default (12 mm loose jacket), not the
# zero-loss combi default and not the harsher uninsulated t=0.
assert wh_result is not None
got_jan_kwh = wh_result.solar_storage_monthly_kwh[0]
assert abs(got_jan_kwh - expected_jan_kwh) < 1e-4
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,