From 4cf3db388ff0deae72ed3d415648f356b21ba79e Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Thu, 2 Jul 2026 16:07:20 +0000 Subject: [PATCH] =?UTF-8?q?Elmhurst=20'No=20Insulation'=20cylinder=20(0=20?= =?UTF-8?q?mm=20lodged)=20takes=20the=20uninsulated=20storage=20loss=20?= =?UTF-8?q?=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- .../rdsap/test_cert_to_inputs.py | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py b/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py index 0f20e7a39..62d75c22d 100644 --- a/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py +++ b/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py @@ -6451,6 +6451,78 @@ def test_uninsulated_cylinder_computes_storage_loss_via_table2_jacket_t0() -> No assert got_jan_kwh > jacket_50mm_jan_kwh +def test_elmhurst_no_insulation_cylinder_thickness_zero_takes_uninsulated_loss() -> None: + """The Elmhurst site-notes mapper lodges a "No Insulation" cylinder as + `cylinder_insulation_type=None` + `cylinder_insulation_thickness_mm=0` + — per its own comment "the lodged §15.1 Insulation Thickness (0 mm) + carries the storage-loss signal the cascade's SAP 10.2 Table 2 + dispatch needs" (`_ELMHURST_CYLINDER_NO_INSULATION_LABELS`, + datatypes/epc/domain/mapper.py). The cascade must honour that signal: + a lodged 0 mm with no insulation type is the uninsulated cylinder — + Table 2 loose-jacket branch at t = 0 — not the zero-loss combi + default it previously fell to (the type-None guard returned None + before the thickness was ever read). + """ + # Arrange — same dwelling as the type-0 test; only the lodgement + # shape differs (type None + explicit 0 mm, the Elmhurst path). + 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()], + sap_heating=make_sap_heating( + main_heating_details=[hp_main], + water_heating_code=901, + cylinder_size=3, # Medium → 160 L + cylinder_insulation_type=None, # Elmhurst "No Insulation" + cylinder_insulation_thickness_mm=0, + cylinder_thermostat="Y", + ), + ) + expected_jan_kwh = ( + 160.0 + * cylinder_storage_loss_factor_table_2( + insulation_type="loose_jacket", thickness_mm=0.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 — identical series to the gov-API type-0 lodgement: the two + # front-ends describe the same uninsulated cylinder. + 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,