from enum import Enum from mip import OptimizationStatus from typing import Mapping, Optional, TypedDict, List from recommendations.optimiser.CostOptimiser import CostOptimiser from recommendations.optimiser.GainOptimiser import GainOptimiser class Measure(TypedDict): id: str cost: float gain: float class Strategies(Enum): CASE_1_TRY_MIN_COST_WITH_CONSTRAINTS = "case_1_try_min_cost_with_constraints" CASE_1_SOLVE_MAX_GAIN_UNDER_BUDGET = "case_1_solve_max_gain_under_budget" CASE_2_SOLVE_MAX_GAIN_UNDER_BUDGET = "case_2_solve_max_gain_under_budget" CASE_3_SOLVE_MIN_COST_FOR_TARGET = "case_3_solve_min_cost_for_target" class StrategicOptimiser: """ Domain-level optimiser implementing logical optimisation logic. Behaviour: 1) If both budget and target_gain are provided: - Minimise cost subject to: gain >= target_gain cost <= budget - If infeasible: maximise gain subject to cost <= budget 2) If only budget is provided: - Maximise gain under budget 3) If only target_gain is provided: - Minimise cost to achieve gain """ def __init__( self, components: list[list[Mapping[str, int | float | str]]], budget: Optional[float] = None, target_gain: Optional[float] = None, allow_slack: bool = False, verbose: bool = False, ) -> None: if not components: raise ValueError("Components cannot be empty.") if budget is None and target_gain is None: raise ValueError("At least one of budget or target_gain must be provided.") self.components = components self.budget = budget self.target_gain = target_gain self.verbose = verbose self.allow_slack = allow_slack self.solution: List[Measure] = [] self.solution_cost: float = 0.0 self.solution_gain: float = 0.0 # For debugging purposes, we keep a record of which option was selected self.strategy_used: Optional[Strategies] = None def solve(self) -> None: """ Primary entry point for solving the optimisation problem based on the provided budget and target gain. :return: """ # Case 1: budget + target if self.budget is not None and self.target_gain is not None: # Given: # Budget B # Target gain G # # We want the solution to: # # Primary problem (P1) # min cost # subject to # # gain >= G # cost <= B # multiple-choice constraints # # If (P1) is feasible → that solution is exactly what you want. # If (P1) is infeasible → solve the following problem (P2): # # max gain # subject to # # cost <= B if self._try_min_cost_with_constraints(): # Keep a record of the strategy used to solve the problem, for debugging purposes self.strategy_used = Strategies.CASE_1_TRY_MIN_COST_WITH_CONSTRAINTS return self._solve_max_gain_under_budget() self.strategy_used = Strategies.CASE_1_SOLVE_MAX_GAIN_UNDER_BUDGET return # Case 2: budget only if self.budget is not None: self._solve_max_gain_under_budget() self.strategy_used = Strategies.CASE_2_SOLVE_MAX_GAIN_UNDER_BUDGET return # Case 3: target only self._solve_min_cost_for_target() self.strategy_used = Strategies.CASE_3_SOLVE_MIN_COST_FOR_TARGET return # --------------------------------------------------------- # Internal Functions # --------------------------------------------------------- def _try_min_cost_with_constraints(self) -> bool: """ Try to minimise cost while satisfying: gain >= target_gain cost <= budget """ opt = CostOptimiser( self.components, min_gain=self.target_gain, verbose=self.verbose, allow_slack=self.allow_slack ) opt.setup() opt.add_budget_constraint(self.budget) opt.solve() if opt.m.status == OptimizationStatus.INFEASIBLE: return False self._store_solution(opt.solution) return True def _solve_max_gain_under_budget(self) -> None: opt = GainOptimiser( self.components, max_cost=self.budget, max_gain=None, allow_slack=self.allow_slack, verbose=self.verbose ) opt.setup() opt.solve() self._store_solution(opt.solution) def _solve_min_cost_for_target(self) -> None: opt = CostOptimiser( self.components, min_gain=self.target_gain, verbose=self.verbose ) opt.setup() opt.solve() self._store_solution(opt.solution) def _store_solution(self, solution: List[Measure]) -> None: self.solution = solution self.solution_cost = sum(m["cost"] for m in solution) self.solution_gain = sum(m["gain"] for m in solution)