Model/tests/domain/modelling/test_considered_measures.py
Khalim Conn-Kowlessar 7942a8101a feat(modelling): considered_measures allowlist on the orchestrator
Add domain/modelling/considered_measures.py::restrict_to_considered_measures —
the pure allowlist that limits a run to a chosen set of MeasureType (mirroring
the legacy engine's `inclusions`). It filters at the Option level, so a
multi-option Recommendation (e.g. Heating & Hot Water competing HHRSH against
an ASHP bundle) is kept with only its allowed Options; a Recommendation left
with none is dropped. None = consider everything (unrestricted default).

Thread `considered_measures: frozenset[MeasureType] | None` through
ModellingOrchestrator.run -> _plan_for -> _scored_candidate_groups /
_candidate_recommendations (applies the filter) and _measure_dependencies
(suppresses a forced dependency whose required measure is outside the
allowlist, so a restricted run forces nothing it is not considering). The
local-run seam (harness.console.run_modelling) gains the same param.

The Optimiser still freely chooses among survivors — including none. Tests:
the pure filter (3 cases) + an orchestrator-seam test proving a
{solar_pv}-restricted run yields only solar_pv options. 257 pass + 3 xfail;
pyright clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 20:32:11 +00:00

83 lines
2.8 KiB
Python

"""Slice 3 — `restrict_to_considered_measures`, the pure allowlist that limits a
run to a chosen set of measure types (mirroring the legacy engine's
`inclusions`).
It filters at the Option level, so a multi-option Recommendation (e.g. Heating &
Hot Water offering both HHRSH and an ASHP bundle) is kept with only its allowed
Options; a Recommendation left with no allowed Option is dropped entirely. A
None allowlist means "consider everything" (today's unrestricted behaviour).
"""
from domain.modelling.considered_measures import restrict_to_considered_measures
from domain.modelling.measure_type import MeasureType
from domain.modelling.recommendation import MeasureOption, Recommendation
from domain.modelling.simulation import EpcSimulation
def _option(measure_type: MeasureType) -> MeasureOption:
return MeasureOption(
measure_type=measure_type, description=str(measure_type), overlay=EpcSimulation()
)
def _heating_rec() -> Recommendation:
# Heating & Hot Water competes HHRSH against an ASHP bundle in one rec.
return Recommendation(
surface="Heating & Hot Water",
options=(
_option(MeasureType.HIGH_HEAT_RETENTION_STORAGE_HEATERS),
_option(MeasureType.AIR_SOURCE_HEAT_PUMP),
),
)
def _solar_rec() -> Recommendation:
return Recommendation(surface="Solar PV", options=(_option(MeasureType.SOLAR_PV),))
def _wall_rec() -> Recommendation:
return Recommendation(
surface="Wall", options=(_option(MeasureType.CAVITY_WALL_INSULATION),)
)
def test_none_allowlist_keeps_everything() -> None:
# Arrange
recommendations = [_heating_rec(), _solar_rec(), _wall_rec()]
# Act
kept = restrict_to_considered_measures(recommendations, None)
# Assert
assert kept == recommendations
def test_drops_recommendations_with_no_allowed_option() -> None:
# Arrange
considered = frozenset(
{MeasureType.HIGH_HEAT_RETENTION_STORAGE_HEATERS, MeasureType.SOLAR_PV}
)
# Act
kept = restrict_to_considered_measures(
[_heating_rec(), _solar_rec(), _wall_rec()], considered
)
# Assert — the wall rec is gone; heating + solar survive.
surfaces = {rec.surface for rec in kept}
assert surfaces == {"Heating & Hot Water", "Solar PV"}
def test_filters_options_within_a_kept_recommendation() -> None:
# Arrange — HHRSH is allowed but the competing ASHP bundle is not.
considered = frozenset(
{MeasureType.HIGH_HEAT_RETENTION_STORAGE_HEATERS, MeasureType.SOLAR_PV}
)
# Act
kept = restrict_to_considered_measures([_heating_rec()], considered)
# Assert — the heating rec keeps ONLY its HHRSH option, ASHP is dropped.
assert len(kept) == 1
kept_types = [option.measure_type for option in kept[0].options]
assert kept_types == [MeasureType.HIGH_HEAT_RETENTION_STORAGE_HEATERS]