mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
171 lines
6.5 KiB
Python
171 lines
6.5 KiB
Python
"""Behaviour of the wall Recommendation Generator: detecting a treatable
|
|
wall and emitting a Recommendation whose Measure Option carries the
|
|
Simulation Overlay for the intervention. See CONTEXT.md / ADR-0016.
|
|
"""
|
|
|
|
from datatypes.epc.domain.epc_property_data import (
|
|
BuildingPartIdentifier,
|
|
EpcPropertyData,
|
|
SapBuildingPart,
|
|
)
|
|
from domain.modelling.scoring.overlay_applicator import apply_simulations
|
|
from domain.modelling.product import Product
|
|
from domain.modelling.recommendation import Recommendation
|
|
from domain.modelling.generators.wall_recommendation import recommend_cavity_wall
|
|
from domain.modelling.generators.solid_wall_recommendation import recommend_solid_wall
|
|
from repositories.product.product_repository import ProductRepository
|
|
from tests.domain.sap10_calculator.worksheet._elmhurst_worksheet_000490 import (
|
|
build_epc,
|
|
)
|
|
|
|
|
|
class _StubProducts(ProductRepository):
|
|
"""In-memory ProductRepository returning a fixed cavity Product."""
|
|
|
|
def get(self, measure_type: str) -> Product:
|
|
return Product(
|
|
measure_type=measure_type, unit_cost_per_m2=18.5, contingency_rate=0.10
|
|
)
|
|
|
|
|
|
def _part(epc: EpcPropertyData, identifier: BuildingPartIdentifier) -> SapBuildingPart:
|
|
return next(p for p in epc.sap_building_parts if p.identifier is identifier)
|
|
|
|
|
|
def test_uninsulated_main_cavity_wall_yields_a_cavity_fill_recommendation() -> None:
|
|
# Arrange
|
|
baseline: EpcPropertyData = build_epc() # MAIN: cavity (4), uninsulated (4)
|
|
|
|
# Act
|
|
recommendation: Recommendation | None = recommend_cavity_wall(baseline, _StubProducts())
|
|
|
|
# Assert
|
|
assert recommendation is not None
|
|
assert recommendation.surface == "Main wall"
|
|
assert len(recommendation.options) == 1
|
|
option = recommendation.options[0]
|
|
assert option.measure_type == "cavity_wall_insulation"
|
|
simulated: EpcPropertyData = apply_simulations(baseline, [option.overlay])
|
|
assert _part(simulated, BuildingPartIdentifier.MAIN).wall_insulation_type == 2
|
|
|
|
|
|
def test_already_insulated_main_wall_yields_no_recommendation() -> None:
|
|
# Arrange
|
|
baseline: EpcPropertyData = build_epc()
|
|
_part(baseline, BuildingPartIdentifier.MAIN).wall_insulation_type = 2 # filled
|
|
|
|
# Act
|
|
recommendation: Recommendation | None = recommend_cavity_wall(baseline, _StubProducts())
|
|
|
|
# Assert
|
|
assert recommendation is None
|
|
|
|
|
|
def test_cavity_option_carries_fully_loaded_cost_from_area_and_product() -> None:
|
|
# Arrange
|
|
baseline: EpcPropertyData = build_epc() # MAIN gross heat-loss area 45.93 m^2
|
|
products = _StubProducts() # cavity 18.5 GBP/m^2, contingency 0.10
|
|
|
|
# Act
|
|
recommendation: Recommendation | None = recommend_cavity_wall(baseline, products)
|
|
|
|
# Assert
|
|
assert recommendation is not None
|
|
cost = recommendation.options[0].cost
|
|
assert cost is not None
|
|
assert abs(cost.total - 45.93 * 18.5) <= 0.01
|
|
assert abs(cost.contingency_rate - 0.10) <= 1e-9
|
|
|
|
|
|
def test_non_cavity_main_wall_yields_no_recommendation() -> None:
|
|
# Arrange
|
|
baseline: EpcPropertyData = build_epc()
|
|
main: SapBuildingPart = _part(baseline, BuildingPartIdentifier.MAIN)
|
|
main.wall_construction = 2 # solid (not cavity); still uninsulated
|
|
|
|
# Act
|
|
recommendation: Recommendation | None = recommend_cavity_wall(baseline, _StubProducts())
|
|
|
|
# Assert
|
|
assert recommendation is None
|
|
|
|
|
|
def test_at_regs_age_band_cavity_yields_no_cavity_fill_recommendation() -> None:
|
|
# Arrange — a cavity wall lodged as-built at construction age band L
|
|
# (2012-2022): the as-built cascade already assigns the at-regs U-value, so
|
|
# filling the cavity cannot lower it. The Wall U-Value Gate withholds the
|
|
# Option (ADR-0051); the legacy insulation-state trigger alone would offer
|
|
# a paid no-op.
|
|
baseline: EpcPropertyData = build_epc() # MAIN: cavity (4), uninsulated (4)
|
|
_part(baseline, BuildingPartIdentifier.MAIN).construction_age_band = "L"
|
|
|
|
# Act
|
|
recommendation: Recommendation | None = recommend_cavity_wall(
|
|
baseline, _StubProducts()
|
|
)
|
|
|
|
# Assert
|
|
assert recommendation is None
|
|
|
|
|
|
def test_at_regs_age_band_wall_yields_no_solid_wall_recommendation() -> None:
|
|
# Arrange — a timber-frame wall lodged as-built at construction age band L
|
|
# (2012-2022). RdSAP's cascade assigns it the at-regs U-value (0.28), and
|
|
# 100 mm IWI resolves to the SAME value — the measure cannot change the
|
|
# modelled wall, so offering it sells £4k of insulation for +0.00 SAP
|
|
# (property 711795, PRD #1435 WS2). The Wall U-Value Gate withholds it
|
|
# (ADR-0051).
|
|
baseline: EpcPropertyData = build_epc()
|
|
main: SapBuildingPart = _part(baseline, BuildingPartIdentifier.MAIN)
|
|
main.wall_construction = 5 # timber frame — IWI-only construction
|
|
main.wall_insulation_type = 4 # as-built / uninsulated — the legacy trigger
|
|
main.construction_age_band = "L"
|
|
|
|
# Act
|
|
recommendation: Recommendation | None = recommend_solid_wall(
|
|
baseline, _StubProducts()
|
|
)
|
|
|
|
# Assert
|
|
assert recommendation is None
|
|
|
|
|
|
def test_treatable_old_band_solid_wall_keeps_its_insulation_options() -> None:
|
|
# Arrange — solid brick as-built at age band B: the cascade U (~2.1) far
|
|
# exceeds what 100 mm EWI/IWI achieves, so the Wall U-Value Gate must let
|
|
# both Options through unchanged (the gate is physical possibility only —
|
|
# ADR-0051).
|
|
baseline: EpcPropertyData = build_epc() # age band B
|
|
main: SapBuildingPart = _part(baseline, BuildingPartIdentifier.MAIN)
|
|
main.wall_construction = 3 # solid brick — EWI + IWI constructable
|
|
main.wall_insulation_type = 4
|
|
|
|
# Act
|
|
recommendation: Recommendation | None = recommend_solid_wall(
|
|
baseline, _StubProducts()
|
|
)
|
|
|
|
# Assert
|
|
assert recommendation is not None
|
|
assert {option.measure_type for option in recommendation.options} == {
|
|
"external_wall_insulation",
|
|
"internal_wall_insulation",
|
|
}
|
|
|
|
|
|
def test_park_home_wall_yields_no_solid_wall_recommendation() -> None:
|
|
# Arrange — a park home (wall_construction code 8) with an uninsulated
|
|
# as-built wall. Code 8 is NOT system-built (ADR-0019); a park home's
|
|
# proprietary panel is never EWI/IWI-suitable, so the generator excludes it.
|
|
baseline: EpcPropertyData = build_epc()
|
|
main: SapBuildingPart = _part(baseline, BuildingPartIdentifier.MAIN)
|
|
main.wall_construction = 8
|
|
main.wall_insulation_type = 4 # as-built / uninsulated — the trigger
|
|
|
|
# Act
|
|
recommendation: Recommendation | None = recommend_solid_wall(
|
|
baseline, _StubProducts()
|
|
)
|
|
|
|
# Assert
|
|
assert recommendation is None
|