mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
measure_dependency.py now owns only the selection semantics: the trigger set and the forced-edge wrapping. It delegates production (detection + pricing) to recommend_ventilation and wraps the returned Recommendation into the MeasureDependency, picking the cheapest Option (one MEV today; readies the seam for MEV-c / MVHR). The orchestrator's _measure_dependencies call is unchanged. Trimmed the now-redundant option-detail assertions — those live in test_ventilation_recommendation. 138 pass, behaviour-preserving. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
72 lines
3.3 KiB
Python
72 lines
3.3 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.
|
|
|
|
This module owns only the **selection semantics** (the trigger set + the
|
|
forced-edge wrapping). **Production** — detecting that the dwelling needs
|
|
ventilation and pricing the work — is the ventilation Recommendation Generator's
|
|
job (`generators.ventilation_recommendation`), exactly like wall/roof/floor.
|
|
`ventilation_dependency` delegates to it and wraps its Recommendation into the
|
|
forced edge; the Recommendation is consumed here, never offered to the pool.
|
|
"""
|
|
|
|
from typing import Optional
|
|
|
|
from datatypes.epc.domain.epc_property_data import EpcPropertyData
|
|
from domain.modelling.generators.ventilation_recommendation import (
|
|
recommend_ventilation,
|
|
)
|
|
from domain.modelling.optimisation.optimiser import MeasureDependency, ScoredOption
|
|
from domain.modelling.recommendation import MeasureOption, Recommendation
|
|
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",
|
|
}
|
|
)
|
|
|
|
|
|
def ventilation_dependency(
|
|
epc: EpcPropertyData, products: ProductRepository
|
|
) -> Optional[MeasureDependency]:
|
|
"""The ventilation Measure Dependency for a dwelling, or None when it needs
|
|
no ventilation (already mechanically ventilated). Delegates production —
|
|
detection + pricing — to the ventilation Recommendation Generator, then
|
|
wraps its Recommendation into the forced "fabric requires ventilation"
|
|
edge."""
|
|
recommendation: Optional[Recommendation] = recommend_ventilation(epc, products)
|
|
if recommendation is None:
|
|
return None
|
|
return MeasureDependency(
|
|
triggers=MEASURES_NEEDING_VENTILATION,
|
|
# Forced, never freely scored — the role-1 signal is irrelevant (0.0).
|
|
required=ScoredOption(option=_required_option(recommendation), sap_gain=0.0),
|
|
)
|
|
|
|
|
|
def _required_option(recommendation: Recommendation) -> MeasureOption:
|
|
"""Pick the Option the dependency forces in — the cheapest, mirroring the
|
|
legacy "default to the cheapest ventilation unit". There is one MEV Option
|
|
today; this readies the seam for MEV-c / MVHR alternatives."""
|
|
return min(recommendation.options, key=_option_total)
|
|
|
|
|
|
def _option_total(option: MeasureOption) -> float:
|
|
if option.cost is None:
|
|
raise ValueError(
|
|
f"ventilation option {option.measure_type!r} has no cost; cannot force in"
|
|
)
|
|
return option.cost.total
|