feat(modelling): loft overlay 270→300 mm + Elmhurst cascade pin (#1158)

Completes #1158 end-to-end. recommend_loft_insulation now emits a
300 mm overlay (was 270 mm). The Elmhurst before/after re-lodgement of
the loft-insulation measure on cert 001431 lodges the after-cert at
300 mm roof insulation; pinning before→overlay→after requires the
overlay to match that depth — at 270 mm the cascade left a +0.173 SAP
residual, at 300 mm it closes at delta 0.000000 on SAP/CO2/PE.

Adds test_loft_overlay_reproduces_the_relodged_after and updates the
roof generator unit test's thickness assertion to 300.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-03 09:39:21 +00:00
parent 4c0a907a54
commit 44d62c0c9b
5 changed files with 25 additions and 3 deletions

View file

@ -20,8 +20,10 @@ from repositories.product.product_repository import ProductRepository
_LOFT_MEASURE_TYPE = "loft_insulation"
# RdSAP 10 Table 16: 0 mm lodged roof insulation is an uninsulated loft.
_ROOF_UNINSULATED_MM = 0
# Recommended loft-insulation depth (mm) — the building-regs standard top-up.
_RECOMMENDED_LOFT_THICKNESS_MM = 270
# Recommended loft-insulation depth (mm). Elmhurst re-lodges a loft-insulation
# measure at 300 mm; pinning the before→after cascade (000490/001431) requires
# the overlay to match that depth exactly (see test_elmhurst_cascade_pins).
_RECOMMENDED_LOFT_THICKNESS_MM = 300
def recommend_loft_insulation(

Binary file not shown.

Binary file not shown.

View file

@ -20,6 +20,7 @@ from datatypes.epc.domain.epc_property_data import EpcPropertyData
from domain.modelling.package_scorer import PackageScorer, Score
from domain.modelling.product import Product
from domain.modelling.recommendation import Recommendation
from domain.modelling.roof_recommendation import recommend_loft_insulation
from domain.modelling.simulation import EpcSimulation
from domain.modelling.wall_recommendation import recommend_cavity_wall
from domain.sap10_calculator.calculator import Sap10Calculator, SapResult
@ -79,3 +80,22 @@ def test_cavity_wall_overlay_reproduces_the_relodged_after() -> None:
_assert_overlay_reproduces_after(
before, after, recommendation.options[0].overlay
)
def test_loft_overlay_reproduces_the_relodged_after() -> None:
# Arrange
before: EpcPropertyData = parse_recommendation_summary(
"loft_001431_before.pdf"
)
after: EpcPropertyData = parse_recommendation_summary(
"loft_001431_after.pdf"
)
recommendation: Recommendation | None = recommend_loft_insulation(
before, _AnyProduct()
)
assert recommendation is not None
# Act / Assert
_assert_overlay_reproduces_after(
before, after, recommendation.options[0].overlay
)

View file

@ -46,7 +46,7 @@ def test_uninsulated_loft_yields_a_loft_insulation_recommendation() -> None:
option = recommendation.options[0]
assert option.measure_type == "loft_insulation"
simulated: EpcPropertyData = apply_simulations(baseline, [option.overlay])
assert _part(simulated, BuildingPartIdentifier.MAIN).roof_insulation_thickness == 270
assert _part(simulated, BuildingPartIdentifier.MAIN).roof_insulation_thickness == 300
def test_already_insulated_loft_yields_no_recommendation() -> None: