From 0625b9b179ed7a6ea8a630ce26aab3313a318821 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 23 Jul 2026 13:45:58 +0000 Subject: [PATCH] =?UTF-8?q?Lock=20the=20sloping-ceiling=20override's=20U-v?= =?UTF-8?q?alue=20end-to-end=20(col-3=20as-built,=20col-1a=20by=20depth)?= =?UTF-8?q?=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/domain/epc/test_roof_type_overlay.py | 48 ++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/domain/epc/test_roof_type_overlay.py b/tests/domain/epc/test_roof_type_overlay.py index 23d39890a..3c4e859cc 100644 --- a/tests/domain/epc/test_roof_type_overlay.py +++ b/tests/domain/epc/test_roof_type_overlay.py @@ -12,6 +12,28 @@ import pytest from datatypes.epc.domain.epc_property_data import BuildingPartIdentifier from domain.epc.property_overlays.roof_type_overlay import roof_overlay_for from domain.epc.property_overrides.roof_type import RoofType +from domain.sap10_ml.rdsap_uvalues import Country, u_roof + + +def _resolved_roof_u(roof_type_value: str, age_band: str) -> float: + """Score a resolved roof override through the real `u_roof`, deriving the + sloping-ceiling flag exactly as the calculator does from the overlay's + `roof_construction_type` string (`heat_transmission.py`: + `is_pitched_sloping_ceiling = "sloping ceiling" in roof_type_lower`). This + ties the overlay's emitted string to the U it actually produces — a silent + loft-scored roof if the string ever stops containing "sloping ceiling".""" + simulation = roof_overlay_for(roof_type_value, 0) + assert simulation is not None + overlay = simulation.building_parts[BuildingPartIdentifier.MAIN] + roof_type_lower = (overlay.roof_construction_type or "").lower() + is_sloping = "sloping ceiling" in roof_type_lower + return u_roof( + country=Country.ENG, + age_band=age_band, + insulation_thickness_mm=overlay.roof_insulation_thickness, + is_sloping_ceiling=is_sloping, + is_pitched_sloping_ceiling=is_sloping, + ) _FLAT_THICKNESS_MEMBERS: list[tuple[RoofType, int]] = [ (RoofType.FLAT_12MM, 12), @@ -218,6 +240,32 @@ def test_sloping_ceiling_as_built_defers_to_the_age_band_default() -> None: assert overlay.roof_insulation_thickness is None +def test_as_built_sloping_ceiling_scores_the_slope_default_not_the_loft_floor() -> None: + # ADR-0066: an as-built sloping ceiling at an old band takes Table 18 col (3) + # (band F = 0.68 W/m²K), NOT the loft col (1) 0.40 "modern-retrofit" floor it + # was mis-scored at when the classifier routed it onto the loft ladder. This + # is the whole point of the family — the age-band default must be the slope + # column, driven by the overlay's construction string. + + # Act + u_value = _resolved_roof_u("Pitched, sloping ceiling, as built", age_band="F") + + # Assert — col (3), not the loft col (1) 0.40. + assert abs(u_value - 0.68) <= 1e-4 + + +def test_known_depth_sloping_ceiling_scores_the_insulated_slope_ladder() -> None: + # ADR-0066: a known-depth sloping ceiling scores on Table 17 col (1a) + # "insulated slope" — 150 mm → 0.30 W/m²K (Elmhurst-confirmed, byte-identical + # to the loft col (1) value, so it is a *deliberate* match, not a fallback). + + # Act + u_value = _resolved_roof_u("Pitched, sloping ceiling, 150 mm insulation", age_band="F") + + # Assert + assert abs(u_value - 0.30) <= 1e-4 + + @pytest.mark.parametrize( "roof_type_value", [