Vaulted ND roof on a pre-1950 dwelling gets sloping-ceiling insulation 🟩

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-07-02 11:01:43 +00:00
parent 0d465346c9
commit f3bdf0e884

View file

@ -10,7 +10,7 @@ Flat-roof and room-in-roof branches land in later slices. No scoring, no
persistence impact is produced later by scoring (ADR-0016).
"""
from typing import Optional
from typing import Final, Optional, Union
from datatypes.epc.domain.epc_property_data import (
BuildingPartIdentifier,
@ -33,6 +33,36 @@ _ROOF_UNINSULATED_MM = 0
_RECOMMENDED_LOFT_THICKNESS_MM = 300
# Recommended sloping-ceiling depth (mm); Elmhurst re-lodges 100 mm (ADR-0021).
_RECOMMENDED_SLOPING_CEILING_THICKNESS_MM = 100
# Age bands whose as-built pitched roof is uninsulated (ADR-0047: A-D
# uninsulated, E-F limited insulation, G+ insulated) — the resolution for a
# cert lodging the "ND" (Not Defined) sentinel or no thickness at all.
_AS_BUILT_UNINSULATED_AGE_BANDS: Final[frozenset[str]] = frozenset(
{"A", "B", "C", "D"}
)
def _pitched_roof_is_uninsulated(
thickness: Union[str, int, None], age_band: Optional[str]
) -> bool:
"""The pitched-roof uninsulated trigger, sentinel-aware (ADR-0047).
`roof_insulation_thickness` lodges numerics AND string sentinels; the
sentinels are data, not noise, and must never make an undefined cert MORE
eligible than an explicitly-lodged one. An explicit 0 is uninsulated; the
"ND" (Not Defined) sentinel or nothing lodged resolves to the age
band's as-built state (only A-D built without pitched-roof insulation).
Anything else ("NI" = insulation present at unknown thickness per RdSAP 10
§5.11.4, or a measured depth) means insulation is there. Eligibility only
the calculator resolves the same sentinels separately for the U-cascade
(deliberate two-home split, ADR-0047)."""
if isinstance(thickness, int):
return thickness == 0
if thickness is None or thickness == "ND":
return (
age_band is not None
and age_band.upper() in _AS_BUILT_UNINSULATED_AGE_BANDS
)
return False
_FLAT_ROOF_MEASURE_TYPE = MeasureType.FLAT_ROOF_INSULATION
# Recommended flat-roof depth (mm); Elmhurst re-lodges 200 mm (ADR-0021).
_RECOMMENDED_FLAT_ROOF_THICKNESS_MM = 200
@ -68,8 +98,13 @@ def recommend_roof_insulation(
# plain pitched loft, a thatched roof (the covering doesn't block insulating
# the loft floor), and an unlodged roof type (the modal UK case), matching
# the pre-dispatcher behaviour of firing on `roof_insulation_thickness == 0`.
if "sloping ceiling" in roof_type:
if main.roof_insulation_thickness != _ROOF_UNINSULATED_MM:
# A vaulted ceiling follows the slope like a sloping ceiling (no loft
# void) — same disjunction the calculator's `is_sloping_ceiling` uses
# (heat_transmission.py), so generator and scorer read one story (ADR-0047).
if "sloping ceiling" in roof_type or "vaulted" in roof_type:
if not _pitched_roof_is_uninsulated(
main.roof_insulation_thickness, main.construction_age_band
):
return None
return _roof_recommendation(
epc,