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>
98 lines
3.4 KiB
Python
98 lines
3.4 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 (
|
|
combine_considered_measures,
|
|
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_combine_treats_none_as_all_and_intersects_two_allowlists() -> None:
|
|
# Arrange
|
|
a = frozenset({MeasureType.SOLAR_PV, MeasureType.LOFT_INSULATION})
|
|
b = frozenset({MeasureType.SOLAR_PV, MeasureType.CAVITY_WALL_INSULATION})
|
|
|
|
# Act / Assert — None means "all", so it never narrows; two sets intersect.
|
|
assert combine_considered_measures(None, b) == b
|
|
assert combine_considered_measures(a, None) == a
|
|
assert combine_considered_measures(None, None) is None
|
|
assert combine_considered_measures(a, b) == frozenset({MeasureType.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]
|