mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
149 lines
5.2 KiB
Python
149 lines
5.2 KiB
Python
from mip import Model, xsum, minimize, BINARY, OptimizationStatus
|
|
from utils.logger import setup_logger
|
|
|
|
logger = setup_logger()
|
|
|
|
|
|
class CostOptimiser:
|
|
"""
|
|
This class is used to minimise cost, given a constrained minimum gain
|
|
"""
|
|
|
|
# We add an optional buffer to the minimum gain to allow for slack in the optimisation
|
|
BUFFER = 0.2
|
|
|
|
def __init__(
|
|
self, components, min_gain, verbose=False, allow_slack=True
|
|
):
|
|
self.components = components
|
|
self.min_gain = min_gain
|
|
self.gain_constraint = None
|
|
self.m = None
|
|
self.variables = []
|
|
self.solution = []
|
|
self.allow_slack = allow_slack
|
|
|
|
self.solution_cost = None
|
|
self.solution_gain = None
|
|
self.verbose = verbose
|
|
|
|
@classmethod
|
|
def calculate_sap_gain_with_slack(cls, min_gain: int | float):
|
|
"""
|
|
Adds a small amount of buffer to the minimum gain, to account for possible error in SAP predictions
|
|
:param min_gain: Numerical value for the minimum gain
|
|
:return:
|
|
"""
|
|
if min_gain == 0:
|
|
return min_gain
|
|
elif min_gain <= 5:
|
|
return min_gain + 0.25
|
|
elif min_gain <= 20:
|
|
return min_gain + 0.5
|
|
else:
|
|
return min_gain + 0.75
|
|
|
|
def setup(self):
|
|
# Initialize Model
|
|
self.m = Model("knapsack")
|
|
# Set the verbosity level
|
|
self.m.verbose = 1 if self.verbose else 0
|
|
|
|
# Create variables
|
|
self.variables = [
|
|
[self.m.add_var(var_type=BINARY, name=str(component["id"])) for component in group] for group in
|
|
self.components
|
|
]
|
|
|
|
# Set objective
|
|
# This objective is to minimize
|
|
# cost_ig * x_ig, where cost_ig represents the cost for ith part in group g
|
|
# and x_ig is the binary decision variable for the ith part in group g
|
|
self.m.objective = minimize(
|
|
xsum(
|
|
component['cost'] * var for group, group_vars in zip(self.components, self.variables) for component, var
|
|
in
|
|
zip(group, group_vars)
|
|
)
|
|
)
|
|
|
|
# Add constraints
|
|
# This constrain ensures that sum of gain_ig * x_ig >= min_gain, where gain_ig represents the gain for the ith
|
|
# component
|
|
# in group g, and x_ig is the binary decision variable for the ith component in group g
|
|
gain_expression = xsum(
|
|
item['gain'] * var for group, group_vars in zip(self.components, self.variables) for item, var in
|
|
zip(group, group_vars)
|
|
) >= self.min_gain
|
|
|
|
self.gain_constraint = self.m.add_constr(gain_expression)
|
|
|
|
# At most one item from each group
|
|
# This constraint ensures that at most one item from each group is selected
|
|
# This is expressed by summing up the decision variables for each group and ensuring that the sum is <= 1
|
|
for group_vars in self.variables:
|
|
self.m += xsum(var for var in group_vars) <= 1
|
|
|
|
def add_budget_constraint(self, budget: int | float) -> None:
|
|
# Inject budget constraint, which ensures that sum of cost_ig * x_ig <= budget, where cost_ig represents the
|
|
# cost for the ith component in group g, and x_ig is the binary decision variable for the ith component in
|
|
# group g
|
|
|
|
self.m += (
|
|
xsum(
|
|
item["cost"] * var
|
|
for group, group_vars in zip(self.components, self.variables)
|
|
for item, var in zip(group, group_vars)
|
|
)
|
|
<= budget
|
|
)
|
|
|
|
def setup_slack(self):
|
|
|
|
# Remove the original gain constraint
|
|
self.m.remove(self.gain_constraint)
|
|
# Add slack variable
|
|
s = self.m.add_var(lb=0)
|
|
|
|
# Modify the constraint
|
|
self.m += xsum(
|
|
item['gain'] * var for group, group_vars in zip(self.components, self.variables) for item, var in
|
|
zip(group, group_vars)
|
|
) + s >= self.min_gain
|
|
|
|
# Modify the objective to penalize the use of slack
|
|
penalty = 10000 # you can adjust this based on how much you want to penalize the use of slack
|
|
self.m.objective = minimize(
|
|
xsum(
|
|
component['cost'] * var for group, group_vars in zip(self.components, self.variables) for component, var
|
|
in
|
|
zip(group, group_vars)
|
|
) + penalty * s
|
|
)
|
|
|
|
def solve(self):
|
|
# Solve the problem
|
|
self.m.optimize()
|
|
|
|
if self.m.status == OptimizationStatus.INFEASIBLE:
|
|
if self.allow_slack:
|
|
self.setup_slack()
|
|
self.m.optimize()
|
|
else:
|
|
# Explicity return an empty solution
|
|
self.solution = []
|
|
self.solution_cost = 0
|
|
self.solution_gain = 0
|
|
return
|
|
|
|
# If we still have an infeasible solution, we return an empty solution
|
|
|
|
self.solution = [
|
|
item for group, group_vars in zip(self.components, self.variables) for item, var in zip(group, group_vars)
|
|
if
|
|
var.x >= 0.99
|
|
]
|
|
|
|
# Get the selected items
|
|
self.solution_cost = self.m.objective.x
|
|
self.solution_gain = sum([component['gain'] for component in self.solution])
|