Lock the sloping-ceiling override's U-value end-to-end (col-3 as-built, col-1a by depth) 🟩

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-07-23 13:45:58 +00:00
parent c01f48d4fd
commit 0625b9b179

View file

@ -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",
[