Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 43 additions & 40 deletions desdeo/problem/gurobipy_evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ class GurobipyEvaluatorWarning(UserWarning):
class GurobipyEvaluator:
"""Defines as evaluator that transforms an instance of Problem into a GurobipyModel."""

# gp.Model does not support these, so the evaluator will handle them
variables: dict[str, gp.Var | gp.MVar]
constraints: dict[str, gp.Constr]
objective_functions: dict[str, gp.Var | gp.MVar | gp.LinExpr | gp.QuadExpr | gp.MLinExpr | gp.MQuadExpr]
scalarizations: dict[str, gp.Var | gp.MVar | gp.LinExpr | gp.QuadExpr | gp.MLinExpr | gp.MQuadExpr]
extra_functions: dict[
Expand All @@ -54,13 +55,14 @@ def __init__(self, problem: Problem):
self.scalarizations = {}
self.extra_functions = {}
self.constants = {}
self.mvars = {}
self.variables = {}
self.constraints = {}

# set the parser
self.parse = MathParser(to_format=FormatEnum.gurobipy).parse

# Add variables
self.model = self.init_variables(problem)
self.variables = self.init_variables(problem)

# Add constants, if any
if problem.constants is not None:
Expand All @@ -75,15 +77,15 @@ def __init__(self, problem: Problem):

# Add constraints, if any
if problem.constraints is not None:
self.model = self.init_constraints(problem)
self.constraints = self.init_constraints(problem)

# Add scalarization functions, if any
if problem.scalarization_funcs is not None:
self.scalarizations = self.init_scalarizations(problem)

self.problem = problem

def init_variables(self, problem: Problem) -> gp.Model:
def init_variables(self, problem: Problem) -> dict[str, gp.Var | gp.MVar]:
"""Add variables to the GurobipyModel.

Args:
Expand All @@ -94,8 +96,9 @@ def init_variables(self, problem: Problem) -> gp.Model:
I.e., the variables are of a non supported type.

Returns:
GurobipyModel: the GurobipyModel with the variables added as attributes.
dict[str, gp.Var | gp.MVar]: the variables added to the model.
"""
variables = {}
for var in problem.variables:
if isinstance(var, Variable):
# handle regular variables
Expand All @@ -121,6 +124,7 @@ def init_variables(self, problem: Problem) -> gp.Model:
# set the initial value, if one has been defined
if var.initial_value is not None:
gvar.setAttr("Start", var.initial_value)
variables[var.symbol] = gvar

elif isinstance(var, TensorVariable):
# handle tensor variables, i.e., vectors etc..
Expand Down Expand Up @@ -160,12 +164,12 @@ def init_variables(self, problem: Problem) -> gp.Model:
# set the initial value, if one has been defined
if var.initial_values is not None:
gvar.setAttr("Start", np.array(var.get_initial_values()))
self.mvars[var.symbol] = gvar
variables[var.symbol] = gvar

# update the model before returning, so that other expressions can reference the variables
self.model.update()

return self.model
return variables

def init_constants(self, problem: Problem) -> dict[str, int | float | list[int] | list[float]]:
"""Add constants to a GurobipyEvaluator.
Expand Down Expand Up @@ -276,6 +280,7 @@ def init_constraints(self, problem: Problem) -> gp.Model:
Returns:
GurobipyModel: the GurobipyModel with the constraint expressions added.
"""
constraints = {}
for cons in problem.constraints:
gp_expr = self.parse(cons.func, callback=self.get_expression_by_name)

Expand All @@ -289,10 +294,10 @@ def init_constraints(self, problem: Problem) -> gp.Model:
msg = f"Constraint type of {con_type} not supported. Must be one of {ConstraintTypeEnum}."
raise GurobipyEvaluatorError(msg)

self.model.addConstr(gp_expr, name=cons.symbol)
constraints[cons.symbol] = self.model.addConstr(gp_expr, name=cons.symbol)

self.model.update()
return self.model
return constraints

def init_scalarizations(
self, problem: Problem
Expand Down Expand Up @@ -345,6 +350,7 @@ def add_constraint(self, constraint: Constraint) -> gp.Constr:

return_cons = self.model.addConstr(gp_expr, name=constraint.symbol)
self.model.update()
self.constraints[constraint.symbol] = return_cons
return return_cons

def add_objective(self, obj: Objective):
Expand Down Expand Up @@ -461,14 +467,16 @@ def add_variable(self, var: Variable | TensorVariable) -> gp.Var | gp.MVar:
# set the initial value, if one has been defined
if var.initial_values is not None:
gvar.setAttr("Start", np.array(var.get_initial_values()))
self.mvars[var.symbol] = gvar

self.model.update()
self.variables[var.symbol] = gvar
return gvar

def get_expression_by_name(
self, name: str
) -> gp.Var | gp.MVar | gp.LinExpr | gp.QuadExpr | gp.MLinExpr | gp.MQuadExpr | gp.GenExpr | int | float:
) -> (
gp.Var | gp.MVar | gp.LinExpr | gp.QuadExpr | gp.MLinExpr | gp.MQuadExpr | gp.GenExpr | int | float | np.ndarray
):
"""Returns a gurobipy expression corresponding to the name.

Only looks for variables, objective functions, scalarizations, extra functions, and constants.
Expand All @@ -480,20 +488,22 @@ def get_expression_by_name(
Returns:
gurobipy expression: A mathematical expression that gp.Model can use either as a constraint or an objective
"""
expression = self.model.getVarByName(name)
if expression is None:
# check if an MVar by checking gurobi.MVars stored in the evaluator directly,
# which results in terms multiplied by zero being removed from the equations:
if name in self.mvars:
expression = self.mvars[name]
elif name in self.objective_functions:
expression = self.objective_functions[name]
elif name in self.scalarizations:
expression = self.scalarizations[name]
elif name in self.extra_functions:
expression = self.extra_functions[name]
elif name in self.constants:
if name in self.variables:
expression = self.variables[name]
elif name in self.objective_functions:
expression = self.objective_functions[name]
elif name in self.scalarizations:
expression = self.scalarizations[name]
elif name in self.extra_functions:
expression = self.extra_functions[name]
elif name in self.constants:
if isinstance(self.constants[name], list):
expression = np.array(self.constants[name])
else:
expression = self.constants[name]
else:
msg = f"No expression with name {name} found in the gurobipy model."
raise GurobipyEvaluatorError(msg)
return expression

def get_values(self) -> dict[str, float | int | bool | list[float] | list[int]]:
Expand All @@ -507,12 +517,7 @@ def get_values(self) -> dict[str, float | int | bool | list[float] | list[int]]:
result_dict = {}

for var in self.problem.variables:
# if var is type MVar, get the values of MVar
if var.symbol in self.mvars:
result_dict[var.symbol] = self.mvars[var.symbol].getAttr(gp.GRB.Attr.X)
else:
result_dict[var.symbol] = self.model.getVarByName(var.symbol).getAttr(gp.GRB.Attr.X)

result_dict[var.symbol] = self.variables[var.symbol].getAttr(gp.GRB.Attr.X)
for obj in self.problem.objectives:
result_dict[obj.symbol] = self.objective_functions[obj.symbol].getValue()

Expand All @@ -526,7 +531,7 @@ def get_values(self) -> dict[str, float | int | bool | list[float] | list[int]]:

if self.problem.constraints is not None:
for const in self.problem.constraints:
result_dict[const.symbol] = -self.model.getConstrByName(const.symbol).getAttr("Slack")
result_dict[const.symbol] = -self.constraints[const.symbol].getAttr("Slack")

if self.problem.scalarization_funcs is not None:
for scal in self.problem.scalarization_funcs:
Expand All @@ -544,7 +549,8 @@ def remove_constraint(self, symbol: str):
Args:
symbol (str): a str representing the symbol of the constraint to be removed.
"""
self.model.remove(self.model.getConstrByName(symbol))
self.model.remove(self.constraints[symbol])
self.constraints.pop(symbol)
self.model.update()

def remove_variable(self, symbol: str):
Expand All @@ -557,18 +563,15 @@ def remove_variable(self, symbol: str):
Args:
symbol (str): a str representing the symbol of the variable to be removed.
"""
if symbol in self.mvars:
self.model.remove(self.mvars[symbol])
self.mvars.pop(symbol)
else:
self.model.remove(self.model.getVarByName(symbol))
self.model.remove(self.variables[symbol])
self.variables.pop(symbol)
self.model.update()

def set_optimization_target(self, target: str, maximize: bool = False): # noqa: FBT001, FBT002
def set_optimization_target(self, target: str, maximize: bool = False):
"""Sets a minimization objective to match the target objective or scalarization of the gurobipy model.

Args:
target (str): an str representing a symbol. Needs to match an objective function or scaralization
target (str): an str representing a symbol. Needs to match an objective function or scalarization
function already found in the model.
maximize (bool): If true, the target function is maximized instead of minimized

Expand Down
2 changes: 2 additions & 0 deletions desdeo/problem/testproblems/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"river_pollution_problem_discrete",
"river_pollution_scenario",
"rocket_injector_design",
"simple_constrained_quadratic_tensor_test_problem",
"simple_data_problem",
"simple_integer_test_problem",
"simple_knapsack",
Expand Down Expand Up @@ -74,6 +75,7 @@
)
from .rocket_injector_design_problem import rocket_injector_design
from .simple_problem import (
simple_constrained_quadratic_tensor_test_problem,
simple_data_problem,
simple_integer_test_problem,
simple_linear_test_problem,
Expand Down
69 changes: 67 additions & 2 deletions desdeo/problem/testproblems/simple_problem.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Defines simple test problems for testing purposes."""

from desdeo.problem.schema import (
Constant,
Constraint,
Expand All @@ -7,10 +9,13 @@
Objective,
ObjectiveTypeEnum,
Problem,
TensorConstant,
TensorVariable,
Variable,
VariableTypeEnum,
)


def simple_test_problem() -> Problem:
"""Defines a simple problem suitable for testing purposes."""
variables = [
Expand Down Expand Up @@ -131,8 +136,8 @@ def simple_data_problem() -> Problem:
func=None,
objective_type=ObjectiveTypeEnum.data_based,
maximize=i == 1,
ideal=3000 if i == 1 else -60.0 if i == 3 else 0,
nadir=0 if i == 1 else 15 - 2.0 if i == 3 else 15,
ideal=3000 if i == 1 else -60.0 if i == 3 else 0, # noqa: PLR2004
nadir=0 if i == 1 else 15 - 2.0 if i == 3 else 15, # noqa: PLR2004
)
for i in range(1, n_objectives + 1)
]
Expand Down Expand Up @@ -188,6 +193,66 @@ def simple_linear_test_problem() -> Problem:
)


def simple_constrained_quadratic_tensor_test_problem() -> Problem:
"""Defines a simple constrained quadratic problem with tensor variables, suitable for testing purposes."""
xvar = TensorVariable(
name="X",
symbol="X",
variable_type=VariableTypeEnum.real,
shape=[
2,
],
initial_values=[1, 1],
lowerbounds=[-10, -10],
upperbounds=[10, 10],
)

mmult = TensorConstant(
name="Mmult",
symbol="A",
shape=[2, 2],
values=[[1, 0.5], [0.5, 1]],
)

bvector = TensorConstant(
name="mcon",
symbol="b",
shape=[
2,
],
values=[1, 1],
)

cons = Constraint(
name="cons",
symbol="cons",
cons_type=ConstraintTypeEnum.LTE,
func="b-A@X",
is_linear=True,
is_convex=True,
is_twice_differentiable=True,
)

obj = Objective(
name="f_1",
symbol="f_1",
func="-0.5*X@X", # this is equivalent to 0.5*X.T@X, since X is a 1D tensor variable
maximize=True,
is_linear=False,
is_convex=True,
is_twice_differentiable=True,
)

return Problem(
name="Simple constrained quadratic tensor test problem.",
description="A simple problem for testing purposes.",
variables=[xvar],
constants=[mmult, bvector],
constraints=[cons],
objectives=[obj],
)


def simple_scenario_test_problem():
"""Returns a simple, scenario-based multiobjective optimization test problem."""
constants = [Constant(name="c_1", symbol="c_1", value=3)]
Expand Down
4 changes: 4 additions & 0 deletions desdeo/tools/gurobipy_solver_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ def __init__(self, problem: Problem, options: dict[str, any] | None = None):
if options is not None:
for key, value in options.items():
self.evaluator.model.setParam(key, value)
else:
# Set some default parameters that are good for most problems.
self.evaluator.model.setParam("OutputFlag", 0) # Suppress Gurobi output
self.evaluator.model.setParam("LogToConsole", 0) # Suppress Gurobi logging to console

def solve(self, target: str) -> SolverResults:
"""Solve the problem for the given target.
Expand Down
Loading
Loading