feat(modelling): overlay models solid-wall insulation (IWI/EWI), pinned

Slice 1 of solid-wall insulation. BuildingPartOverlay gains a
wall_insulation_thickness field; the generic applicator already folds it onto
SapBuildingPart by name. With wall_insulation_type=1 (EWI) / 3 (IWI) + 100 mm,
the calculator derives the post-insulation U-value (§5.8 documentary path,
λ=0.04 default) — and for IWI also lowers the thermal-mass parameter. Two new
Elmhurst before/after cascade pins (solid-brick EWI + IWI, cert 001431)
reproduce the re-lodged after at abs(diff) <= 1e-4 across SAP/CO2/PE.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-04 15:11:26 +00:00
parent afabfa0147
commit 68aa80c174
6 changed files with 60 additions and 2 deletions

View file

@ -22,6 +22,10 @@ class BuildingPartOverlay:
"""
wall_insulation_type: Optional[int] = None
# Added solid-wall insulation depth (mm) — drives the calculator's Table 6
# bucket / §5.8 documentary U-value for EWI (`wall_insulation_type=1`) and
# IWI (`wall_insulation_type=3`); λ defaults to 0.04 W/m·K in the calculator.
wall_insulation_thickness: Optional[int] = None
roof_insulation_thickness: Optional[int] = None
floor_insulation_thickness: Optional[int] = None
floor_insulation_type_str: Optional[str] = None

View file

@ -16,13 +16,16 @@ from __future__ import annotations
from typing import Final
from datatypes.epc.domain.epc_property_data import EpcPropertyData
from datatypes.epc.domain.epc_property_data import (
BuildingPartIdentifier,
EpcPropertyData,
)
from domain.modelling.scoring.package_scorer import PackageScorer, Score
from domain.modelling.product import Product
from domain.modelling.recommendation import Recommendation
from domain.modelling.generators.floor_recommendation import recommend_floor_insulation
from domain.modelling.generators.roof_recommendation import recommend_loft_insulation
from domain.modelling.simulation import EpcSimulation
from domain.modelling.simulation import BuildingPartOverlay, EpcSimulation
from domain.modelling.generators.wall_recommendation import recommend_cavity_wall
from domain.sap10_calculator.calculator import Sap10Calculator, SapResult
from repositories.product.product_repository import ProductRepository
@ -35,6 +38,14 @@ from tests.domain.modelling._elmhurst_recommendation import (
# e2e tolerance.
_PIN_ABS: Final[float] = 1e-4
# RdSAP wall_insulation_type codes for solid-wall insulation (Elmhurst
# Summary "E External" / "I Internal"); cf. domain/sap10_ml/rdsap_uvalues.py.
_WALL_INSULATION_EXTERNAL: Final[int] = 1
_WALL_INSULATION_INTERNAL: Final[int] = 3
# Recommended solid-wall insulation depth (mm); the calculator's λ default
# (0.04 W/m·K) matches Elmhurst's lodged thermal conductivity.
_SOLID_WALL_INSULATION_MM: Final[int] = 100
class _AnyProduct(ProductRepository):
"""In-memory ProductRepository returning a fixed Product for any Measure
@ -83,6 +94,49 @@ def test_cavity_wall_overlay_reproduces_the_relodged_after() -> None:
)
def test_solid_brick_ewi_overlay_reproduces_the_relodged_after() -> None:
# Arrange — 100 mm external wall insulation on a solid-brick main wall.
before: EpcPropertyData = parse_recommendation_summary(
"solid_brick_ewi_001431_before.pdf"
)
after: EpcPropertyData = parse_recommendation_summary(
"solid_brick_ewi_001431_after.pdf"
)
overlay = EpcSimulation(
building_parts={
BuildingPartIdentifier.MAIN: BuildingPartOverlay(
wall_insulation_type=_WALL_INSULATION_EXTERNAL,
wall_insulation_thickness=_SOLID_WALL_INSULATION_MM,
)
}
)
# Act / Assert
_assert_overlay_reproduces_after(before, after, overlay)
def test_solid_brick_iwi_overlay_reproduces_the_relodged_after() -> None:
# Arrange — 100 mm internal wall insulation on a solid-brick main wall
# (also lowers the thermal-mass parameter, unlike EWI).
before: EpcPropertyData = parse_recommendation_summary(
"solid_brick_iwi_001431_before.pdf"
)
after: EpcPropertyData = parse_recommendation_summary(
"solid_brick_iwi_001431_after.pdf"
)
overlay = EpcSimulation(
building_parts={
BuildingPartIdentifier.MAIN: BuildingPartOverlay(
wall_insulation_type=_WALL_INSULATION_INTERNAL,
wall_insulation_thickness=_SOLID_WALL_INSULATION_MM,
)
}
)
# Act / Assert
_assert_overlay_reproduces_after(before, after, overlay)
def test_loft_overlay_reproduces_the_relodged_after() -> None:
# Arrange
before: EpcPropertyData = parse_recommendation_summary(