"""Behaviour of the Optimiser core: a grouped-knapsack MILP over per-Option role-1 scores (ADR-0016). Picks at most one Option per Recommendation (disjoint groups, no cross-group constraints) to maximise total SAP gain subject to the Scenario budget. This is the warm-start *signal* — the truthful figure comes from the whole-package re-score + repair (a later slice); here we test the selection with synthetic scores and no calculator. """ from __future__ import annotations from domain.modelling.optimiser import ScoredOption, optimise from domain.modelling.recommendation import Cost, MeasureOption from domain.modelling.simulation import EpcSimulation def _scored(measure_type: str, *, gain: float, cost: float) -> ScoredOption: return ScoredOption( option=MeasureOption( measure_type=measure_type, description=measure_type, overlay=EpcSimulation(), cost=Cost(total=cost, contingency_rate=0.0), ), sap_gain=gain, ) def _selected_types(selection: list[ScoredOption]) -> set[str]: return {scored.option.measure_type for scored in selection} def test_grouped_knapsack_maximises_gain_within_budget() -> None: # Arrange — wall group has two mutually-exclusive options; roof + floor one # each. EWI has the best gain but is unaffordable alongside the rest. groups: list[list[ScoredOption]] = [ [ _scored("external_wall_insulation", gain=10.0, cost=8000.0), _scored("cavity_wall_insulation", gain=6.0, cost=1000.0), ], [_scored("loft_insulation", gain=4.0, cost=1500.0)], [_scored("suspended_floor_insulation", gain=3.0, cost=2000.0)], ] # Act selection: list[ScoredOption] = optimise(groups, budget=5000.0) # Assert — cavity + loft + floor (cost 4500, gain 13) beats any package # containing the 8000 EWI option within the 5000 budget. assert _selected_types(selection) == { "cavity_wall_insulation", "loft_insulation", "suspended_floor_insulation", } def test_picks_at_most_one_option_per_group() -> None: # Arrange — both wall options are individually affordable. groups: list[list[ScoredOption]] = [ [ _scored("external_wall_insulation", gain=10.0, cost=2000.0), _scored("cavity_wall_insulation", gain=6.0, cost=1000.0), ], ] # Act selection: list[ScoredOption] = optimise(groups, budget=10000.0) # Assert — never both treatments of the same wall; the higher-gain one wins. assert len(selection) == 1 assert _selected_types(selection) == {"external_wall_insulation"} def test_no_budget_picks_the_best_option_in_every_group() -> None: # Arrange groups: list[list[ScoredOption]] = [ [ _scored("external_wall_insulation", gain=10.0, cost=8000.0), _scored("cavity_wall_insulation", gain=6.0, cost=1000.0), ], [_scored("loft_insulation", gain=4.0, cost=1500.0)], ] # Act — None budget = unconstrained. selection: list[ScoredOption] = optimise(groups, budget=None) # Assert assert _selected_types(selection) == { "external_wall_insulation", "loft_insulation", } def test_budget_too_small_for_any_option_selects_nothing() -> None: # Arrange groups: list[list[ScoredOption]] = [ [_scored("cavity_wall_insulation", gain=6.0, cost=1000.0)], [_scored("loft_insulation", gain=4.0, cost=1500.0)], ] # Act selection: list[ScoredOption] = optimise(groups, budget=500.0) # Assert — nothing affordable; selecting none is the optimum. assert selection == [] def test_no_groups_selects_nothing() -> None: # Act / Assert assert optimise([], budget=10000.0) == [] def test_within_budget_partial_selection_prefers_the_higher_gain_option() -> None: # Arrange — only one of the two fits the budget; pick the affordable best. groups: list[list[ScoredOption]] = [ [_scored("external_wall_insulation", gain=10.0, cost=8000.0)], [_scored("loft_insulation", gain=4.0, cost=1500.0)], ] # Act selection: list[ScoredOption] = optimise(groups, budget=2000.0) # Assert — EWI is unaffordable; loft alone is the best within £2000. assert _selected_types(selection) == {"loft_insulation"}