mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
feat(modelling): price the cavity Option from area x Product — closes #1155
recommend_cavity_wall now takes a ProductRepository and prices the Measure Option: Cost(total = gross_heat_loss_wall_area(MAIN) x product.unit_cost_per_m2, contingency_rate = product.contingency_rate). Detection is unchanged and runs before pricing, so ineligible walls still return None without a catalogue hit. Completes #1155 — the cavity-wall Recommendation Generator now detects an uninsulated main cavity wall and emits a priced Option carrying the filled- cavity overlay. Four behaviour tests (detection x3 + fully-loaded cost). pyright strict clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b2c8980dd2
commit
bb2c0068ff
2 changed files with 49 additions and 7 deletions
|
|
@ -11,8 +11,12 @@ from datatypes.epc.domain.epc_property_data import (
|
|||
BuildingPartIdentifier,
|
||||
EpcPropertyData,
|
||||
)
|
||||
from domain.modelling.recommendation import MeasureOption, Recommendation
|
||||
from domain.building_geometry import gross_heat_loss_wall_area
|
||||
from domain.modelling.recommendation import Cost, MeasureOption, Recommendation
|
||||
from domain.modelling.simulation import BuildingPartOverlay, EpcSimulation
|
||||
from repositories.product.product_repository import ProductRepository
|
||||
|
||||
_CAVITY_MEASURE_TYPE = "cavity_wall_insulation"
|
||||
|
||||
# RdSAP 10 Table 5 wall_construction: 4 = "Cavity". Table 6
|
||||
# wall_insulation_type: 4 = "as-built / assumed" (uninsulated), 2 = "Filled
|
||||
|
|
@ -23,9 +27,12 @@ _WALL_UNINSULATED = 4
|
|||
_FILLED_CAVITY = 2
|
||||
|
||||
|
||||
def recommend_cavity_wall(epc: EpcPropertyData) -> Optional[Recommendation]:
|
||||
def recommend_cavity_wall(
|
||||
epc: EpcPropertyData, products: ProductRepository
|
||||
) -> Optional[Recommendation]:
|
||||
"""Return a cavity-fill Recommendation for the main wall when it is an
|
||||
uninsulated cavity wall, else None."""
|
||||
uninsulated cavity wall, else None. The Option's cost is the heat-loss wall
|
||||
area priced at the Product's fully-loaded unit cost, with its contingency."""
|
||||
main = next(
|
||||
part
|
||||
for part in epc.sap_building_parts
|
||||
|
|
@ -38,8 +45,15 @@ def recommend_cavity_wall(epc: EpcPropertyData) -> Optional[Recommendation]:
|
|||
):
|
||||
return None
|
||||
|
||||
product = products.get(_CAVITY_MEASURE_TYPE)
|
||||
wall_area: float = gross_heat_loss_wall_area(epc, BuildingPartIdentifier.MAIN)
|
||||
cost = Cost(
|
||||
total=wall_area * product.unit_cost_per_m2,
|
||||
contingency_rate=product.contingency_rate,
|
||||
)
|
||||
|
||||
option = MeasureOption(
|
||||
measure_type="cavity_wall_insulation",
|
||||
measure_type=_CAVITY_MEASURE_TYPE,
|
||||
description="Cavity wall insulation (fill the existing cavity)",
|
||||
overlay=EpcSimulation(
|
||||
building_parts={
|
||||
|
|
@ -48,5 +62,6 @@ def recommend_cavity_wall(epc: EpcPropertyData) -> Optional[Recommendation]:
|
|||
)
|
||||
}
|
||||
),
|
||||
cost=cost,
|
||||
)
|
||||
return Recommendation(surface="Main wall", options=(option,))
|
||||
|
|
|
|||
|
|
@ -9,13 +9,24 @@ from datatypes.epc.domain.epc_property_data import (
|
|||
SapBuildingPart,
|
||||
)
|
||||
from domain.modelling.overlay_applicator import apply_simulations
|
||||
from domain.modelling.product import Product
|
||||
from domain.modelling.recommendation import Recommendation
|
||||
from domain.modelling.wall_recommendation import recommend_cavity_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)
|
||||
|
||||
|
|
@ -25,7 +36,7 @@ def test_uninsulated_main_cavity_wall_yields_a_cavity_fill_recommendation() -> N
|
|||
baseline: EpcPropertyData = build_epc() # MAIN: cavity (4), uninsulated (4)
|
||||
|
||||
# Act
|
||||
recommendation: Recommendation | None = recommend_cavity_wall(baseline)
|
||||
recommendation: Recommendation | None = recommend_cavity_wall(baseline, _StubProducts())
|
||||
|
||||
# Assert
|
||||
assert recommendation is not None
|
||||
|
|
@ -43,12 +54,28 @@ def test_already_insulated_main_wall_yields_no_recommendation() -> None:
|
|||
_part(baseline, BuildingPartIdentifier.MAIN).wall_insulation_type = 2 # filled
|
||||
|
||||
# Act
|
||||
recommendation: Recommendation | None = recommend_cavity_wall(baseline)
|
||||
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()
|
||||
|
|
@ -56,7 +83,7 @@ def test_non_cavity_main_wall_yields_no_recommendation() -> None:
|
|||
main.wall_construction = 2 # solid (not cavity); still uninsulated
|
||||
|
||||
# Act
|
||||
recommendation: Recommendation | None = recommend_cavity_wall(baseline)
|
||||
recommendation: Recommendation | None = recommend_cavity_wall(baseline, _StubProducts())
|
||||
|
||||
# Assert
|
||||
assert recommendation is None
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue