Model/domain/modelling/scenario.py
Khalim Conn-Kowlessar caa0847b70 Scenario carries its Fabric First flag from the scenario table 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 11:44:31 +00:00

50 lines
2 KiB
Python

"""Scenario — the named retrofit brief the Modelling stage scores against.
Built by a user in the scenario-builder UI and persisted before any modelling
fires; the pipeline is handed only its id and reads it back via a
`ScenarioRepository`. This is the thin slice the Modelling stage uses today:
the goal + budget that the Optimiser will consume (#1160) and `is_default`
(which drives `plan.is_default`). The legacy file-path / portfolio-aggregate
columns are not modelled. Carries no phases — multi-phase is deferred
(ADR-0005). See CONTEXT.md.
"""
from dataclasses import dataclass
from typing import Optional
from domain.modelling.measure_type import MeasureType
_NO_EXCLUSIONS: frozenset[MeasureType] = frozenset()
@dataclass(frozen=True)
class Scenario:
"""A retrofit brief: its goal, optional budget, and whether it is the
Property's default Scenario. `goal` / `goal_value` are the lodged target
(e.g. "INCREASING_EPC" → band "C"); carried for the Optimiser, not yet
enforced.
`exclusions` are the measure types the brief bars from the run (the only
measure-scoping the live ``scenario`` table persists — there is no
inclusions column). Empty means nothing is barred.
`fabric_first` constrains the Optimiser to treat the building envelope
first: fabric measures are optimised with the full budget, and heating /
renewables are only considered on top of the committed fabric when the
fabric alone does not meet the brief's target."""
id: int
goal: str
goal_value: str
budget: Optional[float]
is_default: bool
exclusions: frozenset[MeasureType] = _NO_EXCLUSIONS
fabric_first: bool = False
def considered_measures(self) -> Optional[frozenset[MeasureType]]:
"""The measure-type allowlist the Scenario's exclusions imply: every
modelled measure minus the excluded ones, or None (consider every
measure) when nothing is excluded."""
if not self.exclusions:
return None
return frozenset(MeasureType) - self.exclusions