Model/domain/modelling/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

42 lines
1.6 KiB
Python

"""Restricting a modelling run to a chosen set of measure types.
The allowlist a run "considers" — mirroring the legacy engine's `inclusions`
(`backend/app/plan/schemas.py`). It filters the candidate Recommendations 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 no allowed Option is dropped. The Optimiser still
freely chooses among what survives — including choosing nothing.
A `None` allowlist means "consider every modelled measure" (the unrestricted
default).
"""
from __future__ import annotations
from collections.abc import Iterable
from typing import Optional
from domain.modelling.measure_type import MeasureType
from domain.modelling.recommendation import Recommendation
def restrict_to_considered_measures(
recommendations: Iterable[Recommendation],
considered_measures: Optional[frozenset[MeasureType]],
) -> list[Recommendation]:
"""Keep only the Options whose measure type is in ``considered_measures``,
dropping any Recommendation left with none. ``None`` keeps everything."""
if considered_measures is None:
return list(recommendations)
restricted: list[Recommendation] = []
for recommendation in recommendations:
kept = tuple(
option
for option in recommendation.options
if option.measure_type in considered_measures
)
if kept:
restricted.append(
Recommendation(surface=recommendation.surface, options=kept)
)
return restricted