mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
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>
68 lines
2.9 KiB
Python
68 lines
2.9 KiB
Python
"""The ventilation Recommendation Generator.
|
|
|
|
Detects a dwelling that lacks adequate mechanical ventilation and emits a
|
|
Recommendation whose single Measure Option installs decentralised mechanical
|
|
extract ventilation (MEV), priced per installed unit. Like the wall/roof/floor
|
|
generators it does detection + pricing and carries no scores (ADR-0016).
|
|
|
|
Unlike them it is **not** run by the candidate-pool runner: ventilation is a
|
|
forced Measure Dependency of fabric insulation (it only ever costs SAP, so the
|
|
Optimiser would never choose it), so this Recommendation is consumed by
|
|
``optimisation.measure_dependency`` and injected into the package, never freely
|
|
selected. The legacy intervention was "mechanical, extract only"; the guard
|
|
mirrors legacy ``Property.has_ventilation``.
|
|
"""
|
|
|
|
from typing import Optional
|
|
|
|
from datatypes.epc.domain.epc_property_data import EpcPropertyData
|
|
from domain.modelling.measure_type import MeasureType
|
|
from domain.modelling.recommendation import Cost, MeasureOption, Recommendation
|
|
from domain.modelling.simulation import EpcSimulation, VentilationOverlay
|
|
from repositories.product.product_repository import ProductRepository
|
|
|
|
_VENTILATION_MEASURE_TYPE = MeasureType.MECHANICAL_VENTILATION
|
|
|
|
# The SAP10.2 §2 mechanical-ventilation kind installed: decentralised MEV
|
|
# ("mechanical extract, decentralised (MEV dc)" → MechanicalVentilationKind
|
|
# name), the legacy "mechanical, extract only" intervention.
|
|
_MEV_KIND = "EXTRACT_OR_PIV_OUTSIDE"
|
|
|
|
# Best practice installs one MEV unit per wet zone; the legacy recommendation
|
|
# fits two units per dwelling.
|
|
_VENTILATION_UNIT_COUNT = 2
|
|
|
|
|
|
def recommend_ventilation(
|
|
epc: EpcPropertyData, products: ProductRepository
|
|
) -> Optional[Recommendation]:
|
|
"""Return a mechanical-ventilation Recommendation for a dwelling that is not
|
|
already mechanically ventilated, else None. The single Option installs MEV
|
|
and is priced at two fully-loaded units."""
|
|
if _already_mechanically_ventilated(epc):
|
|
return None
|
|
|
|
product = products.get(_VENTILATION_MEASURE_TYPE)
|
|
cost = Cost(
|
|
total=product.unit_cost_per_m2 * _VENTILATION_UNIT_COUNT,
|
|
contingency_rate=product.contingency_rate,
|
|
)
|
|
option = MeasureOption(
|
|
measure_type=_VENTILATION_MEASURE_TYPE,
|
|
description=f"Install {_VENTILATION_UNIT_COUNT} mechanical extract ventilation units",
|
|
overlay=EpcSimulation(
|
|
ventilation=VentilationOverlay(mechanical_ventilation_kind=_MEV_KIND)
|
|
),
|
|
cost=cost,
|
|
material_id=product.id,
|
|
)
|
|
return Recommendation(surface="Ventilation", options=(option,))
|
|
|
|
|
|
def _already_mechanically_ventilated(epc: EpcPropertyData) -> bool:
|
|
"""True when the dwelling already lodges a mechanical ventilation kind
|
|
(MEV/MVHR) — the legacy `has_ventilation` guard."""
|
|
return (
|
|
epc.sap_ventilation is not None
|
|
and epc.sap_ventilation.mechanical_ventilation_kind is not None
|
|
)
|