desdeo 1.2__py3-none-any.whl → 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- desdeo/__init__.py +8 -8
- desdeo/api/README.md +73 -0
- desdeo/api/__init__.py +15 -0
- desdeo/api/app.py +40 -0
- desdeo/api/config.py +69 -0
- desdeo/api/config.toml +53 -0
- desdeo/api/db.py +25 -0
- desdeo/api/db_init.py +79 -0
- desdeo/api/db_models.py +164 -0
- desdeo/api/malaga_db_init.py +27 -0
- desdeo/api/models/__init__.py +66 -0
- desdeo/api/models/archive.py +34 -0
- desdeo/api/models/preference.py +90 -0
- desdeo/api/models/problem.py +507 -0
- desdeo/api/models/reference_point_method.py +18 -0
- desdeo/api/models/session.py +46 -0
- desdeo/api/models/state.py +96 -0
- desdeo/api/models/user.py +51 -0
- desdeo/api/routers/_NAUTILUS.py +245 -0
- desdeo/api/routers/_NAUTILUS_navigator.py +233 -0
- desdeo/api/routers/_NIMBUS.py +762 -0
- desdeo/api/routers/__init__.py +5 -0
- desdeo/api/routers/problem.py +110 -0
- desdeo/api/routers/reference_point_method.py +117 -0
- desdeo/api/routers/session.py +76 -0
- desdeo/api/routers/test.py +16 -0
- desdeo/api/routers/user_authentication.py +366 -0
- desdeo/api/schema.py +94 -0
- desdeo/api/tests/__init__.py +0 -0
- desdeo/api/tests/conftest.py +59 -0
- desdeo/api/tests/test_models.py +701 -0
- desdeo/api/tests/test_routes.py +216 -0
- desdeo/api/utils/database.py +274 -0
- desdeo/api/utils/logger.py +29 -0
- desdeo/core.py +27 -0
- desdeo/emo/__init__.py +29 -0
- desdeo/emo/hooks/archivers.py +172 -0
- desdeo/emo/methods/EAs.py +418 -0
- desdeo/emo/methods/__init__.py +0 -0
- desdeo/emo/methods/bases.py +59 -0
- desdeo/emo/operators/__init__.py +1 -0
- desdeo/emo/operators/crossover.py +780 -0
- desdeo/emo/operators/evaluator.py +118 -0
- desdeo/emo/operators/generator.py +356 -0
- desdeo/emo/operators/mutation.py +1053 -0
- desdeo/emo/operators/selection.py +1036 -0
- desdeo/emo/operators/termination.py +178 -0
- desdeo/explanations/__init__.py +6 -0
- desdeo/explanations/explainer.py +100 -0
- desdeo/explanations/utils.py +90 -0
- desdeo/mcdm/__init__.py +19 -0
- desdeo/mcdm/nautili.py +345 -0
- desdeo/mcdm/nautilus.py +477 -0
- desdeo/mcdm/nautilus_navigator.py +655 -0
- desdeo/mcdm/nimbus.py +417 -0
- desdeo/mcdm/pareto_navigator.py +269 -0
- desdeo/mcdm/reference_point_method.py +116 -0
- desdeo/problem/__init__.py +79 -0
- desdeo/problem/evaluator.py +561 -0
- desdeo/problem/gurobipy_evaluator.py +562 -0
- desdeo/problem/infix_parser.py +341 -0
- desdeo/problem/json_parser.py +944 -0
- desdeo/problem/pyomo_evaluator.py +468 -0
- desdeo/problem/schema.py +1808 -0
- desdeo/problem/simulator_evaluator.py +298 -0
- desdeo/problem/sympy_evaluator.py +244 -0
- desdeo/problem/testproblems/__init__.py +73 -0
- desdeo/problem/testproblems/binh_and_korn_problem.py +88 -0
- desdeo/problem/testproblems/dtlz2_problem.py +102 -0
- desdeo/problem/testproblems/forest_problem.py +275 -0
- desdeo/problem/testproblems/knapsack_problem.py +163 -0
- desdeo/problem/testproblems/mcwb_problem.py +831 -0
- desdeo/problem/testproblems/mixed_variable_dimenrions_problem.py +83 -0
- desdeo/problem/testproblems/momip_problem.py +172 -0
- desdeo/problem/testproblems/nimbus_problem.py +143 -0
- desdeo/problem/testproblems/pareto_navigator_problem.py +89 -0
- desdeo/problem/testproblems/re_problem.py +492 -0
- desdeo/problem/testproblems/river_pollution_problem.py +434 -0
- desdeo/problem/testproblems/rocket_injector_design_problem.py +140 -0
- desdeo/problem/testproblems/simple_problem.py +351 -0
- desdeo/problem/testproblems/simulator_problem.py +92 -0
- desdeo/problem/testproblems/spanish_sustainability_problem.py +945 -0
- desdeo/problem/testproblems/zdt_problem.py +271 -0
- desdeo/problem/utils.py +245 -0
- desdeo/tools/GenerateReferencePoints.py +181 -0
- desdeo/tools/__init__.py +102 -0
- desdeo/tools/generics.py +145 -0
- desdeo/tools/gurobipy_solver_interfaces.py +258 -0
- desdeo/tools/indicators_binary.py +11 -0
- desdeo/tools/indicators_unary.py +375 -0
- desdeo/tools/interaction_schema.py +38 -0
- desdeo/tools/intersection.py +54 -0
- desdeo/tools/iterative_pareto_representer.py +99 -0
- desdeo/tools/message.py +234 -0
- desdeo/tools/ng_solver_interfaces.py +199 -0
- desdeo/tools/non_dominated_sorting.py +133 -0
- desdeo/tools/patterns.py +281 -0
- desdeo/tools/proximal_solver.py +99 -0
- desdeo/tools/pyomo_solver_interfaces.py +464 -0
- desdeo/tools/reference_vectors.py +462 -0
- desdeo/tools/scalarization.py +3138 -0
- desdeo/tools/scipy_solver_interfaces.py +454 -0
- desdeo/tools/score_bands.py +464 -0
- desdeo/tools/utils.py +320 -0
- desdeo/utopia_stuff/__init__.py +0 -0
- desdeo/utopia_stuff/data/1.json +15 -0
- desdeo/utopia_stuff/data/2.json +13 -0
- desdeo/utopia_stuff/data/3.json +15 -0
- desdeo/utopia_stuff/data/4.json +17 -0
- desdeo/utopia_stuff/data/5.json +15 -0
- desdeo/utopia_stuff/from_json.py +40 -0
- desdeo/utopia_stuff/reinit_user.py +38 -0
- desdeo/utopia_stuff/utopia_db_init.py +212 -0
- desdeo/utopia_stuff/utopia_problem.py +403 -0
- desdeo/utopia_stuff/utopia_problem_old.py +415 -0
- desdeo/utopia_stuff/utopia_reference_solutions.py +79 -0
- desdeo-2.0.0.dist-info/LICENSE +21 -0
- desdeo-2.0.0.dist-info/METADATA +168 -0
- desdeo-2.0.0.dist-info/RECORD +120 -0
- {desdeo-1.2.dist-info → desdeo-2.0.0.dist-info}/WHEEL +1 -1
- desdeo-1.2.dist-info/METADATA +0 -16
- desdeo-1.2.dist-info/RECORD +0 -4
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
"""Defines an evaluator compatible with the Problem JSON format and transforms it into a GurobipyModel."""
|
|
2
|
+
|
|
3
|
+
from operator import eq as _eq
|
|
4
|
+
from operator import le as _le
|
|
5
|
+
import warnings
|
|
6
|
+
|
|
7
|
+
import gurobipy as gp
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from desdeo.problem.json_parser import FormatEnum, MathParser
|
|
11
|
+
from desdeo.problem.schema import (
|
|
12
|
+
Constant,
|
|
13
|
+
Constraint,
|
|
14
|
+
ConstraintTypeEnum,
|
|
15
|
+
Objective,
|
|
16
|
+
Problem,
|
|
17
|
+
ScalarizationFunction,
|
|
18
|
+
TensorConstant,
|
|
19
|
+
TensorVariable,
|
|
20
|
+
Variable,
|
|
21
|
+
VariableTypeEnum
|
|
22
|
+
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class GurobipyEvaluatorError(Exception):
|
|
27
|
+
"""Raised when an error within the GurobipyEvaluator class is encountered."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class GurobipyEvaluatorWarning(UserWarning):
|
|
31
|
+
"""Raised when the problem contains features that are poorly supported in gurobipy."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class GurobipyEvaluator:
|
|
35
|
+
"""Defines as evaluator that transforms an instance of Problem into a GurobipyModel."""
|
|
36
|
+
|
|
37
|
+
# gp.Model does not support these, so the evaluator will handle them
|
|
38
|
+
objective_functions: dict[str, gp.Var | gp.MVar | gp.LinExpr | gp.QuadExpr | gp.MLinExpr | gp.MQuadExpr]
|
|
39
|
+
scalarizations: dict[str, gp.Var | gp.MVar | gp.LinExpr | gp.QuadExpr | gp.MLinExpr | gp.MQuadExpr]
|
|
40
|
+
extra_functions: dict[
|
|
41
|
+
str, gp.Var | gp.MVar | gp.LinExpr | gp.QuadExpr | gp.MLinExpr | gp.MQuadExpr | gp.GenExpr | int | float
|
|
42
|
+
]
|
|
43
|
+
constants: dict[str, int | float | list[int] | list[float]]
|
|
44
|
+
|
|
45
|
+
model: gp.Model
|
|
46
|
+
|
|
47
|
+
def __init__(self, problem: Problem):
|
|
48
|
+
"""Initialized the evaluator.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
problem (Problem): the problem to be transformed in a GurobipyModel.
|
|
52
|
+
"""
|
|
53
|
+
self.model = gp.Model(problem.name)
|
|
54
|
+
self.objective_functions = {}
|
|
55
|
+
self.scalarizations = {}
|
|
56
|
+
self.extra_functions = {}
|
|
57
|
+
self.constants = {}
|
|
58
|
+
self.mvars = {}
|
|
59
|
+
|
|
60
|
+
# set the parser
|
|
61
|
+
self.parse = MathParser(to_format=FormatEnum.gurobipy).parse
|
|
62
|
+
|
|
63
|
+
# Add variables
|
|
64
|
+
self.model = self.init_variables(problem)
|
|
65
|
+
|
|
66
|
+
# Add constants, if any
|
|
67
|
+
if problem.constants is not None:
|
|
68
|
+
self.constants = self.init_constants(problem)
|
|
69
|
+
|
|
70
|
+
# Add extra expressions, if any
|
|
71
|
+
if problem.extra_funcs is not None:
|
|
72
|
+
self.extra_functions = self.init_extras(problem)
|
|
73
|
+
|
|
74
|
+
# Add objective function expressions
|
|
75
|
+
self.objective_functions = self.init_objectives(problem)
|
|
76
|
+
|
|
77
|
+
# Add constraints, if any
|
|
78
|
+
if problem.constraints is not None:
|
|
79
|
+
self.model = self.init_constraints(problem)
|
|
80
|
+
|
|
81
|
+
# Add scalarization functions, if any
|
|
82
|
+
if problem.scalarization_funcs is not None:
|
|
83
|
+
self.scalarizations = self.init_scalarizations(problem)
|
|
84
|
+
|
|
85
|
+
self.problem = problem
|
|
86
|
+
|
|
87
|
+
def init_variables(self, problem: Problem) -> gp.Model:
|
|
88
|
+
"""Add variables to the GurobipyModel.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
problem (Problem): problem from which to extract the variables.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
GurobipyEvaluatorError: when a problem in extracting the variables is encountered.
|
|
95
|
+
I.e., the variables are of a non supported type.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
GurobipyModel: the GurobipyModel with the variables added as attributes.
|
|
99
|
+
"""
|
|
100
|
+
for var in problem.variables:
|
|
101
|
+
if isinstance(var, Variable):
|
|
102
|
+
# handle regular variables
|
|
103
|
+
lowerbound = var.lowerbound if var.lowerbound is not None else float("-inf")
|
|
104
|
+
upperbound = var.upperbound if var.upperbound is not None else float("inf")
|
|
105
|
+
|
|
106
|
+
# figure out the variable type
|
|
107
|
+
match var.variable_type:
|
|
108
|
+
case VariableTypeEnum.integer:
|
|
109
|
+
# variable is integer
|
|
110
|
+
domain = gp.GRB.INTEGER
|
|
111
|
+
case VariableTypeEnum.real:
|
|
112
|
+
# variable is real
|
|
113
|
+
domain = gp.GRB.CONTINUOUS
|
|
114
|
+
case VariableTypeEnum.binary:
|
|
115
|
+
domain = gp.GRB.BINARY
|
|
116
|
+
case _:
|
|
117
|
+
msg = f"Could not figure out the type for variable {var}."
|
|
118
|
+
raise GurobipyEvaluatorError(msg)
|
|
119
|
+
|
|
120
|
+
# add the variable to the model
|
|
121
|
+
gvar = self.model.addVar(lb=lowerbound, ub=upperbound, vtype=domain, name=var.symbol)
|
|
122
|
+
# set the initial value, if one has been defined
|
|
123
|
+
if var.initial_value is not None:
|
|
124
|
+
gvar.setAttr("Start", var.initial_value)
|
|
125
|
+
|
|
126
|
+
elif isinstance(var, TensorVariable):
|
|
127
|
+
# handle tensor variables, i.e., vectors etc..
|
|
128
|
+
lowerbounds = var.get_lowerbound_values() if var.lowerbounds is not None else np.full(var.shape, float("-inf")).tolist()
|
|
129
|
+
upperbounds = var.get_upperbound_values() if var.upperbounds is not None else np.full(var.shape, float("inf")).tolist()
|
|
130
|
+
|
|
131
|
+
# figure out the variable type
|
|
132
|
+
match var.variable_type:
|
|
133
|
+
case VariableTypeEnum.integer:
|
|
134
|
+
# variable is integer
|
|
135
|
+
domain = gp.GRB.INTEGER
|
|
136
|
+
case VariableTypeEnum.real:
|
|
137
|
+
# variable is real
|
|
138
|
+
domain = gp.GRB.CONTINUOUS
|
|
139
|
+
case VariableTypeEnum.binary:
|
|
140
|
+
domain = gp.GRB.BINARY
|
|
141
|
+
case _:
|
|
142
|
+
msg = f"Could not figure out the type for variable {var}."
|
|
143
|
+
raise GurobipyEvaluatorError(msg)
|
|
144
|
+
|
|
145
|
+
# add the variable to the model
|
|
146
|
+
gvar = self.model.addMVar(shape=tuple(var.shape), lb=np.array(lowerbounds), ub=np.array(upperbounds), vtype=domain, name=var.symbol)
|
|
147
|
+
# set the initial value, if one has been defined
|
|
148
|
+
if var.initial_values is not None:
|
|
149
|
+
gvar.setAttr("Start", np.array(var.get_initial_values()))
|
|
150
|
+
self.mvars[var.symbol] = gvar
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# update the model before returning, so that other expressions can reference the variables
|
|
154
|
+
self.model.update()
|
|
155
|
+
|
|
156
|
+
return self.model
|
|
157
|
+
|
|
158
|
+
def init_constants(self, problem: Problem) -> dict[str, int | float | list[int] | list[float]]:
|
|
159
|
+
"""Add constants to a GurobipyEvaluator.
|
|
160
|
+
|
|
161
|
+
Gurobi does not really have constants, so this function instead
|
|
162
|
+
stores them in a dict, that is then stored in the evaluator.
|
|
163
|
+
This is necessary to get the MathParser to understand the constants
|
|
164
|
+
used in the problem, but updating them at a later point will not update
|
|
165
|
+
any expression referencing them. The expressions that have been defined
|
|
166
|
+
using these constants will keep using the numeric value of the constant
|
|
167
|
+
at the time when the expression was created.
|
|
168
|
+
Thus, it might be best to avoid using constants if you are intending
|
|
169
|
+
to use the gurobipy solver.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
problem (Problem): problem from which to extract the constants.
|
|
173
|
+
|
|
174
|
+
Raises:
|
|
175
|
+
GurobipyEvaluatorError: when the domain of a constant cannot be figured out.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
dict[str, int | float]: a dict containing the constants.
|
|
179
|
+
"""
|
|
180
|
+
constants: dict[str, int | float | list[int] | list[float]] = {}
|
|
181
|
+
for con in problem.constants:
|
|
182
|
+
if isinstance(con, Constant):
|
|
183
|
+
constants[con.symbol] = con.value
|
|
184
|
+
elif isinstance(con, TensorConstant):
|
|
185
|
+
constants[con.symbol] = con.get_values()
|
|
186
|
+
|
|
187
|
+
return constants
|
|
188
|
+
|
|
189
|
+
def init_extras(
|
|
190
|
+
self, problem: Problem
|
|
191
|
+
) -> dict[str, gp.Var | gp.MVar | gp.LinExpr | gp.QuadExpr | gp.MLinExpr | gp.MQuadExpr | gp.GenExpr | int | float]:
|
|
192
|
+
"""Add extra function expressions to a Gurobipy Model.
|
|
193
|
+
|
|
194
|
+
Gurobi does not support extra expressions natively, so this function instead
|
|
195
|
+
stores them in a dict to be used by the evaluator.
|
|
196
|
+
This is necessary to get the MathParser to understand the extra expressions
|
|
197
|
+
used in the problem, but updating them at a later point will not update
|
|
198
|
+
any expression referencing them. The expressions that have been defined
|
|
199
|
+
using these extra expressions will keep using the value of the extra expression
|
|
200
|
+
at the time when the expression was created.
|
|
201
|
+
Thus, it might be best to avoid using extra expressions if you are intending
|
|
202
|
+
to use the gurobipy solver.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
problem (Problem): problem from which the extract the extra function expressions.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
GurobipyModel: the GurobipyModel with the expressions added as attributes.
|
|
209
|
+
"""
|
|
210
|
+
extra_functions: dict[
|
|
211
|
+
str, gp.Var | gp.MVar | gp.LinExpr | gp.QuadExpr | gp.MLinExpr | gp.MQuadExpr | gp.GenExpr | int | float
|
|
212
|
+
] = {}
|
|
213
|
+
|
|
214
|
+
for extra in problem.extra_funcs:
|
|
215
|
+
extra_functions[extra.symbol] = self.parse(extra.func, callback=self.get_expression_by_name)
|
|
216
|
+
|
|
217
|
+
return extra_functions
|
|
218
|
+
|
|
219
|
+
def init_objectives(
|
|
220
|
+
self, problem: Problem
|
|
221
|
+
) -> dict[str, gp.Var | gp.MVar | gp.LinExpr | gp.QuadExpr | gp.MLinExpr | gp.MQuadExpr]:
|
|
222
|
+
"""Add objective function expressions to a Gurobipy Model.
|
|
223
|
+
|
|
224
|
+
Does not yet add any actual gurobipy optimization objectives, only creates a dict containing the
|
|
225
|
+
expressions of the objectives. The objective expressions are stored in the
|
|
226
|
+
evaluator and appropiate gurobipy objective must be added to the model before solving.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
problem (Problem): problem from which to extract the objective function expresions.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
dict: dict containing the objective functions.
|
|
233
|
+
"""
|
|
234
|
+
objective_functions: dict[str, gp.Var | gp.MVar | gp.LinExpr | gp.QuadExpr | gp.MLinExpr | gp.MQuadExpr] = {}
|
|
235
|
+
for obj in problem.objectives:
|
|
236
|
+
gp_expr = self.parse(obj.func, callback=self.get_expression_by_name)
|
|
237
|
+
if isinstance(gp_expr, int | float):
|
|
238
|
+
warnings.warn(
|
|
239
|
+
"One or more of the problem objectives seems to be a constant.",
|
|
240
|
+
GurobipyEvaluatorWarning,
|
|
241
|
+
stacklevel=2,
|
|
242
|
+
)
|
|
243
|
+
if isinstance(gp_expr, gp.GenExpr):
|
|
244
|
+
msg = f"Gurobi does not support objective functions that are not linear or quadratic {gp_expr}"
|
|
245
|
+
raise GurobipyEvaluatorError(msg)
|
|
246
|
+
|
|
247
|
+
objective_functions[obj.symbol] = gp_expr
|
|
248
|
+
|
|
249
|
+
# the obj.symbol_min objectives are used when optimizing and building scalarizations etc...
|
|
250
|
+
objective_functions[f"{obj.symbol}_min"] = -gp_expr if obj.maximize else gp_expr
|
|
251
|
+
|
|
252
|
+
return objective_functions
|
|
253
|
+
|
|
254
|
+
def init_constraints(self, problem: Problem) -> gp.Model:
|
|
255
|
+
"""Add constraint expressions to a Gurobipy Model.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
problem (Problem): the problem from which to extract the constraint function expressions.
|
|
259
|
+
model (GurobipyModel): the GurobipyModel to add the exprssions to.
|
|
260
|
+
|
|
261
|
+
Raises:
|
|
262
|
+
GurobipyEvaluatorError: when an unsupported constraint type is encountered.
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
GurobipyModel: the GurobipyModel with the constraint expressions added.
|
|
266
|
+
"""
|
|
267
|
+
for cons in problem.constraints:
|
|
268
|
+
gp_expr = self.parse(cons.func, callback=self.get_expression_by_name)
|
|
269
|
+
|
|
270
|
+
match con_type := cons.cons_type:
|
|
271
|
+
case ConstraintTypeEnum.LTE:
|
|
272
|
+
# constraints in DESDEO are defined such that they must be less than zero
|
|
273
|
+
gp_expr = _le(gp_expr, 0)
|
|
274
|
+
case ConstraintTypeEnum.EQ:
|
|
275
|
+
gp_expr = _eq(gp_expr, 0)
|
|
276
|
+
case _:
|
|
277
|
+
msg = f"Constraint type of {con_type} not supported. Must be one of {ConstraintTypeEnum}."
|
|
278
|
+
raise GurobipyEvaluatorError(msg)
|
|
279
|
+
|
|
280
|
+
self.model.addConstr(gp_expr, name=cons.symbol)
|
|
281
|
+
|
|
282
|
+
self.model.update()
|
|
283
|
+
return self.model
|
|
284
|
+
|
|
285
|
+
def init_scalarizations(
|
|
286
|
+
self, problem: Problem
|
|
287
|
+
) -> dict[str, gp.Var | gp.MVar | gp.LinExpr | gp.QuadExpr | gp.MLinExpr | gp.MQuadExpr]:
|
|
288
|
+
"""Add scalrization expressions to a gurobipy model.
|
|
289
|
+
|
|
290
|
+
Scalarizations work identically to objectives, except they are stored in a different
|
|
291
|
+
dict in the GurobipyModel. If you want to solve the problem using a scalarization, the
|
|
292
|
+
evaluator needs to set it as an optimization target first.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
problem (Problem): the problem from which to extract the scalarization function expressions.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
dict: the dict with the scalarization expressions. Scalarization functions are always minimized.
|
|
299
|
+
"""
|
|
300
|
+
scalarizations: dict[str, gp.Var | gp.MVar | gp.LinExpr | gp.QuadExpr | gp.MLinExpr | gp.MQuadExpr] = {}
|
|
301
|
+
|
|
302
|
+
for scal in problem.scalarization_funcs:
|
|
303
|
+
scalarizations[scal.symbol] = self.parse(scal.func, self.get_expression_by_name)
|
|
304
|
+
|
|
305
|
+
return scalarizations
|
|
306
|
+
|
|
307
|
+
def add_constraint(self, constraint: Constraint) -> gp.Constr:
|
|
308
|
+
"""Add a constraint expression to a GurobipyModel.
|
|
309
|
+
|
|
310
|
+
If adding a lot of constraints, this function may end up being very slow compared
|
|
311
|
+
to adding the constraints to the stored model directly, because of the model.update() calls.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
constraint (Constraint): the constraint function expression.
|
|
315
|
+
|
|
316
|
+
Raises:
|
|
317
|
+
GurobipyEvaluatorError: when an unsupported constraint type is encountered.
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
gurobipy.Constr: The gurobipy constraint that was added.
|
|
321
|
+
"""
|
|
322
|
+
gp_expr = self.parse(constraint.func, self.get_expression_by_name)
|
|
323
|
+
|
|
324
|
+
match con_type := constraint.cons_type:
|
|
325
|
+
case ConstraintTypeEnum.LTE:
|
|
326
|
+
# constraints in DESDEO are defined such that they must be less than zero
|
|
327
|
+
gp_expr = _le(gp_expr, 0)
|
|
328
|
+
case ConstraintTypeEnum.EQ:
|
|
329
|
+
gp_expr = _eq(gp_expr, 0)
|
|
330
|
+
case _:
|
|
331
|
+
msg = f"Constraint type of {con_type} not supported. Must be one of {ConstraintTypeEnum}."
|
|
332
|
+
raise GurobipyEvaluatorError(msg)
|
|
333
|
+
|
|
334
|
+
return_cons = self.model.addConstr(gp_expr, name=constraint.symbol)
|
|
335
|
+
self.model.update()
|
|
336
|
+
return return_cons
|
|
337
|
+
|
|
338
|
+
def add_objective(self, obj: Objective):
|
|
339
|
+
"""Adds an objective function expression to a GurobipyModel.
|
|
340
|
+
|
|
341
|
+
Does not yet add any actual gurobipy optimization objectives, only adds them to the dict
|
|
342
|
+
containing the expressions of the objectives. The objective expressions are stored in the
|
|
343
|
+
GurobipyModel and the evaluator must add the appropiate gurobipy objective before solving.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
obj (Objective): the objective function expression to be added.
|
|
347
|
+
"""
|
|
348
|
+
gp_expr = self.parse(obj.func, self.get_expression_by_name)
|
|
349
|
+
if isinstance(gp_expr, int | float):
|
|
350
|
+
warnings.warn(
|
|
351
|
+
"One or more of the problem objectives seems to be a constant.", GurobipyEvaluatorWarning, stacklevel=2
|
|
352
|
+
)
|
|
353
|
+
if isinstance(gp.GenExpr, int):
|
|
354
|
+
msg = f"Gurobi does not support objective functions that are not linear or quadratic {gp_expr}"
|
|
355
|
+
raise GurobipyEvaluatorError(msg)
|
|
356
|
+
|
|
357
|
+
self.objective_functions[obj.symbol] = gp_expr
|
|
358
|
+
|
|
359
|
+
# the obj.symbol_min objectives are used when optimizing and building scalarizations etc...
|
|
360
|
+
self.objective_functions[f"{obj.symbol}_min"] = -gp_expr if obj.maximize else gp_expr
|
|
361
|
+
|
|
362
|
+
def add_scalarization_function(self, scal: ScalarizationFunction):
|
|
363
|
+
"""Adds a scalrization expression to a gurobipy model.
|
|
364
|
+
|
|
365
|
+
Scalarizations work identically to objectives, except they are stored in a different
|
|
366
|
+
dict in the GurobipyModel. If you want to solve the problem using a scalarization, the
|
|
367
|
+
evaluator needs to set it as an optimization target first.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
scal (ScalarizationFunction): The scalarization function to be added.
|
|
371
|
+
"""
|
|
372
|
+
self.scalarizations[scal.symbol] = self.parse(scal.func, self.get_expression_by_name)
|
|
373
|
+
|
|
374
|
+
def add_variable(self, var: Variable | TensorVariable) -> gp.Var | gp.MVar:
|
|
375
|
+
"""Add variables to the GurobipyModel.
|
|
376
|
+
|
|
377
|
+
If adding a lot of variables, this function may end up being very slow compared
|
|
378
|
+
to adding the variables to the stored model directly, because of the model.update() calls.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
var (Variable): The definition of the variable to be added.
|
|
382
|
+
|
|
383
|
+
Raises:
|
|
384
|
+
GurobipyEvaluatorError: when a problem in extracting the variables is encountered.
|
|
385
|
+
I.e., the variables are of a non supported type.
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
gp.Var: the variable that was added to the model.
|
|
389
|
+
"""
|
|
390
|
+
if isinstance(var, Variable):
|
|
391
|
+
# handle regular variables
|
|
392
|
+
lowerbound = var.lowerbound if var.lowerbound is not None else float("-inf")
|
|
393
|
+
upperbound = var.upperbound if var.upperbound is not None else float("inf")
|
|
394
|
+
|
|
395
|
+
# figure out the variable type
|
|
396
|
+
match var.variable_type:
|
|
397
|
+
case VariableTypeEnum.integer:
|
|
398
|
+
# variable is integer
|
|
399
|
+
domain = gp.GRB.INTEGER
|
|
400
|
+
case VariableTypeEnum.real:
|
|
401
|
+
# variable is real
|
|
402
|
+
domain = gp.GRB.CONTINUOUS
|
|
403
|
+
case VariableTypeEnum.binary:
|
|
404
|
+
domain = gp.GRB.BINARY
|
|
405
|
+
case _:
|
|
406
|
+
msg = f"Could not figure out the type for variable {var}."
|
|
407
|
+
raise GurobipyEvaluatorError(msg)
|
|
408
|
+
|
|
409
|
+
# add the variable to the model
|
|
410
|
+
gvar = self.model.addVar(lb=lowerbound, ub=upperbound, vtype=domain, name=var.symbol)
|
|
411
|
+
# set the initial value, if one has been defined
|
|
412
|
+
if var.initial_value is not None:
|
|
413
|
+
gvar.setAttr("Start", var.initial_value)
|
|
414
|
+
elif isinstance(var, TensorVariable):
|
|
415
|
+
# handle tensor variables, i.e., vectors etc..
|
|
416
|
+
lowerbounds = var.get_lowerbound_values() if var.lowerbounds is not None else np.full(var.shape, float("-inf")).tolist()
|
|
417
|
+
upperbounds = var.get_upperbound_values() if var.upperbounds is not None else np.full(var.shape, float("inf")).tolist()
|
|
418
|
+
|
|
419
|
+
# figure out the variable type
|
|
420
|
+
match var.variable_type:
|
|
421
|
+
case VariableTypeEnum.integer:
|
|
422
|
+
# variable is integer
|
|
423
|
+
domain = gp.GRB.INTEGER
|
|
424
|
+
case VariableTypeEnum.real:
|
|
425
|
+
# variable is real
|
|
426
|
+
domain = gp.GRB.CONTINUOUS
|
|
427
|
+
case VariableTypeEnum.binary:
|
|
428
|
+
domain = gp.GRB.BINARY
|
|
429
|
+
case _:
|
|
430
|
+
msg = f"Could not figure out the type for variable {var}."
|
|
431
|
+
raise GurobipyEvaluatorError(msg)
|
|
432
|
+
|
|
433
|
+
# add the variable to the model
|
|
434
|
+
gvar = self.model.addMVar(shape=tuple(var.shape), lb=np.array(lowerbounds), ub=np.array(upperbounds), vtype=domain, name=var.symbol)
|
|
435
|
+
# set the initial value, if one has been defined
|
|
436
|
+
if var.initial_values is not None:
|
|
437
|
+
gvar.setAttr("Start", np.array(var.get_initial_values()))
|
|
438
|
+
self.mvars[var.symbol] = gvar
|
|
439
|
+
|
|
440
|
+
self.model.update()
|
|
441
|
+
return gvar
|
|
442
|
+
|
|
443
|
+
def get_expression_by_name(
|
|
444
|
+
self, name: str
|
|
445
|
+
) -> gp.Var | gp.MVar | gp.LinExpr | gp.QuadExpr | gp.MLinExpr | gp.MQuadExpr | gp.GenExpr | int | float:
|
|
446
|
+
"""Returns a gurobipy expression corresponding to the name.
|
|
447
|
+
|
|
448
|
+
Only looks for variables, objective functions, scalarizations, extra functions, and constants.
|
|
449
|
+
This will not find constraints.
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
name (str): The symbol of the expression.
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
gurobipy expression: A mathematical expression that gp.Model can use either as a constraint or an objective
|
|
456
|
+
"""
|
|
457
|
+
expression = self.model.getVarByName(name)
|
|
458
|
+
if expression is None:
|
|
459
|
+
# check if an MVar by checking gurobi.MVars stored in the evaluator directly,
|
|
460
|
+
# which results in terms multiplied by zero being removed from the equations:
|
|
461
|
+
if name in self.mvars:
|
|
462
|
+
expression = self.mvars[name]
|
|
463
|
+
elif name in self.objective_functions:
|
|
464
|
+
expression = self.objective_functions[name]
|
|
465
|
+
elif name in self.scalarizations:
|
|
466
|
+
expression = self.scalarizations[name]
|
|
467
|
+
elif name in self.extra_functions:
|
|
468
|
+
expression = self.extra_functions[name]
|
|
469
|
+
elif name in self.constants:
|
|
470
|
+
expression = self.constants[name]
|
|
471
|
+
return expression
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def get_values(self) -> dict[str, float | int | bool | list[float] | list[int]]: # noqa: C901
|
|
475
|
+
"""Get the values from the Gurobipy Model in a dict.
|
|
476
|
+
|
|
477
|
+
The keys of the dict will be the symbols defined in the problem utilized to initialize the evaluator.
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
dict[str, float | int | bool]: a dict with keys equivalent to the symbols defined in self.problem.
|
|
481
|
+
"""
|
|
482
|
+
result_dict = {}
|
|
483
|
+
|
|
484
|
+
for var in self.problem.variables:
|
|
485
|
+
# if var is type MVar, get the values of MVar
|
|
486
|
+
if var.symbol in self.mvars:
|
|
487
|
+
result_dict[var.symbol] = self.mvars[var.symbol].getAttr(gp.GRB.Attr.X)
|
|
488
|
+
else:
|
|
489
|
+
result_dict[var.symbol] = self.model.getVarByName(var.symbol).getAttr(gp.GRB.Attr.X)
|
|
490
|
+
|
|
491
|
+
for obj in self.problem.objectives:
|
|
492
|
+
result_dict[obj.symbol] = self.objective_functions[obj.symbol].getValue()
|
|
493
|
+
|
|
494
|
+
if self.problem.constants is not None:
|
|
495
|
+
for con in self.problem.constants:
|
|
496
|
+
result_dict[con.symbol] = self.constants[con.symbol]
|
|
497
|
+
|
|
498
|
+
if self.problem.extra_funcs is not None:
|
|
499
|
+
for extra in self.problem.extra_funcs:
|
|
500
|
+
result_dict[extra.symbol] = self.extra_functions[extra.symbol].getValue()
|
|
501
|
+
|
|
502
|
+
if self.problem.constraints is not None:
|
|
503
|
+
for const in self.problem.constraints:
|
|
504
|
+
result_dict[const.symbol] = -self.model.getConstrByName(const.symbol).getAttr("Slack")
|
|
505
|
+
|
|
506
|
+
if self.problem.scalarization_funcs is not None:
|
|
507
|
+
for scal in self.problem.scalarization_funcs:
|
|
508
|
+
result_dict[scal.symbol] = self.scalarizations[scal.symbol].getValue()
|
|
509
|
+
|
|
510
|
+
return result_dict
|
|
511
|
+
|
|
512
|
+
def remove_constraint(self, symbol: str):
|
|
513
|
+
"""Removes a constraint from the model.
|
|
514
|
+
|
|
515
|
+
If removing a lot of constraints or dealing with a very large model this function
|
|
516
|
+
may be slow because of the model.update() calls. Accessing the stored model directly
|
|
517
|
+
may be faster.
|
|
518
|
+
|
|
519
|
+
Args:
|
|
520
|
+
symbol (str): a str representing the symbol of the constraint to be removed.
|
|
521
|
+
"""
|
|
522
|
+
self.model.remove(self.model.getConstrByName(symbol))
|
|
523
|
+
self.model.update()
|
|
524
|
+
|
|
525
|
+
def remove_variable(self, symbol: str):
|
|
526
|
+
"""Removes a variable from the model.
|
|
527
|
+
|
|
528
|
+
If removing a lot of variables or dealing with a very large model this function
|
|
529
|
+
may be slow because of the model.update() calls. Accessing the stored model directly
|
|
530
|
+
may be faster.
|
|
531
|
+
|
|
532
|
+
Args:
|
|
533
|
+
symbol (str): a str representing the symbol of the variable to be removed.
|
|
534
|
+
"""
|
|
535
|
+
if symbol in self.mvars:
|
|
536
|
+
self.model.remove(self.mvars[symbol])
|
|
537
|
+
self.mvars.pop(symbol)
|
|
538
|
+
else:
|
|
539
|
+
self.model.remove(self.model.getVarByName(symbol))
|
|
540
|
+
self.model.update()
|
|
541
|
+
|
|
542
|
+
def set_optimization_target(self, target: str, maximize: bool = False): # noqa: FBT001, FBT002
|
|
543
|
+
"""Sets a minimization objective to match the target objective or scalarization of the gurobipy model.
|
|
544
|
+
|
|
545
|
+
Args:
|
|
546
|
+
target (str): an str representing a symbol. Needs to match an objective function or scaralization
|
|
547
|
+
function already found in the model.
|
|
548
|
+
maximize (bool): If true, the target function is maximized instead of minimized
|
|
549
|
+
|
|
550
|
+
Raises:
|
|
551
|
+
GurobipyEvaluatorError: the given target was not an attribute of the gurobipy model.
|
|
552
|
+
"""
|
|
553
|
+
if not ((target in self.objective_functions) or (target in self.scalarizations)):
|
|
554
|
+
msg = f"The gurobipy model has no objective or scalarization named {target}."
|
|
555
|
+
raise GurobipyEvaluatorError(msg)
|
|
556
|
+
|
|
557
|
+
obj_expr = self.get_expression_by_name(target)
|
|
558
|
+
|
|
559
|
+
if maximize:
|
|
560
|
+
self.model.setObjective(obj_expr, sense=gp.GRB.MAXIMIZE)
|
|
561
|
+
else:
|
|
562
|
+
self.model.setObjective(obj_expr)
|