Carry the sloping-ceiling insulation depth into the overlay 🟪

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

View file

@ -1,6 +1,6 @@
"""Map a Landlord-Override `RoofType` value to a roof Simulation Overlay (ADR-0032/0033).
Two resolvable families:
Three resolvable families:
* `"Pitched, N mm loft insulation"` the loft depth N maps to
`BuildingPartOverlay.roof_insulation_thickness` (mm); the calculator scores the
@ -9,9 +9,13 @@ Two resolvable families:
age-band default (`_FLAT_ROOF_BY_AGE`, ADR-0033). The calculator's flat path
keys on the `roof_construction_type` *string* (`"flat" in `), so the overlay
sets that to `"Flat"` and leaves thickness `None` for the (separately overlaid)
construction age band to drive the U-value. No flat `RoofType` value carries an
explicit mm depth, confirmed by the Elmhurst sweep (As Built / Unknown
age-band default).
construction age band to drive the U-value. A flat `RoofType` value may also
carry an explicit mm depth (Table 16 col (1), ADR-0041).
* `"Pitched, sloping ceiling, …"` a slope-following ceiling (no loft void,
ADR-0066). The overlay sets `roof_construction_type` to `"Pitched, sloping
ceiling"` so the calculator's `is_pitched_sloping_ceiling` fires: "as built"
leaves thickness `None` for the Table 18 col (3) age-band default, while the
`N mm` ladder carries the depth (Table 17 col (1a) "insulated slope").
Everything else (room-in-roof, "Unknown loft insulation", party-ceiling adjacency
markers like "Another Premises Above") has no clean depth or shape correction, so
@ -27,11 +31,10 @@ from datatypes.epc.domain.epc_property_data import BuildingPartIdentifier
from domain.modelling.simulation import BuildingPartOverlay, EpcSimulation
_LOFT_MM = re.compile(r"(\d+)\+?\s*mm loft insulation")
# A flat roof carries no loft, so its depth reads "N mm insulation" (no "loft").
_FLAT_MM = re.compile(r"(\d+)\+?\s*mm insulation")
# A sloping ceiling's depth reads the same "N mm insulation" shape (no "loft");
# the "sloping ceiling" prefix, not the suffix, distinguishes it from a flat roof.
_SLOPING_MM = re.compile(r"(\d+)\+?\s*mm insulation")
# A flat roof or sloping ceiling carries no loft, so its depth reads
# "N mm insulation" (no "loft"); the leading shape ("Flat," / "Pitched, sloping
# ceiling"), not the suffix, distinguishes the two.
_MM_INSULATION = re.compile(r"(\d+)\+?\s*mm insulation")
def roof_overlay_for(
@ -60,7 +63,7 @@ def _overlay_for(roof_type_value: str) -> Optional[BuildingPartOverlay]:
# A known depth ("N mm insulation") rides into the overlay and scores on
# Table 17 col (1a) "insulated slope"; "as built" leaves thickness None so
# the Table 18 col (3) age-band default drives it. Never the loft ladder.
sloping_depth = _SLOPING_MM.search(roof_type_value)
sloping_depth = _MM_INSULATION.search(roof_type_value)
return BuildingPartOverlay(
roof_construction_type="Pitched, sloping ceiling",
roof_insulation_thickness=(
@ -68,7 +71,7 @@ def _overlay_for(roof_type_value: str) -> Optional[BuildingPartOverlay]:
),
)
if roof_type_value.startswith("Flat,"):
flat_depth = _FLAT_MM.search(roof_type_value)
flat_depth = _MM_INSULATION.search(roof_type_value)
if flat_depth is not None:
# A flat roof with a known depth: RdSAP 10 Table 16 col (1) ("joists at
# ceiling level and flat roof") scores it by thickness, so carry the