mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
The measures a run considers should come from the Scenario, not a CLI flag.
The live scenario table persists exclusions only (no inclusions column), as a
Postgres text-array of exact MeasureType values.
- Scenario gains `exclusions: frozenset[MeasureType]` + `considered_measures()`
(all measures minus the excluded ones, or None when none are excluded).
- ScenarioModel.to_domain parses the `{a,b,c}` exclusions array into
MeasureTypes, raising on a token that is not an exact MeasureType value
(no high-level category expansion), per the strict-enum convention.
- ModellingOrchestrator._plan_for derives the allowlist from the Scenario's
exclusions, combined (intersection) with any explicit considered_measures
override via the new `combine_considered_measures`.
- run_modelling_e2e sources the allowlist from the Scenario; --measures /
--exclude-measures become optional overlays (e.g. the technical
secondary_heating_removal exclusion the catalogue cannot yet stock).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
56 lines
2.1 KiB
Python
56 lines
2.1 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 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
|