Model/domain/modelling/optimisation/measure_dependency.py
Khalim Conn-Kowlessar 84ec6da032 refactor(modelling): group domain/modelling into generators/scoring/optimisation
domain/modelling/ had grown to 15 flat modules. Group the behavioural ones into
subpackages — generators/ (wall/roof/floor Recommendation Generators), scoring/
(overlay applicator, package scorer, role-1/3 scoring), optimisation/ (optimiser
+ measure dependency) — and leave the shared value-object vocabulary
(recommendation, plan, scenario, product, contingencies, simulation) flat at the
top, since it is imported everywhere. Pure move + import-path rewrite across 89
import sites; no behaviour change. 136 pass, pyright strict clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 13:48:36 +00:00

82 lines
3.4 KiB
Python

"""The ventilation Measure Dependency — a data-declared "fabric insulation
requires adequate ventilation" edge (CONTEXT.md: Measure Dependency; ADR-0016).
Wall insulation tightens the envelope, so SAP10.2 (and good practice) require
adequate ventilation alongside it. The optimiser must never *choose* ventilation
(it only ever costs SAP), so it is excluded from the candidate pool and instead
injected into the Optimised Package before the whole-package re-score, where its
real — negative — SAP contribution lands in the truthful figure and the repair
decision. The trigger set is held as data (mirroring the legacy
`assumptions.measures_needing_ventilation`), so extending it (e.g. to roof
insulation) is a data edit, not control flow.
The intervention is decentralised mechanical extract ventilation (MEV), the
legacy "mechanical, extract only" recommendation; it is only forced when the
dwelling is not already mechanically ventilated (legacy `has_ventilation`).
"""
from typing import Optional
from datatypes.epc.domain.epc_property_data import EpcPropertyData
from domain.modelling.optimisation.optimiser import MeasureDependency, ScoredOption
from domain.modelling.recommendation import Cost, MeasureOption
from domain.modelling.simulation import EpcSimulation, VentilationOverlay
from repositories.product.product_repository import ProductRepository
# The measure types that force a ventilation dependency (cf. legacy
# `assumptions.measures_needing_ventilation`).
MEASURES_NEEDING_VENTILATION: frozenset[str] = frozenset(
{
"cavity_wall_insulation",
"internal_wall_insulation",
"external_wall_insulation",
}
)
_VENTILATION_MEASURE_TYPE = "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 ventilation_dependency(
epc: EpcPropertyData, products: ProductRepository
) -> Optional[MeasureDependency]:
"""The ventilation Measure Dependency for a dwelling, or None when it is
already mechanically ventilated (so MEV must not be forced on). The required
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,
)
return MeasureDependency(
triggers=MEASURES_NEEDING_VENTILATION,
required=ScoredOption(option=option, sap_gain=0.0),
)
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
)