desdeo 1.1.3__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.
Files changed (122) hide show
  1. desdeo/__init__.py +8 -8
  2. desdeo/api/README.md +73 -0
  3. desdeo/api/__init__.py +15 -0
  4. desdeo/api/app.py +40 -0
  5. desdeo/api/config.py +69 -0
  6. desdeo/api/config.toml +53 -0
  7. desdeo/api/db.py +25 -0
  8. desdeo/api/db_init.py +79 -0
  9. desdeo/api/db_models.py +164 -0
  10. desdeo/api/malaga_db_init.py +27 -0
  11. desdeo/api/models/__init__.py +66 -0
  12. desdeo/api/models/archive.py +34 -0
  13. desdeo/api/models/preference.py +90 -0
  14. desdeo/api/models/problem.py +507 -0
  15. desdeo/api/models/reference_point_method.py +18 -0
  16. desdeo/api/models/session.py +46 -0
  17. desdeo/api/models/state.py +96 -0
  18. desdeo/api/models/user.py +51 -0
  19. desdeo/api/routers/_NAUTILUS.py +245 -0
  20. desdeo/api/routers/_NAUTILUS_navigator.py +233 -0
  21. desdeo/api/routers/_NIMBUS.py +762 -0
  22. desdeo/api/routers/__init__.py +5 -0
  23. desdeo/api/routers/problem.py +110 -0
  24. desdeo/api/routers/reference_point_method.py +117 -0
  25. desdeo/api/routers/session.py +76 -0
  26. desdeo/api/routers/test.py +16 -0
  27. desdeo/api/routers/user_authentication.py +366 -0
  28. desdeo/api/schema.py +94 -0
  29. desdeo/api/tests/__init__.py +0 -0
  30. desdeo/api/tests/conftest.py +59 -0
  31. desdeo/api/tests/test_models.py +701 -0
  32. desdeo/api/tests/test_routes.py +216 -0
  33. desdeo/api/utils/database.py +274 -0
  34. desdeo/api/utils/logger.py +29 -0
  35. desdeo/core.py +27 -0
  36. desdeo/emo/__init__.py +29 -0
  37. desdeo/emo/hooks/archivers.py +172 -0
  38. desdeo/emo/methods/EAs.py +418 -0
  39. desdeo/emo/methods/__init__.py +0 -0
  40. desdeo/emo/methods/bases.py +59 -0
  41. desdeo/emo/operators/__init__.py +1 -0
  42. desdeo/emo/operators/crossover.py +780 -0
  43. desdeo/emo/operators/evaluator.py +118 -0
  44. desdeo/emo/operators/generator.py +356 -0
  45. desdeo/emo/operators/mutation.py +1053 -0
  46. desdeo/emo/operators/selection.py +1036 -0
  47. desdeo/emo/operators/termination.py +178 -0
  48. desdeo/explanations/__init__.py +6 -0
  49. desdeo/explanations/explainer.py +100 -0
  50. desdeo/explanations/utils.py +90 -0
  51. desdeo/mcdm/__init__.py +19 -0
  52. desdeo/mcdm/nautili.py +345 -0
  53. desdeo/mcdm/nautilus.py +477 -0
  54. desdeo/mcdm/nautilus_navigator.py +655 -0
  55. desdeo/mcdm/nimbus.py +417 -0
  56. desdeo/mcdm/pareto_navigator.py +269 -0
  57. desdeo/mcdm/reference_point_method.py +116 -0
  58. desdeo/problem/__init__.py +79 -0
  59. desdeo/problem/evaluator.py +561 -0
  60. desdeo/problem/gurobipy_evaluator.py +562 -0
  61. desdeo/problem/infix_parser.py +341 -0
  62. desdeo/problem/json_parser.py +944 -0
  63. desdeo/problem/pyomo_evaluator.py +468 -0
  64. desdeo/problem/schema.py +1808 -0
  65. desdeo/problem/simulator_evaluator.py +298 -0
  66. desdeo/problem/sympy_evaluator.py +244 -0
  67. desdeo/problem/testproblems/__init__.py +73 -0
  68. desdeo/problem/testproblems/binh_and_korn_problem.py +88 -0
  69. desdeo/problem/testproblems/dtlz2_problem.py +102 -0
  70. desdeo/problem/testproblems/forest_problem.py +275 -0
  71. desdeo/problem/testproblems/knapsack_problem.py +163 -0
  72. desdeo/problem/testproblems/mcwb_problem.py +831 -0
  73. desdeo/problem/testproblems/mixed_variable_dimenrions_problem.py +83 -0
  74. desdeo/problem/testproblems/momip_problem.py +172 -0
  75. desdeo/problem/testproblems/nimbus_problem.py +143 -0
  76. desdeo/problem/testproblems/pareto_navigator_problem.py +89 -0
  77. desdeo/problem/testproblems/re_problem.py +492 -0
  78. desdeo/problem/testproblems/river_pollution_problem.py +434 -0
  79. desdeo/problem/testproblems/rocket_injector_design_problem.py +140 -0
  80. desdeo/problem/testproblems/simple_problem.py +351 -0
  81. desdeo/problem/testproblems/simulator_problem.py +92 -0
  82. desdeo/problem/testproblems/spanish_sustainability_problem.py +945 -0
  83. desdeo/problem/testproblems/zdt_problem.py +271 -0
  84. desdeo/problem/utils.py +245 -0
  85. desdeo/tools/GenerateReferencePoints.py +181 -0
  86. desdeo/tools/__init__.py +102 -0
  87. desdeo/tools/generics.py +145 -0
  88. desdeo/tools/gurobipy_solver_interfaces.py +258 -0
  89. desdeo/tools/indicators_binary.py +11 -0
  90. desdeo/tools/indicators_unary.py +375 -0
  91. desdeo/tools/interaction_schema.py +38 -0
  92. desdeo/tools/intersection.py +54 -0
  93. desdeo/tools/iterative_pareto_representer.py +99 -0
  94. desdeo/tools/message.py +234 -0
  95. desdeo/tools/ng_solver_interfaces.py +199 -0
  96. desdeo/tools/non_dominated_sorting.py +133 -0
  97. desdeo/tools/patterns.py +281 -0
  98. desdeo/tools/proximal_solver.py +99 -0
  99. desdeo/tools/pyomo_solver_interfaces.py +464 -0
  100. desdeo/tools/reference_vectors.py +462 -0
  101. desdeo/tools/scalarization.py +3138 -0
  102. desdeo/tools/scipy_solver_interfaces.py +454 -0
  103. desdeo/tools/score_bands.py +464 -0
  104. desdeo/tools/utils.py +320 -0
  105. desdeo/utopia_stuff/__init__.py +0 -0
  106. desdeo/utopia_stuff/data/1.json +15 -0
  107. desdeo/utopia_stuff/data/2.json +13 -0
  108. desdeo/utopia_stuff/data/3.json +15 -0
  109. desdeo/utopia_stuff/data/4.json +17 -0
  110. desdeo/utopia_stuff/data/5.json +15 -0
  111. desdeo/utopia_stuff/from_json.py +40 -0
  112. desdeo/utopia_stuff/reinit_user.py +38 -0
  113. desdeo/utopia_stuff/utopia_db_init.py +212 -0
  114. desdeo/utopia_stuff/utopia_problem.py +403 -0
  115. desdeo/utopia_stuff/utopia_problem_old.py +415 -0
  116. desdeo/utopia_stuff/utopia_reference_solutions.py +79 -0
  117. desdeo-2.0.0.dist-info/LICENSE +21 -0
  118. desdeo-2.0.0.dist-info/METADATA +168 -0
  119. desdeo-2.0.0.dist-info/RECORD +120 -0
  120. {desdeo-1.1.3.dist-info → desdeo-2.0.0.dist-info}/WHEEL +1 -1
  121. desdeo-1.1.3.dist-info/METADATA +0 -18
  122. desdeo-1.1.3.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)