"""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 combine_considered_measures( a: Optional[frozenset[MeasureType]], b: Optional[frozenset[MeasureType]], ) -> Optional[frozenset[MeasureType]]: """Intersect two allowlists, treating ``None`` as "all measures". Used to layer an explicit override over the allowlist a Scenario's exclusions imply: None ∧ x = x, and both present narrows to their intersection.""" if a is None: return b if b is None: return a return a & b 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