Model/domain/modelling/generators/wall_recommendation.py
Khalim Conn-Kowlessar d58ac60d29 feat(modelling): MeasureType StrEnum as the canonical measure vocabulary
Introduce domain/modelling/measure_type.py — a StrEnum with one member per
modelled measure (the 15 the generators emit). A StrEnum so each member *is*
its string value: it persists straight into the `recommendation` varchar
column, is the optimiser's group-by key, and compares equal to the catalogue /
EPC strings — so it replaces the per-generator string constants with no
persistence or optimiser change.

Repoint every generator's measure-type constant/literal to a MeasureType
member (wall, solid_wall, roof, floor, glazing, lighting, ventilation,
heating, solar). Field annotations stay `str` for now; tightening them to
MeasureType is the next slice.

This is the enum the historical engine deferred (engine.py:970
"TODO - formalise property measure types into an enum") and the vocabulary the
forthcoming `considered_measures` allowlist will speak (mirroring the legacy
`inclusions`).

Suite green: tests/domain/modelling + orchestration + harness 253 pass + 3
xfail; pyright clean on the enum + generators.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 19:54:04 +00:00

69 lines
2.5 KiB
Python

"""The wall Recommendation Generator.
Detects a treatable main wall on an EpcPropertyData and emits a Recommendation
whose Measure Option carries the Simulation Overlay for the intervention. No
scoring, no persistence — impact is produced later by scoring (ADR-0016).
"""
from typing import Optional
from datatypes.epc.domain.epc_property_data import (
BuildingPartIdentifier,
EpcPropertyData,
)
from domain.building_geometry import gross_heat_loss_wall_area
from domain.modelling.measure_type import MeasureType
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 = MeasureType.CAVITY_WALL_INSULATION
# RdSAP 10 Table 5 wall_construction: 4 = "Cavity". Table 6
# wall_insulation_type: 4 = "as-built / assumed" (uninsulated), 2 = "Filled
# cavity" (the calculator's dedicated filled-cavity U row — see
# domain/sap10_ml/rdsap_uvalues.py u_wall).
_CAVITY_WALL_CONSTRUCTION = 4
_WALL_UNINSULATED = 4
_FILLED_CAVITY = 2
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. 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
if part.identifier is BuildingPartIdentifier.MAIN
)
if (
main.wall_construction != _CAVITY_WALL_CONSTRUCTION
or main.wall_insulation_type != _WALL_UNINSULATED
):
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_MEASURE_TYPE,
description="Cavity wall insulation (fill the existing cavity)",
overlay=EpcSimulation(
building_parts={
BuildingPartIdentifier.MAIN: BuildingPartOverlay(
wall_insulation_type=_FILLED_CAVITY
)
}
),
cost=cost,
material_id=product.id,
)
return Recommendation(surface="Main wall", options=(option,))