mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
feat(modelling): recommend_roof_insulation insulates a sloping ceiling
Slice 1 of the roof-insulation generator (ADR-0021). New `recommend_roof_insulation` dispatcher keys on the `roof_construction_type` string: a "sloping ceiling" roof that is uninsulated (roof_insulation_thickness 0/None) gets a single `sloping_ceiling_insulation` Option whose overlay raises roof_insulation_thickness to 100 mm. Pinned against the Elmhurst before→after cert 001431 at 1e-4. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
0c7ce634db
commit
6484610b6c
4 changed files with 98 additions and 1 deletions
|
|
@ -18,12 +18,19 @@ from domain.modelling.simulation import BuildingPartOverlay, EpcSimulation
|
|||
from repositories.product.product_repository import ProductRepository
|
||||
|
||||
_LOFT_MEASURE_TYPE = "loft_insulation"
|
||||
_SLOPING_CEILING_MEASURE_TYPE = "sloping_ceiling_insulation"
|
||||
# RdSAP 10 Table 16: 0 mm lodged roof insulation is an uninsulated loft.
|
||||
_ROOF_UNINSULATED_MM = 0
|
||||
# A roof is uninsulated when its lodged insulation thickness is 0 or absent —
|
||||
# the Elmhurst mapper resolves "As Built" to 0 for pitched/sloping roofs and to
|
||||
# None for flat roofs (ADR-0021).
|
||||
_ROOF_UNINSULATED_THICKNESSES: tuple[Optional[int], ...] = (None, 0)
|
||||
# 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
|
||||
# Recommended sloping-ceiling depth (mm); Elmhurst re-lodges 100 mm (ADR-0021).
|
||||
_RECOMMENDED_SLOPING_CEILING_THICKNESS_MM = 100
|
||||
|
||||
|
||||
def recommend_loft_insulation(
|
||||
|
|
@ -62,3 +69,63 @@ def recommend_loft_insulation(
|
|||
material_id=product.id,
|
||||
)
|
||||
return Recommendation(surface="Roof", options=(option,))
|
||||
|
||||
|
||||
def recommend_roof_insulation(
|
||||
epc: EpcPropertyData, products: ProductRepository
|
||||
) -> Optional[Recommendation]:
|
||||
"""Return the single roof-insulation Recommendation for the MAIN roof,
|
||||
dispatched by roof type (ADR-0021): a sloping ceiling is insulated at the
|
||||
rafters to 100 mm. Returns None when the roof type has no applicable measure
|
||||
or the roof is already insulated."""
|
||||
main = next(
|
||||
part
|
||||
for part in epc.sap_building_parts
|
||||
if part.identifier is BuildingPartIdentifier.MAIN
|
||||
)
|
||||
roof_type: str = (main.roof_construction_type or "").lower()
|
||||
|
||||
if "sloping ceiling" in roof_type:
|
||||
if main.roof_insulation_thickness not in _ROOF_UNINSULATED_THICKNESSES:
|
||||
return None
|
||||
return _roof_recommendation(
|
||||
epc,
|
||||
products,
|
||||
measure_type=_SLOPING_CEILING_MEASURE_TYPE,
|
||||
description="Sloping-ceiling insulation (insulate at the rafters)",
|
||||
thickness_mm=_RECOMMENDED_SLOPING_CEILING_THICKNESS_MM,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def _roof_recommendation(
|
||||
epc: EpcPropertyData,
|
||||
products: ProductRepository,
|
||||
*,
|
||||
measure_type: str,
|
||||
description: str,
|
||||
thickness_mm: int,
|
||||
) -> Recommendation:
|
||||
"""Build a single-Option "Roof" Recommendation: the measure's insulation
|
||||
overlay (raising `roof_insulation_thickness` to the recommended depth)
|
||||
priced at the roof area."""
|
||||
product = products.get(measure_type)
|
||||
area: float = roof_area(epc, BuildingPartIdentifier.MAIN)
|
||||
cost = Cost(
|
||||
total=area * product.unit_cost_per_m2,
|
||||
contingency_rate=product.contingency_rate,
|
||||
)
|
||||
option = MeasureOption(
|
||||
measure_type=measure_type,
|
||||
description=description,
|
||||
overlay=EpcSimulation(
|
||||
building_parts={
|
||||
BuildingPartIdentifier.MAIN: BuildingPartOverlay(
|
||||
roof_insulation_thickness=thickness_mm
|
||||
)
|
||||
}
|
||||
),
|
||||
cost=cost,
|
||||
material_id=product.id,
|
||||
)
|
||||
return Recommendation(surface="Roof", options=(option,))
|
||||
|
|
|
|||
BIN
tests/domain/modelling/fixtures/sloping_ceiling_001431_after.pdf
Normal file
BIN
tests/domain/modelling/fixtures/sloping_ceiling_001431_after.pdf
Normal file
Binary file not shown.
Binary file not shown.
|
|
@ -27,7 +27,10 @@ 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.generators.roof_recommendation import (
|
||||
recommend_loft_insulation,
|
||||
recommend_roof_insulation,
|
||||
)
|
||||
from domain.modelling.simulation import BuildingPartOverlay, EpcSimulation
|
||||
from domain.modelling.generators.wall_recommendation import recommend_cavity_wall
|
||||
from domain.geospatial.planning_restrictions import PlanningRestrictions
|
||||
|
|
@ -336,6 +339,33 @@ def test_loft_overlay_reproduces_the_relodged_after() -> None:
|
|||
)
|
||||
|
||||
|
||||
def test_roof_generator_insulates_a_sloping_ceiling_pinning_its_after() -> None:
|
||||
# Arrange — a pitched roof with an uninsulated sloping ceiling; the re-lodged
|
||||
# after raises its insulation from As Built to 100 mm (ADR-0021).
|
||||
before: EpcPropertyData = parse_recommendation_summary(
|
||||
"sloping_ceiling_001431_before.pdf"
|
||||
)
|
||||
after: EpcPropertyData = parse_recommendation_summary(
|
||||
"sloping_ceiling_001431_after.pdf"
|
||||
)
|
||||
|
||||
# Act — the dispatcher detects "sloping ceiling" and offers the sloping
|
||||
# measure (not loft).
|
||||
recommendation: Recommendation | None = recommend_roof_insulation(
|
||||
before, _AnyProduct()
|
||||
)
|
||||
assert recommendation is not None
|
||||
options: dict[str, MeasureOption] = {
|
||||
option.measure_type: option for option in recommendation.options
|
||||
}
|
||||
|
||||
# Assert — one sloping-ceiling Option whose overlay reproduces the after.
|
||||
assert set(options) == {"sloping_ceiling_insulation"}
|
||||
_assert_overlay_reproduces_after(
|
||||
before, after, options["sloping_ceiling_insulation"].overlay
|
||||
)
|
||||
|
||||
|
||||
def test_solid_floor_overlay_reproduces_the_relodged_after() -> None:
|
||||
# Arrange
|
||||
before: EpcPropertyData = parse_recommendation_summary(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue