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.
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.2.dist-info → desdeo-2.0.0.dist-info}/WHEEL +1 -1
  121. desdeo-1.2.dist-info/METADATA +0 -16
  122. desdeo-1.2.dist-info/RECORD +0 -4
@@ -0,0 +1,3138 @@
1
+ """Defines various functions for scalarizing multiobjective optimization problems.
2
+
3
+ Note that when scalarization functions are defined, they must add the post-fix
4
+ '_min' to any symbol representing objective functions so that the maximization
5
+ or minimization of the corresponding objective functions may be correctly
6
+ accounted for when computing scalarization function values.
7
+ """
8
+
9
+ import numpy as np
10
+
11
+ from desdeo.problem import (
12
+ Constraint,
13
+ ConstraintTypeEnum,
14
+ Problem,
15
+ ScalarizationFunction,
16
+ Variable,
17
+ VariableTypeEnum,
18
+ )
19
+ from desdeo.tools.utils import (
20
+ get_corrected_ideal,
21
+ get_corrected_nadir,
22
+ get_corrected_reference_point,
23
+ )
24
+
25
+
26
+ class ScalarizationError(Exception):
27
+ """Raised when issues with creating or adding scalarization functions are encountered."""
28
+
29
+
30
+ class Op:
31
+ """Defines the supported operators in the MathJSON format."""
32
+
33
+ # TODO: move this to problem/schema.py, make it use this, and import it here from there
34
+ # Basic arithmetic operators
35
+ NEGATE = "Negate"
36
+ ADD = "Add"
37
+ SUB = "Subtract"
38
+ MUL = "Multiply"
39
+ DIV = "Divide"
40
+
41
+ # Exponentation and logarithms
42
+ EXP = "Exp"
43
+ LN = "Ln"
44
+ LB = "Lb"
45
+ LG = "Lg"
46
+ LOP = "LogOnePlus"
47
+ SQRT = "Sqrt"
48
+ SQUARE = "Square"
49
+ POW = "Power"
50
+
51
+ # Rounding operators
52
+ ABS = "Abs"
53
+ CEIL = "Ceil"
54
+ FLOOR = "Floor"
55
+
56
+ # Trigonometric operations
57
+ ARCCOS = "Arccos"
58
+ ARCCOSH = "Arccosh"
59
+ ARCSIN = "Arcsin"
60
+ ARCSINH = "Arcsinh"
61
+ ARCTAN = "Arctan"
62
+ ARCTANH = "Arctanh"
63
+ COS = "Cos"
64
+ COSH = "Cosh"
65
+ SIN = "Sin"
66
+ SINH = "Sinh"
67
+ TAN = "Tan"
68
+ TANH = "Tanh"
69
+
70
+ # Comparison operators
71
+ EQUAL = "Equal"
72
+ GREATER = "Greater"
73
+ GREATER_EQUAL = "GreaterEqual"
74
+ LESS = "Less"
75
+ LESS_EQUAL = "LessEqual"
76
+ NOT_EQUAL = "NotEqual"
77
+
78
+ # Other operators
79
+ MAX = "Max"
80
+ RATIONAL = "Rational"
81
+
82
+
83
+ def objective_dict_has_all_symbols(problem: Problem, obj_dict: dict[str, float]) -> bool:
84
+ """Check that a dict has all the objective function symbols of a problem as its keys.
85
+
86
+ Args:
87
+ problem (Problem): the problem with the objective symbols.
88
+ obj_dict (dict[str, float]): a dict that should have a key for each objective symbol.
89
+
90
+ Returns:
91
+ bool: whether all the symbols are present or not.
92
+ """
93
+ return all(obj.symbol in obj_dict for obj in problem.objectives)
94
+
95
+
96
+ def add_asf_nondiff( # noqa: PLR0913
97
+ problem: Problem,
98
+ symbol: str,
99
+ reference_point: dict[str, float],
100
+ ideal: dict[str, float] | None = None,
101
+ nadir: dict[str, float] | None = None,
102
+ delta: float = 0.000001,
103
+ rho: float = 0.000001,
104
+ *,
105
+ reference_in_aug=False,
106
+ ) -> tuple[Problem, str]:
107
+ r"""Add the achievement scalarizing function for a problem with the reference point.
108
+
109
+ This is the non-differentiable variant of the achievement scalarizing function, which
110
+ means the resulting scalarization function is non-differentiable.
111
+ Requires that the ideal and nadir point have been defined for the problem.
112
+
113
+ The scalarization is defined as follows:
114
+
115
+ \begin{equation}
116
+ \mathcal{S}_\text{ASF}(F(\mathbf{x}); \mathbf{q}, \mathbf{z}^\star, \mathbf{z}^\text{nad}) =
117
+ \underset{i=1,\ldots,k}{\text{max}}
118
+ \left[
119
+ \frac{f_i(\mathbf{x}) - q_i}{z^\text{nad}_i - (z_i^\star - \delta)}
120
+ \right]
121
+ + \rho\sum_{i=1}^{k} \frac{f_i(\mathbf{x})}{z_i^\text{nad} - (z_i^\star - \delta)},
122
+ \end{equation}
123
+
124
+ where $\mathbf{q} = [q_1,\dots,q_k]$ is a reference point, $\mathbf{z^\star} = [z_1^\star,\dots,z_k^\star]$
125
+ is the ideal point, $\mathbf{z}^\text{nad} = [z_1^\text{nad},\dots,z_k^\text{nad}]$ is the nadir point, $k$
126
+ is the number of objective functions, and $\delta$ and $\rho$ are small scalar values. The summation term
127
+ in the scalarization is known as the _augmentation term_. If the reference point is chosen to
128
+ be used in the augmentation term (`reference_in_aug=True`), then
129
+ the reference point components are subtracted from the objective function values in the nominator
130
+ of the augmentation term. That is:
131
+
132
+ \begin{equation}
133
+ \mathcal{S}_\text{ASF}(F(\mathbf{x}); \mathbf{q}, \mathbf{z}^\star, \mathbf{z}^\text{nad}) =
134
+ \underset{i=1,\ldots,k}{\text{max}}
135
+ \left[
136
+ \frac{f_i(\mathbf{x}) - q_i}{z^\text{nad}_i - (z_i^\star - \delta)}
137
+ \right]
138
+ + \rho\sum_{i=1}^{k} \frac{f_i(\mathbf{x}) - q_i}{z_i^\text{nad} - (z_i^\star - \delta)}.
139
+ \end{equation}
140
+
141
+ Args:
142
+ problem (Problem): the problem to which the scalarization function should be added.
143
+ symbol (str): the symbol to reference the added scalarization function.
144
+ reference_point (dict[str, float]): a reference point as an objective dict.
145
+ ideal (dict[str, float], optional): ideal point values. If not given, attempt will be made
146
+ to calculate ideal point from problem.
147
+ nadir (dict[str, float], optional): nadir point values. If not given, attempt will be made
148
+ to calculate nadir point from problem.
149
+ delta (float, optional): the scalar value used to define the utopian point (ideal - delta).
150
+ Defaults to 0.000001.
151
+ rho (float, optional): the weight factor used in the augmentation term. Defaults to 0.000001.
152
+ reference_in_aug (bool): whether the reference point should be used in
153
+ the augmentation term as well. Defaults to False.
154
+
155
+ Raises:
156
+ ScalarizationError: there are missing elements in the reference point, or if any of the ideal or nadir
157
+ point values are undefined (None).
158
+
159
+ Returns:
160
+ tuple[Problem, str]: A tuple containing a copy of the problem with the scalarization function added,
161
+ and the symbol of the added scalarization function.
162
+ """
163
+ # check that the reference point has all the objective components
164
+ if not objective_dict_has_all_symbols(problem, reference_point):
165
+ msg = f"The given reference point {reference_point} does not have a component defined for all the objectives."
166
+ raise ScalarizationError(msg)
167
+
168
+ # check if ideal point is specified
169
+ # if not specified, try to calculate corrected ideal point
170
+ if ideal is not None:
171
+ ideal_point = ideal
172
+ elif problem.get_ideal_point() is not None:
173
+ ideal_point = get_corrected_ideal(problem)
174
+ else:
175
+ msg = "Ideal point not defined!"
176
+ raise ScalarizationError(msg)
177
+
178
+ # check if nadir point is specified
179
+ # if not specified, try to calculate corrected nadir point
180
+ if nadir is not None:
181
+ nadir_point = nadir
182
+ elif problem.get_nadir_point() is not None:
183
+ nadir_point = get_corrected_nadir(problem)
184
+ else:
185
+ msg = "Nadir point not defined!"
186
+ raise ScalarizationError(msg)
187
+
188
+ if any(value is None for value in ideal_point.values()) or any(value is None for value in nadir_point.values()):
189
+ msg = f"There are undefined values in either the ideal ({ideal_point}) or the nadir point ({nadir_point})."
190
+ raise ScalarizationError(msg)
191
+
192
+ # Build the max term
193
+ max_operands = [
194
+ (
195
+ f"({obj.symbol}_min - {reference_point[obj.symbol]}{" * -1" if obj.maximize else ''}) "
196
+ f"/ ({nadir_point[obj.symbol]} - ({ideal_point[obj.symbol]} - {delta}))"
197
+ )
198
+ for obj in problem.objectives
199
+ ]
200
+ max_term = f"{Op.MAX}({', '.join(max_operands)})"
201
+
202
+ # Build the augmentation term
203
+ if not reference_in_aug:
204
+ aug_operands = [
205
+ f"{obj.symbol}_min / ({nadir_point[obj.symbol]} - ({ideal_point[obj.symbol]} - {delta}))"
206
+ for obj in problem.objectives
207
+ ]
208
+ else:
209
+ aug_operands = [
210
+ (
211
+ f"({obj.symbol}_min - {reference_point[obj.symbol]}{" * -1" if obj.maximize else 1}) "
212
+ f"/ ({nadir_point[obj.symbol]} - ({ideal_point[obj.symbol]} - {delta}))"
213
+ )
214
+ for obj in problem.objectives
215
+ ]
216
+
217
+ aug_term = " + ".join(aug_operands)
218
+
219
+ asf_function = f"{max_term} + {rho} * ({aug_term})"
220
+
221
+ # Add the function to the problem
222
+ scalarization_function = ScalarizationFunction(
223
+ name="Achievement scalarizing function",
224
+ symbol=symbol,
225
+ func=asf_function,
226
+ is_linear=False,
227
+ is_convex=False,
228
+ is_twice_differentiable=False,
229
+ )
230
+ return problem.add_scalarization(scalarization_function), symbol
231
+
232
+
233
+ def add_group_asf(
234
+ problem: Problem,
235
+ symbol: str,
236
+ reference_points: list[dict[str, float]],
237
+ ideal: dict[str, float] | None = None,
238
+ nadir: dict[str, float] | None = None,
239
+ delta: float = 1e-6,
240
+ rho: float = 1e-6,
241
+ ) -> tuple[Problem, str]:
242
+ r"""Add the achievement scalarizing function for multiple decision makers.
243
+
244
+ The scalarization function is defined as follows:
245
+
246
+ \begin{align}
247
+ &\mbox{minimize} &&\max_{i,d} [w_{id}(f_{id}(\mathbf{x})-\overline{z}_{id})] +
248
+ \rho \sum^k_{i=1} \sum^{n_d}_{d=1} w_{id}f_{id}(\mathbf{x}) \\
249
+ &\mbox{subject to} &&\mathbf{x} \in \mathbf{X},
250
+ \end{align}
251
+
252
+ where $w_{id} = \frac{1}{z^{nad}_{id} - z^{uto}_{id}}$.
253
+
254
+ Args:
255
+ problem (Problem): the problem to which the scalarization function should be added.
256
+ symbol (str): the symbol to reference the added scalarization function.
257
+ reference_points (list[dict[str, float]]): a list of reference points as objective dicts.
258
+ ideal (dict[str, float], optional): ideal point values. If not given, attempt will be made
259
+ to calculate ideal point from problem.
260
+ nadir (dict[str, float], optional): nadir point values. If not given, attempt will be made
261
+ to calculate nadir point from problem.
262
+ delta (float, optional): a small scalar used to define the utopian point. Defaults to 1e-6.
263
+ rho (float, optional): the weight factor used in the augmentation term. Defaults to 1e-6.
264
+
265
+ Raises:
266
+ ScalarizationError: there are missing elements in any reference point.
267
+
268
+ Returns:
269
+ tuple[Problem, str]: A tuple containing a copy of the problem with the scalarization function added,
270
+ and the symbol of the added scalarization function.
271
+ """
272
+ # check reference points
273
+ for reference_point in reference_points:
274
+ if not objective_dict_has_all_symbols(problem, reference_point):
275
+ msg = f"The give reference point {reference_point} is missing a value for one or more objectives."
276
+ raise ScalarizationError(msg)
277
+
278
+ # check if ideal point is specified
279
+ # if not specified, try to calculate corrected ideal point
280
+ if ideal is not None:
281
+ ideal_point = ideal
282
+ elif problem.get_ideal_point() is not None:
283
+ ideal_point = get_corrected_ideal(problem)
284
+ else:
285
+ msg = "Ideal point not defined!"
286
+ raise ScalarizationError(msg)
287
+
288
+ # check if nadir point is specified
289
+ # if not specified, try to calculate corrected nadir point
290
+ if nadir is not None:
291
+ nadir_point = nadir
292
+ elif problem.get_nadir_point() is not None:
293
+ nadir_point = get_corrected_nadir(problem)
294
+ else:
295
+ msg = "Nadir point not defined!"
296
+ raise ScalarizationError(msg)
297
+
298
+ # calculate the weights
299
+ weights = {
300
+ obj.symbol: 1 / (nadir_point[obj.symbol] - (ideal_point[obj.symbol] - delta)) for obj in problem.objectives
301
+ }
302
+
303
+ # form the max and augmentation terms
304
+ max_terms = []
305
+ aug_exprs = []
306
+ for i in range(len(reference_points)):
307
+ corrected_rp = get_corrected_reference_point(problem, reference_points[i])
308
+ for obj in problem.objectives:
309
+ max_terms.append(f"({weights[obj.symbol]}) * ({obj.symbol}_min - {corrected_rp[obj.symbol]})")
310
+
311
+ aug_expr = " + ".join([f"({weights[obj.symbol]} * {obj.symbol}_min)" for obj in problem.objectives])
312
+ aug_exprs.append(aug_expr)
313
+ max_terms = ", ".join(max_terms)
314
+ aug_exprs = " + ".join(aug_exprs)
315
+
316
+ func = f"{Op.MAX}({max_terms}) + {rho} * ({aug_exprs})"
317
+
318
+ scalarization_function = ScalarizationFunction(
319
+ name="Achievement scalarizing function for multiple decision makers",
320
+ symbol=symbol,
321
+ func=func,
322
+ is_convex=problem.is_convex,
323
+ is_linear=problem.is_linear,
324
+ is_twice_differentiable=False,
325
+ )
326
+ return problem.add_scalarization(scalarization_function), symbol
327
+
328
+
329
+ def add_group_asf_diff(
330
+ problem: Problem,
331
+ symbol: str,
332
+ reference_points: list[dict[str, float]],
333
+ ideal: dict[str, float] | None = None,
334
+ nadir: dict[str, float] | None = None,
335
+ delta: float = 1e-6,
336
+ rho: float = 1e-6,
337
+ ) -> tuple[Problem, str]:
338
+ r"""Add the differentiable variant of the achievement scalarizing function for multiple decision makers.
339
+
340
+ The scalarization function is defined as follows:
341
+
342
+ \begin{align}
343
+ &\mbox{minimize} &&\alpha +
344
+ \rho \sum^k_{i=1} \sum^{n_d}_{d=1} w_{id}f_{id}(\mathbf{x}) \\
345
+ &\mbox{subject to} && w_{id}(f_{id}(\mathbf{x})-\overline{z}_{id}) - \alpha \leq 0,\\
346
+ &&&\mathbf{x} \in \mathbf{X},
347
+ \end{align}
348
+
349
+ where $w_{id} = \frac{1}{z^{nad}_{id} - z^{uto}_{id}}$.
350
+
351
+ Args:
352
+ problem (Problem): the problem to which the scalarization function should be added.
353
+ symbol (str): the symbol to reference the added scalarization function.
354
+ reference_points (list[dict[str, float]]): a list of reference points as objective dicts.
355
+ ideal (dict[str, float], optional): ideal point values. If not given, attempt will be made
356
+ to calculate ideal point from problem.
357
+ nadir (dict[str, float], optional): nadir point values. If not given, attempt will be made
358
+ to calculate nadir point from problem.
359
+ delta (float, optional): a small scalar used to define the utopian point. Defaults to 1e-6.
360
+ rho (float, optional): the weight factor used in the augmentation term. Defaults to 1e-6.
361
+
362
+ Raises:
363
+ ScalarizationError: there are missing elements in any reference point.
364
+
365
+ Returns:
366
+ tuple[Problem, str]: A tuple containing a copy of the problem with the scalarization function added,
367
+ and the symbol of the added scalarization function.
368
+ """
369
+ # check reference points
370
+ for reference_point in reference_points:
371
+ if not objective_dict_has_all_symbols(problem, reference_point):
372
+ msg = f"The give reference point {reference_point} is missing a value for one or more objectives."
373
+ raise ScalarizationError(msg)
374
+
375
+ # check if ideal point is specified
376
+ # if not specified, try to calculate corrected ideal point
377
+ if ideal is not None:
378
+ ideal_point = ideal
379
+ elif problem.get_ideal_point() is not None:
380
+ ideal_point = get_corrected_ideal(problem)
381
+ else:
382
+ msg = "Ideal point not defined!"
383
+ raise ScalarizationError(msg)
384
+
385
+ # check if nadir point is specified
386
+ # if not specified, try to calculate corrected nadir point
387
+ if nadir is not None:
388
+ nadir_point = nadir
389
+ elif problem.get_nadir_point() is not None:
390
+ nadir_point = get_corrected_nadir(problem)
391
+ else:
392
+ msg = "Nadir point not defined!"
393
+ raise ScalarizationError(msg)
394
+
395
+ # define the auxiliary variable
396
+ alpha = Variable(
397
+ name="alpha",
398
+ symbol="_alpha",
399
+ variable_type=VariableTypeEnum.real,
400
+ lowerbound=-float("Inf"),
401
+ upperbound=float("Inf"),
402
+ initial_value=1.0,
403
+ )
404
+
405
+ # calculate the weights
406
+ weights = {
407
+ obj.symbol: 1 / (nadir_point[obj.symbol] - (ideal_point[obj.symbol] - delta)) for obj in problem.objectives
408
+ }
409
+
410
+ # form the constaint and augmentation expressions
411
+ # constraint expressions are formed into a list of lists
412
+ con_terms = []
413
+ aug_exprs = []
414
+ for i in range(len(reference_points)):
415
+ corrected_rp = get_corrected_reference_point(problem, reference_points[i])
416
+ rp = {}
417
+ for obj in problem.objectives:
418
+ rp[obj.symbol] = f"(({weights[obj.symbol]}) * ({obj.symbol}_min - {corrected_rp[obj.symbol]})) - _alpha"
419
+ con_terms.append(rp)
420
+ aug_expr = " + ".join([f"({weights[obj.symbol]} * {obj.symbol}_min)" for obj in problem.objectives])
421
+ aug_exprs.append(aug_expr)
422
+ aug_exprs = " + ".join(aug_exprs)
423
+
424
+ func = f"_alpha + {rho} * ({aug_exprs})"
425
+
426
+ scalarization_function = ScalarizationFunction(
427
+ name="Differentiable achievement scalarizing function for multiple decision makers",
428
+ symbol=symbol,
429
+ func=func,
430
+ is_convex=problem.is_convex,
431
+ is_linear=problem.is_linear,
432
+ is_twice_differentiable=problem.is_twice_differentiable,
433
+ )
434
+
435
+ constraints = []
436
+ # loop to create a constraint for every objective of every reference point given
437
+ for i in range(len(reference_points)):
438
+ for obj in problem.objectives:
439
+ # since we are subtracting a constant value, the linearity, convexity,
440
+ # and differentiability of the objective function, and hence the
441
+ # constraint, should not change.
442
+ constraints.append(
443
+ Constraint(
444
+ name=f"Constraint for {obj.symbol}",
445
+ symbol=f"{obj.symbol}_con_{i+1}",
446
+ func=con_terms[i][obj.symbol],
447
+ cons_type=ConstraintTypeEnum.LTE,
448
+ is_linear=obj.is_linear,
449
+ is_convex=obj.is_convex,
450
+ is_twice_differentiable=obj.is_twice_differentiable,
451
+ )
452
+ )
453
+ _problem = problem.add_variables([alpha])
454
+ _problem = _problem.add_scalarization(scalarization_function)
455
+ return _problem.add_constraints(constraints), symbol
456
+
457
+
458
+ def add_asf_generic_diff( # noqa: PLR0913
459
+ problem: Problem,
460
+ symbol: str,
461
+ reference_point: dict[str, float],
462
+ weights: dict[str, float],
463
+ reference_point_aug: dict[str, float] | None = None,
464
+ weights_aug: dict[str, float] | None = None,
465
+ rho: float = 1e-6,
466
+ ) -> tuple[Problem, str]:
467
+ r"""Adds the differentiable variant of the generic achievement scalarizing function.
468
+
469
+ \begin{align*}
470
+ \min \quad & \alpha + \rho \sum_{i=1}^k \frac{f_i(\mathbf{x})}{w_i} \\
471
+ \text{s.t.} \quad & \frac{f_i(\mathbf{x}) - q_i}{w_i} - \alpha \leq 0,\\
472
+ & \mathbf{x} \in S,
473
+ \end{align*}
474
+
475
+ where $f_i$ are objective functions, $q_i$ is a component of the reference point,
476
+ and $w_i$ are components of the weight vector (which are assumed to be positive),
477
+ $\rho$ and $\delta$ are small scalar values, $S$ is the feasible solution
478
+ space of the original problem, and $\alpha$ is an auxiliary variable.
479
+ The summation term in the scalarization is known as the _augmentation term_.
480
+ If a reference point is chosen to be used in the augmentation term, e.g., a separate
481
+ reference point for the augmentation term is given (`reference_point_aug`), then
482
+ the reference point components are subtracted from the objective function values
483
+ in the nominator of the augmentation term. That is:
484
+
485
+ \begin{align*}
486
+ \min \quad & \alpha + \rho \sum_{i=1}^k \frac{f_i(\mathbf{x}) - q_i}{w_i} \\
487
+ \text{s.t.} \quad & \frac{f_i(\mathbf{x}) - q_i}{w_i} - \alpha \leq 0,\\
488
+ & \mathbf{x} \in S,
489
+ \end{align*}
490
+
491
+ References:
492
+ Wierzbicki, A. P. (1982). A mathematical basis for satisficing decision
493
+ making. Mathematical modelling, 3(5), 391-405.
494
+
495
+ Args:
496
+ problem (Problem): the problem the scalarization is added to.
497
+ symbol (str): the symbol given to the added scalarization.
498
+ reference_point (dict[str, float]): a dict with keys corresponding to objective
499
+ function symbols and values to reference point components, i.e.,
500
+ aspiration levels.
501
+ weights (dict[str, float]): the weights to be used in the scalarization function. Must be positive.
502
+ reference_point_aug (dict[str, float], optional): a dict with keys corresponding to objective
503
+ function symbols and values to reference point components for the augmentation term, i.e.,
504
+ aspiration levels. Defeults to None.
505
+ weights_aug (dict[str, float], optional): the weights to be used in the scalarization function's
506
+ augmentation term. Must be positive. Defaults to None.
507
+ rho (float, optional): a small scalar value to scale the sum in the objective
508
+ function of the scalarization. Defaults to 1e-6.
509
+
510
+ Returns:
511
+ tuple[Problem, str]: a tuple with the copy of the problem with the added
512
+ scalarization and the symbol of the added scalarization.
513
+ """
514
+ # check reference point
515
+ if not objective_dict_has_all_symbols(problem, reference_point):
516
+ msg = f"The give reference point {reference_point} is missing a value for one or more objectives."
517
+ raise ScalarizationError(msg)
518
+
519
+ # check augmentation term reference point
520
+ if reference_point_aug is not None and not objective_dict_has_all_symbols(problem, reference_point_aug):
521
+ msg = (
522
+ f"The given reference point for the augmentation term {reference_point_aug} "
523
+ "does not have a component defined for all the objectives."
524
+ )
525
+ raise ScalarizationError(msg)
526
+
527
+ # check the weight vector
528
+ if not objective_dict_has_all_symbols(problem, weights):
529
+ msg = f"The given weight vector {weights} is missing a value for one or more objectives."
530
+ raise ScalarizationError(msg)
531
+
532
+ # check the weight vector for the augmentation term
533
+ if weights_aug is not None and not objective_dict_has_all_symbols(problem, weights_aug):
534
+ msg = f"The given weight vector {weights_aug} is missing a value for one or more objectives."
535
+ raise ScalarizationError(msg)
536
+
537
+ corrected_rp = get_corrected_reference_point(problem, reference_point)
538
+ if reference_point_aug is not None:
539
+ corrected_rp_aug = get_corrected_reference_point(problem, reference_point_aug)
540
+
541
+ # define the auxiliary variable
542
+ alpha = Variable(
543
+ name="alpha",
544
+ symbol="_alpha",
545
+ variable_type=VariableTypeEnum.real,
546
+ lowerbound=-float("Inf"),
547
+ upperbound=float("Inf"),
548
+ initial_value=1.0,
549
+ )
550
+
551
+ # define the augmentation term
552
+ if reference_point_aug is None and weights_aug is None:
553
+ # no reference point in augmentation term
554
+ # same weights for both terms
555
+ aug_expr = " + ".join([f"({obj.symbol}_min / {weights[obj.symbol]})" for obj in problem.objectives])
556
+ elif reference_point_aug is None and weights_aug is not None:
557
+ # different weights provided for augmentation term
558
+ aug_expr = " + ".join([f"({obj.symbol}_min / {weights_aug[obj.symbol]})" for obj in problem.objectives])
559
+ elif reference_point_aug is not None and weights_aug is None:
560
+ # reference point in augmentation term
561
+ aug_expr = " + ".join(
562
+ [
563
+ f"(({obj.symbol}_min - {corrected_rp_aug[obj.symbol]}) / {weights[obj.symbol]})"
564
+ for obj in problem.objectives
565
+ ]
566
+ )
567
+ else:
568
+ aug_expr = " + ".join(
569
+ [
570
+ f"(({obj.symbol}_min - {corrected_rp_aug[obj.symbol]}) / {weights_aug[obj.symbol]})"
571
+ for obj in problem.objectives
572
+ ]
573
+ )
574
+
575
+ target_expr = f"_alpha + {rho}*" + f"({aug_expr})"
576
+ scalarization = ScalarizationFunction(
577
+ name="Generic ASF scalarization objective function",
578
+ symbol=symbol,
579
+ func=target_expr,
580
+ is_convex=problem.is_convex,
581
+ is_linear=problem.is_linear,
582
+ is_twice_differentiable=problem.is_twice_differentiable,
583
+ )
584
+
585
+ constraints = []
586
+
587
+ for obj in problem.objectives:
588
+ expr = f"({obj.symbol}_min - {corrected_rp[obj.symbol]}) / {weights[obj.symbol]} - _alpha"
589
+
590
+ # since we are subtracting a constant value, the linearity, convexity,
591
+ # and differentiability of the objective function, and hence the
592
+ # constraint, should not change.
593
+ constraints.append(
594
+ Constraint(
595
+ name=f"Constraint for {obj.symbol}",
596
+ symbol=f"{obj.symbol}_con",
597
+ func=expr,
598
+ cons_type=ConstraintTypeEnum.LTE,
599
+ is_linear=obj.is_linear,
600
+ is_convex=obj.is_convex,
601
+ is_twice_differentiable=obj.is_twice_differentiable,
602
+ )
603
+ )
604
+
605
+ _problem = problem.add_variables([alpha])
606
+ _problem = _problem.add_scalarization(scalarization)
607
+ return _problem.add_constraints(constraints), symbol
608
+
609
+
610
+ def add_asf_generic_nondiff( # noqa: PLR0913
611
+ problem: Problem,
612
+ symbol: str,
613
+ reference_point: dict[str, float],
614
+ weights: dict[str, float],
615
+ reference_point_aug: dict[str, float] | None = None,
616
+ weights_aug: dict[str, float] | None = None,
617
+ rho: float = 0.000001,
618
+ ) -> tuple[Problem, str]:
619
+ r"""Adds the generic achievement scalarizing function to a problem with the given reference point, and weights.
620
+
621
+ This is the non-differentiable variant of the generic achievement scalarizing function, which
622
+ means the resulting scalarization function is non-differentiable. Compared to `add_asf_nondiff`, this
623
+ variant is useful, when the problem being scalarized does not have a defined ideal or nadir point,
624
+ or both. The weights should be non-zero to avoid zero division.
625
+
626
+ The scalarization is defined as follows:
627
+
628
+ \begin{equation}
629
+ \mathcal{S}_\text{ASF}(F(\mathbf{x}); \mathbf{q}, \mathbf{w}) =
630
+ \underset{i=1,\ldots,k}{\text{max}}
631
+ \left[
632
+ \frac{f_i(\mathbf{x}) - q_i}{w_i}
633
+ \right]
634
+ + \rho\sum_{i=1}^{k} \frac{f_i(\mathbf{x})}{w_i},
635
+ \end{equation}
636
+
637
+ where $\mathbf{q} = [q_1,\dots,q_k]$ is a reference point, $\mathbf{w} =
638
+ [w_1,\dots,w_k]$ are weights, $k$ is the number of objective functions, and
639
+ $\delta$ and $\rho$ are small scalar values. The summation term in the
640
+ scalarization is known as the _augmentation term_. If a reference point is
641
+ chosen to be used in the augmentation term, e.g., a separate
642
+ reference point for the augmentation term is given (`reference_point_aug`), then
643
+ the reference point components are subtracted from the objective function values
644
+ in the nominator of the augmentation term. That is:
645
+
646
+ \begin{equation}
647
+ \mathcal{S}_\text{ASF}(F(\mathbf{x}); \mathbf{q}, \mathbf{w}) =
648
+ \underset{i=1,\ldots,k}{\text{max}}
649
+ \left[
650
+ \frac{f_i(\mathbf{x}) - q_i}{w_i}
651
+ \right]
652
+ + \rho\sum_{i=1}^{k} \frac{f_i(\mathbf{x}) - q_i}{w_i}.
653
+ \end{equation}
654
+
655
+ Args:
656
+ problem (Problem): the problem to which the scalarization function should be added.
657
+ symbol (str): the symbol to reference the added scalarization function.
658
+ reference_point (dict[str, float]): a reference point with as many components as there are objectives.
659
+ weights (dict[str, float]): the weights to be used in the scalarization function. must be positive.
660
+ reference_point_aug (dict[str, float], optional): a dict with keys corresponding to objective
661
+ function symbols and values to reference point components for the augmentation term, i.e.,
662
+ aspiration levels. Defeults to None.
663
+ weights_aug (dict[str, float], optional): the weights to be used in the scalarization function's
664
+ augmentation term. Must be positive. Defaults to None.
665
+ rho (float, optional): the weight factor used in the augmentation term. Defaults to 0.000001.
666
+
667
+ Raises:
668
+ ScalarizationError: If either the reference point or the weights given are missing any of the objective
669
+ components.
670
+ ScalarizationError: If any of the ideal or nadir point values are undefined (None).
671
+
672
+ Returns:
673
+ tuple[Problem, str]: A tuple containing a copy of the problem with the scalarization function added,
674
+ and the symbol of the added scalarization function.
675
+ """
676
+ # check reference point
677
+ if not objective_dict_has_all_symbols(problem, reference_point):
678
+ msg = f"The give reference point {reference_point} is missing a value for one or more objectives."
679
+ raise ScalarizationError(msg)
680
+
681
+ # check augmentation term reference point
682
+ if reference_point_aug is not None and not objective_dict_has_all_symbols(problem, reference_point_aug):
683
+ msg = (
684
+ f"The given reference point for the augmentation term {reference_point_aug} "
685
+ "does not have a component defined for all the objectives."
686
+ )
687
+ raise ScalarizationError(msg)
688
+
689
+ # check the weight vector
690
+ if not objective_dict_has_all_symbols(problem, weights):
691
+ msg = f"The given weight vector {weights} is missing a value for one or more objectives."
692
+ raise ScalarizationError(msg)
693
+
694
+ # check the weight vector for the augmentation term
695
+ if weights_aug is not None and not objective_dict_has_all_symbols(problem, weights_aug):
696
+ msg = f"The given weight vector {weights_aug} is missing a value for one or more objectives."
697
+ raise ScalarizationError(msg)
698
+
699
+ # get the corrected reference point
700
+ corrected_rp = get_corrected_reference_point(problem, reference_point)
701
+ if reference_point_aug is not None:
702
+ corrected_rp_aug = get_corrected_reference_point(problem, reference_point_aug)
703
+
704
+ # Build the max term
705
+ max_operands = [
706
+ (f"({obj.symbol}_min - {corrected_rp[obj.symbol]}) / ({weights[obj.symbol]})") for obj in problem.objectives
707
+ ]
708
+ max_term = f"{Op.MAX}({', '.join(max_operands)})"
709
+
710
+ # Build the augmentation term
711
+ if reference_point_aug is None and weights_aug is None:
712
+ # no reference point in augmentation term
713
+ # same weights for both terms
714
+ aug_expr = " + ".join([f"({obj.symbol}_min / {weights[obj.symbol]})" for obj in problem.objectives])
715
+ elif reference_point_aug is None and weights_aug is not None:
716
+ # different weights provided for augmentation term
717
+ aug_expr = " + ".join([f"({obj.symbol}_min / {weights_aug[obj.symbol]})" for obj in problem.objectives])
718
+ elif reference_point_aug is not None and weights_aug is None:
719
+ # reference point in augmentation term
720
+ aug_expr = " + ".join(
721
+ [
722
+ f"(({obj.symbol}_min - {corrected_rp_aug[obj.symbol]}) / {weights[obj.symbol]})"
723
+ for obj in problem.objectives
724
+ ]
725
+ )
726
+ else:
727
+ aug_expr = " + ".join(
728
+ [
729
+ f"(({obj.symbol}_min - {corrected_rp_aug[obj.symbol]}) / {weights_aug[obj.symbol]})"
730
+ for obj in problem.objectives
731
+ ]
732
+ )
733
+
734
+ # Collect the terms
735
+ sf = f"{max_term} + {rho} * ({aug_expr})"
736
+
737
+ # Add the function to the problem
738
+ scalarization_function = ScalarizationFunction(
739
+ name="Generic achievement scalarizing function",
740
+ symbol=symbol,
741
+ func=sf,
742
+ is_linear=False,
743
+ is_convex=False,
744
+ is_twice_differentiable=False,
745
+ )
746
+ return problem.add_scalarization(scalarization_function), symbol
747
+
748
+
749
+ def add_nimbus_sf_diff( # noqa: PLR0913
750
+ problem: Problem,
751
+ symbol: str,
752
+ classifications: dict[str, tuple[str, float | None]],
753
+ current_objective_vector: dict[str, float],
754
+ ideal: dict[str, float] | None = None,
755
+ nadir: dict[str, float] | None = None,
756
+ delta: float = 0.000001,
757
+ rho: float = 0.000001,
758
+ ) -> Problem:
759
+ r"""Implements the differentiable variant of the NIMBUS scalarization function.
760
+
761
+ \begin{align*}
762
+ \min \quad & \alpha + \rho \sum_{i =1}^k \frac{f_i(\mathbf{x})}{z_i^{nad} - z_i^{\star\star}} \\
763
+ \text{s.t.} \quad & \frac{f_i(\mathbf{x}) - z_i^*}{z_i^{nad} - z_i^{\star\star}} -
764
+ \alpha \leq 0 \quad & \forall i \in I^< \\
765
+ & \frac{f_i(\mathbf{x}) - \hat{z}_i}{z_i^{nad} - z_i^{\star\star}} - \alpha \leq 0 \quad &
766
+ \forall i \in I^\leq \\
767
+ & f_i(\mathbf{x}) - f_i(\mathbf{x_c}) \leq 0 \quad & \forall i \in I^< \cup I^\leq \cup I^= \\
768
+ & f_i(\mathbf{x}) - \epsilon_i \leq 0 \quad & \forall i \in I^\geq \\
769
+ & \mathbf{x} \in S,
770
+ \end{align*}
771
+
772
+ where $f_i$ are objective functions, $f_i(\mathbf{x_c})$ is a component of
773
+ the current objective function, $\hat{z}_i$ is an aspiration level,
774
+ $\varepsilon_i$ is a reservation level, $z_i^\star$ is a component of the
775
+ ideal point, $z_i^{\star\star} = z_i^\star - \delta$ is a component of the
776
+ utopian point, $z_i^\text{nad}$ is a component of the nadir point, $\rho$ is
777
+ a small scalar, $S$ is the feasible solution space of the problem (i.e., it
778
+ means the other constraints of the problem being solved should be accounted
779
+ for as well), and $\alpha$ is an auxiliary variable.
780
+
781
+ The $I$-sets are related to the classifications given to each objective function value
782
+ in respect to the current objective vector (e.g., by a decision maker). They
783
+ are as follows:
784
+
785
+ - $I^{<}$: values that should improve,
786
+ - $I^{\leq}$: values that should improve until a given aspiration level $\hat{z}_i$,
787
+ - $I^{=}$: values that are fine as they are,
788
+ - $I^{\geq}$: values that can be impaired until some reservation level $\varepsilon_i$, and
789
+ - $I^{\diamond}$: values that are allowed to change freely (not present explicitly in this scalarization function).
790
+
791
+ The aspiration levels and the reservation levels are supplied for each classification, when relevant, in
792
+ the argument `classifications` as follows:
793
+
794
+ ```python
795
+ classifications = {
796
+ "f_1": ("<", None),
797
+ "f_2": ("<=", 42.1),
798
+ "f_3": (">=", 22.2),
799
+ "f_4": ("0", None)
800
+ }
801
+ ```
802
+
803
+ Here, we have assumed four objective functions. The key of the dict is a function's symbol, and the tuple
804
+ consists of a pair where the left element is the classification (self explanatory, '0' is for objective values
805
+ that may change freely), the right element is either `None` or an aspiration or a reservation level
806
+ depending on the classification.
807
+
808
+ References:
809
+ Miettinen, K., & Mäkelä, M. M. (2002). On scalarizing functions in
810
+ multiobjective optimization. OR Spectrum, 24(2), 193-213.
811
+
812
+
813
+ Args:
814
+ problem (Problem): the problem to be scalarized.
815
+ symbol (str): the symbol given to the scalarization function, i.e., target of the optimization.
816
+ classifications (dict[str, tuple[str, float | None]]): a dict, where the key is a symbol
817
+ of an objective function, and the value is a tuple with a classification and an aspiration
818
+ or a reservation level, or `None`, depending on the classification. See above for an
819
+ explanation.
820
+ current_objective_vector (dict[str, float]): the current objective vector that corresponds to
821
+ a Pareto optimal solution. The classifications are assumed to been given in respect to
822
+ this vector.
823
+ ideal (dict[str, float], optional): ideal point values. If not given, attempt will be made
824
+ to calculate ideal point from problem.
825
+ nadir (dict[str, float], optional): nadir point values. If not given, attempt will be made
826
+ to calculate nadir point from problem.
827
+ delta (float, optional): a small scalar used to define the utopian point. Defaults to 0.000001.
828
+ rho (float, optional): a small scalar used in the augmentation term. Defaults to 0.000001.
829
+
830
+ Returns:
831
+ tuple[Problem, str]: a tuple with a copy of the problem with the added scalarizations and the
832
+ symbol of the scalarization.
833
+ """
834
+ # check that classifications have been provided for all objective functions
835
+ if not objective_dict_has_all_symbols(problem, classifications):
836
+ msg = (
837
+ f"The given classifications {classifications} do not define "
838
+ "a classification for all the objective functions."
839
+ )
840
+ raise ScalarizationError(msg)
841
+
842
+ # check that at least one objective function is allowed to be improved and one is
843
+ # allowed to worsen
844
+ if not any(classifications[obj.symbol][0] in ["<", "<="] for obj in problem.objectives) or not any(
845
+ classifications[obj.symbol][0] in [">=", "0"] for obj in problem.objectives
846
+ ):
847
+ msg = (
848
+ f"The given classifications {classifications} should allow at least one objective function value "
849
+ "to improve and one to worsen."
850
+ )
851
+ raise ScalarizationError(msg)
852
+
853
+ # check if ideal point is specified
854
+ # if not specified, try to calculate corrected ideal point
855
+ if ideal is not None:
856
+ ideal_point = ideal
857
+ elif problem.get_ideal_point() is not None:
858
+ ideal_point = get_corrected_ideal(problem)
859
+ else:
860
+ msg = "Ideal point not defined!"
861
+ raise ScalarizationError(msg)
862
+
863
+ # check if nadir point is specified
864
+ # if not specified, try to calculate corrected nadir point
865
+ if nadir is not None:
866
+ nadir_point = nadir
867
+ elif problem.get_nadir_point() is not None:
868
+ nadir_point = get_corrected_nadir(problem)
869
+ else:
870
+ msg = "Nadir point not defined!"
871
+ raise ScalarizationError(msg)
872
+
873
+ # define the auxiliary variable
874
+ alpha = Variable(
875
+ name="alpha",
876
+ symbol="_alpha",
877
+ variable_type=VariableTypeEnum.real,
878
+ lowerbound=-float("Inf"),
879
+ upperbound=float("Inf"),
880
+ initial_value=1.0,
881
+ )
882
+
883
+ # define the objective function of the scalarization
884
+ aug_expr = " + ".join(
885
+ [
886
+ f"{obj.symbol}_min / ({nadir_point[obj.symbol]} - {ideal_point[obj.symbol] - delta})"
887
+ for obj in problem.objectives
888
+ ]
889
+ )
890
+
891
+ target_expr = f"_alpha + {rho}*" + f"({aug_expr})"
892
+ scalarization = ScalarizationFunction(
893
+ name="NIMBUS scalarization objective function",
894
+ symbol=symbol,
895
+ func=target_expr,
896
+ is_linear=problem.is_linear,
897
+ is_convex=problem.is_convex,
898
+ is_twice_differentiable=problem.is_twice_differentiable,
899
+ )
900
+
901
+ constraints = []
902
+
903
+ # create all the constraints
904
+ for obj in problem.objectives:
905
+ _symbol = obj.symbol
906
+ match classifications[_symbol]:
907
+ case ("<", _):
908
+ expr = (
909
+ f"({_symbol}_min - {ideal_point[_symbol]}) / "
910
+ f"({nadir_point[_symbol] - (ideal_point[_symbol] - delta)}) - _alpha"
911
+ )
912
+ constraints.append(
913
+ Constraint(
914
+ name=f"improvement constraint for {_symbol}",
915
+ symbol=f"{_symbol}_lt",
916
+ func=expr,
917
+ cons_type=ConstraintTypeEnum.LTE,
918
+ is_linear=problem.is_linear,
919
+ is_convex=problem.is_convex,
920
+ is_twice_differentiable=problem.is_twice_differentiable,
921
+ )
922
+ )
923
+
924
+ # if obj is to be maximized, then the current objective vector value needs to be multiplied by -1
925
+ expr = f"{_symbol}_min - {current_objective_vector[_symbol]}{' * -1' if obj.maximize else ''}"
926
+ constraints.append(
927
+ Constraint(
928
+ name=f"stay at least equal constraint for {_symbol}",
929
+ symbol=f"{_symbol}_eq",
930
+ func=expr,
931
+ cons_type=ConstraintTypeEnum.LTE,
932
+ is_linear=problem.is_linear,
933
+ is_convex=problem.is_convex,
934
+ is_twice_differentiable=problem.is_twice_differentiable,
935
+ )
936
+ )
937
+ case ("<=", aspiration):
938
+ # if obj is to be maximized, then the current reservation value needs to be multiplied by -1
939
+ expr = (
940
+ f"({_symbol}_min - {aspiration}{' * -1' if obj.maximize else ''}) / "
941
+ f"({nadir_point[_symbol]} - {ideal_point[_symbol] - delta}) - _alpha"
942
+ )
943
+ constraints.append(
944
+ Constraint(
945
+ name=f"improvement until constraint for {_symbol}",
946
+ symbol=f"{_symbol}_lte",
947
+ func=expr,
948
+ cons_type=ConstraintTypeEnum.LTE,
949
+ is_linear=problem.is_linear,
950
+ is_convex=problem.is_convex,
951
+ is_twice_differentiable=problem.is_twice_differentiable,
952
+ )
953
+ )
954
+
955
+ # if obj is to be maximized, then the current objective vector value needs to be multiplied by -1
956
+ expr = f"{_symbol}_min - {current_objective_vector[_symbol]}{' * -1' if obj.maximize else ''}"
957
+ constraints.append(
958
+ Constraint(
959
+ name=f"stay at least equal constraint for {_symbol}",
960
+ symbol=f"{_symbol}_eq",
961
+ func=expr,
962
+ cons_type=ConstraintTypeEnum.LTE,
963
+ is_linear=problem.is_linear,
964
+ is_convex=problem.is_convex,
965
+ is_twice_differentiable=problem.is_twice_differentiable,
966
+ )
967
+ )
968
+ case ("=", _):
969
+ # if obj is to be maximized, then the current objective vector value needs to be multiplied by -1
970
+ expr = f"{_symbol}_min - {current_objective_vector[_symbol]}{' * -1' if obj.maximize else ''}"
971
+ constraints.append(
972
+ Constraint(
973
+ name=f"stay at least equal constraint for {_symbol}",
974
+ symbol=f"{_symbol}_eq",
975
+ func=expr,
976
+ cons_type=ConstraintTypeEnum.LTE,
977
+ is_linear=problem.is_linear,
978
+ is_convex=problem.is_convex,
979
+ is_twice_differentiable=problem.is_twice_differentiable,
980
+ )
981
+ )
982
+ case (">=", reservation):
983
+ # if obj is to be maximized, then the reservation value needs to be multiplied by -1
984
+ expr = f"{_symbol}_min - {reservation}{' * -1' if obj.maximize else ''}"
985
+ constraints.append(
986
+ Constraint(
987
+ name=f"worsen until constriant for {_symbol}",
988
+ symbol=f"{_symbol}_gte",
989
+ func=expr,
990
+ cons_type=ConstraintTypeEnum.LTE,
991
+ is_linear=problem.is_linear,
992
+ is_convex=problem.is_convex,
993
+ is_twice_differentiable=problem.is_twice_differentiable,
994
+ )
995
+ )
996
+ case ("0", _):
997
+ # not relevant for this scalarization
998
+ pass
999
+ case (c, _):
1000
+ msg = (
1001
+ f"Warning! The classification {c} was supplied, but it is not supported."
1002
+ "Must be one of ['<', '<=', '0', '=', '>=']"
1003
+ )
1004
+
1005
+ # add the auxiliary variable, scalarization, and constraints
1006
+ _problem = problem.add_variables([alpha])
1007
+ _problem = _problem.add_scalarization(scalarization)
1008
+ return _problem.add_constraints(constraints), symbol
1009
+
1010
+
1011
+ def add_nimbus_sf_nondiff( # noqa: PLR0913
1012
+ problem: Problem,
1013
+ symbol: str,
1014
+ classifications: dict[str, tuple[str, float | None]],
1015
+ current_objective_vector: dict[str, float],
1016
+ ideal: dict[str, float] | None = None,
1017
+ nadir: dict[str, float] | None = None,
1018
+ delta: float = 0.000001,
1019
+ rho: float = 0.000001,
1020
+ ) -> Problem:
1021
+ r"""Implements the non-differentiable variant of the NIMBUS scalarization function.
1022
+
1023
+ \begin{align*}
1024
+ \underset{\mathbf{x}}{\min}
1025
+ \underset{\substack{j \in I^\leq \\i \in I^<}}{\max}
1026
+ &\left[ \frac{f_i(\mathbf{x}) - z_i^\star}{z_i^\text{nad} - z_i^{\star\star}},
1027
+ \frac{f_j(\mathbf{x}) - \hat{z}_j}{z_j^\text{nad} - x_j^{\star\star}} \right]
1028
+ +\rho \sum_{i =1}^k \frac{f_i(\mathbf{x})}{z_i^{nad} - z_i^{\star\star}} \\
1029
+ \text{s.t.} \quad & f_i(\mathbf{x}) - f_i(\mathbf{x}^c) \leq 0\quad&\forall i \in I^< \cup I^\leq \cup I^=,\\
1030
+ & f_i(\mathbf{x}) - \epsilon_i \leq 0\quad&\forall i \in I^\geq,\\
1031
+ & \mathbf{x} \in S,
1032
+ \end{align*}
1033
+
1034
+ where $f_i$ are objective functions, $f_i(\mathbf{x_c})$ is a component of
1035
+ the current objective function, $\hat{z}_i$ is an aspiration level,
1036
+ $\varepsilon_i$ is a reservation level, $z_i^\star$ is a component of the
1037
+ ideal point, $z_i^{\star\star} = z_i^\star - \delta$ is a component of the
1038
+ utopian point, $z_i^\text{nad}$ is a component of the nadir point, $\rho$ is
1039
+ a small scalar, and $S$ is the feasible solution space of the problem (i.e., it
1040
+ means the other constraints of the problem being solved should be accounted
1041
+ for as well).
1042
+
1043
+ The $I$-sets are related to the classifications given to each objective function value
1044
+ in respect to the current objective vector (e.g., by a decision maker). They
1045
+ are as follows:
1046
+
1047
+ - $I^{<}$: values that should improve,
1048
+ - $I^{\leq}$: values that should improve until a given aspiration level $\hat{z}_i$,
1049
+ - $I^{=}$: values that are fine as they are,
1050
+ - $I^{\geq}$: values that can be impaired until some reservation level $\varepsilon_i$, and
1051
+ - $I^{\diamond}$: values that are allowed to change freely (not present explicitly in this scalarization function).
1052
+
1053
+ The aspiration levels and the reservation levels are supplied for each classification, when relevant, in
1054
+ the argument `classifications` as follows:
1055
+
1056
+ ```python
1057
+ classifications = {
1058
+ "f_1": ("<", None),
1059
+ "f_2": ("<=", 42.1),
1060
+ "f_3": (">=", 22.2),
1061
+ "f_4": ("0", None)
1062
+ }
1063
+ ```
1064
+
1065
+ Here, we have assumed four objective functions. The key of the dict is a function's symbol, and the tuple
1066
+ consists of a pair where the left element is the classification (self explanatory, '0' is for objective values
1067
+ that may change freely), the right element is either `None` or an aspiration or a reservation level
1068
+ depending on the classification.
1069
+
1070
+ References:
1071
+ Miettinen, K., & Mäkelä, M. M. (2002). On scalarizing functions in
1072
+ multiobjective optimization. OR Spectrum, 24(2), 193-213.
1073
+
1074
+
1075
+ Args:
1076
+ problem (Problem): the problem to be scalarized.
1077
+ symbol (str): the symbol given to the scalarization function, i.e., target of the optimization.
1078
+ classifications (dict[str, tuple[str, float | None]]): a dict, where the key is a symbol
1079
+ of an objective function, and the value is a tuple with a classification and an aspiration
1080
+ or a reservation level, or `None`, depending on the classification. See above for an
1081
+ explanation.
1082
+ current_objective_vector (dict[str, float]): the current objective vector that corresponds to
1083
+ a Pareto optimal solution. The classifications are assumed to been given in respect to
1084
+ this vector.
1085
+ ideal (dict[str, float], optional): optional ideal point values. If not given, attempt will be made
1086
+ to calculate ideal point from problem.
1087
+ nadir (dict[str, float], optional): optional nadir point values. If not given, attempt will be made
1088
+ to calculate nadir point from problem.
1089
+ delta (float, optional): a small scalar used to define the utopian point. Defaults to 0.000001.
1090
+ rho (float, optional): a small scalar used in the augmentation term. Defaults to 0.000001.
1091
+
1092
+ Returns:
1093
+ tuple[Problem, str]: a tuple with a copy of the problem with the added scalarizations and the
1094
+ symbol of the scalarization.
1095
+ """
1096
+ # check that classifications have been provided for all objective functions
1097
+ if not objective_dict_has_all_symbols(problem, classifications):
1098
+ msg = (
1099
+ f"The given classifications {classifications} do not define "
1100
+ "a classification for all the objective functions."
1101
+ )
1102
+ raise ScalarizationError(msg)
1103
+
1104
+ # check that at least one objective function is allowed to be improved and one is
1105
+ # allowed to worsen
1106
+ if not any(classifications[obj.symbol][0] in ["<", "<="] for obj in problem.objectives) or not any(
1107
+ classifications[obj.symbol][0] in [">=", "0"] for obj in problem.objectives
1108
+ ):
1109
+ msg = (
1110
+ f"The given classifications {classifications} should allow at least one objective function value "
1111
+ "to improve and one to worsen."
1112
+ )
1113
+ raise ScalarizationError(msg)
1114
+
1115
+ # check if ideal point is specified
1116
+ # if not specified, try to calculate corrected ideal point
1117
+ if ideal is not None:
1118
+ ideal_point = ideal
1119
+ elif problem.get_ideal_point() is not None:
1120
+ ideal_point = get_corrected_ideal(problem)
1121
+ else:
1122
+ msg = "Ideal point not defined!"
1123
+ raise ScalarizationError(msg)
1124
+
1125
+ # check if nadir point is specified
1126
+ # if not specified, try to calculate corrected nadir point
1127
+ if nadir is not None:
1128
+ nadir_point = nadir
1129
+ elif problem.get_nadir_point() is not None:
1130
+ nadir_point = get_corrected_nadir(problem)
1131
+ else:
1132
+ msg = "Nadir point not defined!"
1133
+ raise ScalarizationError(msg)
1134
+
1135
+ corrected_current_point = get_corrected_reference_point(problem, current_objective_vector)
1136
+
1137
+ # max term and constraints
1138
+ max_args = []
1139
+ constraints = []
1140
+
1141
+ for obj in problem.objectives:
1142
+ _symbol = obj.symbol
1143
+ match classifications[_symbol]:
1144
+ case ("<", _):
1145
+ max_expr = (
1146
+ f"({_symbol}_min - {ideal_point[_symbol]}) / "
1147
+ f"({nadir_point[_symbol]} - {ideal_point[_symbol] - delta})"
1148
+ )
1149
+ max_args.append(max_expr)
1150
+
1151
+ con_expr = f"{_symbol}_min - {corrected_current_point[_symbol]}"
1152
+ constraints.append(
1153
+ Constraint(
1154
+ name=f"improvement constraint for {_symbol}",
1155
+ symbol=f"{_symbol}_lt",
1156
+ func=con_expr,
1157
+ cons_type=ConstraintTypeEnum.LTE,
1158
+ is_linear=problem.is_linear,
1159
+ is_convex=problem.is_convex,
1160
+ is_twice_differentiable=problem.is_twice_differentiable,
1161
+ )
1162
+ )
1163
+
1164
+ case ("<=", aspiration):
1165
+ # if obj is to be maximized, then the current reservation value needs to be multiplied by -1
1166
+ max_expr = (
1167
+ f"({_symbol}_min - {aspiration * -1 if obj.maximize else aspiration}) / "
1168
+ f"({nadir_point[_symbol]} - {ideal_point[_symbol] - delta})"
1169
+ )
1170
+ max_args.append(max_expr)
1171
+
1172
+ con_expr = f"{_symbol}_min - {corrected_current_point[_symbol]}"
1173
+ constraints.append(
1174
+ Constraint(
1175
+ name=f"improvement until constraint for {_symbol}",
1176
+ symbol=f"{_symbol}_lte",
1177
+ func=con_expr,
1178
+ cons_type=ConstraintTypeEnum.LTE,
1179
+ is_linear=problem.is_linear,
1180
+ is_convex=problem.is_convex,
1181
+ is_twice_differentiable=problem.is_twice_differentiable,
1182
+ )
1183
+ )
1184
+
1185
+ case ("=", _):
1186
+ con_expr = f"{_symbol}_min - {corrected_current_point[_symbol]}"
1187
+ constraints.append(
1188
+ Constraint(
1189
+ name=f"Stay at least as good constraint for {_symbol}",
1190
+ symbol=f"{_symbol}_eq",
1191
+ func=con_expr,
1192
+ cons_type=ConstraintTypeEnum.LTE,
1193
+ is_linear=problem.is_linear,
1194
+ is_convex=problem.is_convex,
1195
+ is_twice_differentiable=problem.is_twice_differentiable,
1196
+ )
1197
+ )
1198
+ case (">=", reservation):
1199
+ con_expr = f"{_symbol}_min - {-1 * reservation if obj.maximize else reservation}"
1200
+ constraints.append(
1201
+ Constraint(
1202
+ name=f"Worsen until constriant for {_symbol}",
1203
+ symbol=f"{_symbol}_gte",
1204
+ func=con_expr,
1205
+ cons_type=ConstraintTypeEnum.LTE,
1206
+ is_linear=problem.is_linear,
1207
+ is_convex=problem.is_convex,
1208
+ is_twice_differentiable=problem.is_twice_differentiable,
1209
+ )
1210
+ )
1211
+ case ("0", _):
1212
+ # not relevant for this scalarization
1213
+ pass
1214
+ case (c, _):
1215
+ msg = (
1216
+ f"Warning! The classification {c} was supplied, but it is not supported."
1217
+ "Must be one of ['<', '<=', '0', '=', '>=']"
1218
+ )
1219
+
1220
+ max_expr = f"Max({','.join(max_args)})"
1221
+
1222
+ # define the objective function of the scalarization
1223
+ aug_expr = " + ".join(
1224
+ [
1225
+ f"{obj.symbol}_min / ({nadir_point[obj.symbol]} - {ideal_point[obj.symbol] - delta})"
1226
+ for obj in problem.objectives
1227
+ ]
1228
+ )
1229
+
1230
+ target_expr = f"{max_expr} + {rho}*({aug_expr})"
1231
+ scalarization = ScalarizationFunction(
1232
+ name="NIMBUS scalarization objective function",
1233
+ symbol=symbol,
1234
+ func=target_expr,
1235
+ is_linear=False,
1236
+ is_convex=False,
1237
+ is_twice_differentiable=False,
1238
+ )
1239
+
1240
+ _problem = problem.add_scalarization(scalarization)
1241
+ return _problem.add_constraints(constraints), symbol
1242
+
1243
+
1244
+ def add_group_nimbus_sf( # noqa: PLR0913
1245
+ problem: Problem,
1246
+ symbol: str,
1247
+ classifications_list: list[dict[str, tuple[str, float | None]]],
1248
+ current_objective_vector: dict[str, float],
1249
+ ideal: dict[str, float] | None = None,
1250
+ nadir: dict[str, float] | None = None,
1251
+ delta: float = 0.000001,
1252
+ rho: float = 0.000001,
1253
+ ) -> tuple[Problem, str]:
1254
+ r"""Implements the multiple decision maker variant of the NIMBUS scalarization function.
1255
+
1256
+ The scalarization function is defined as follows:
1257
+
1258
+ \begin{align}
1259
+ &\mbox{minimize} &&\max_{i\in I^<,j\in I^\leq,d} [w_{id}(f_{id}(\mathbf{x})-z^{ideal}_{id}),
1260
+ w_{jd}(f_{jd}(\mathbf{x})-\hat{z}_{jd})] +
1261
+ \rho \sum^k_{i=1} \sum^{n_d}_{d=1} w_{id}f_{id}(\mathbf{x}) \\
1262
+ &\mbox{subject to} &&\mathbf{x} \in \mathbf{X},
1263
+ \end{align}
1264
+
1265
+ where $w_{id} = \frac{1}{z^{nad}_{id} - z^{uto}_{id}}$, and $w_{jd} = \frac{1}{z^{nad}_{jd} - z^{uto}_{jd}}$.
1266
+
1267
+ The $I$-sets are related to the classifications given to each objective function value
1268
+ in respect to the current objective vector (e.g., by a decision maker). They
1269
+ are as follows:
1270
+
1271
+ - $I^{<}$: values that should improve,
1272
+ - $I^{\leq}$: values that should improve until a given aspiration level $\hat{z}_i$,
1273
+ - $I^{=}$: values that are fine as they are,
1274
+ - $I^{\geq}$: values that can be impaired until some reservation level $\varepsilon_i$, and
1275
+ - $I^{\diamond}$: values that are allowed to change freely (not present explicitly in this scalarization function).
1276
+
1277
+ The aspiration levels and the reservation levels are supplied for each classification, when relevant, in
1278
+ the argument `classifications` as follows:
1279
+
1280
+ ```python
1281
+ classifications = {
1282
+ "f_1": ("<", None),
1283
+ "f_2": ("<=", 42.1),
1284
+ "f_3": (">=", 22.2),
1285
+ "f_4": ("0", None)
1286
+ }
1287
+ ```
1288
+
1289
+ Here, we have assumed four objective functions. The key of the dict is a function's symbol, and the tuple
1290
+ consists of a pair where the left element is the classification (self explanatory, '0' is for objective values
1291
+ that may change freely), the right element is either `None` or an aspiration or a reservation level
1292
+ depending on the classification.
1293
+
1294
+ Args:
1295
+ problem (Problem): the problem to be scalarized.
1296
+ symbol (str): the symbol given to the scalarization function, i.e., target of the optimization.
1297
+ classifications_list (list[dict[str, tuple[str, float | None]]]): a list of dicts, where the key is a symbol
1298
+ of an objective function, and the value is a tuple with a classification and an aspiration
1299
+ or a reservation level, or `None`, depending on the classification. See above for an
1300
+ explanation.
1301
+ current_objective_vector (dict[str, float]): the current objective vector that corresponds to
1302
+ a Pareto optimal solution. The classifications are assumed to been given in respect to
1303
+ this vector.
1304
+ ideal (dict[str, float], optional): ideal point values. If not given, attempt will be made
1305
+ to calculate ideal point from problem.
1306
+ nadir (dict[str, float], optional): nadir point values. If not given, attempt will be made
1307
+ to calculate nadir point from problem.
1308
+ delta (float, optional): a small scalar used to define the utopian point. Defaults to 0.000001.
1309
+ rho (float, optional): a small scalar used in the augmentation term. Defaults to 0.000001.
1310
+
1311
+ Raises:
1312
+ ScalarizationError: any of the given classifications do not define a classification
1313
+ for all the objective functions or any of the given classifications do not allow at
1314
+ least one objective function value to improve and one to worsen.
1315
+
1316
+ Returns:
1317
+ tuple[Problem, str]: a tuple with the copy of the problem with the added
1318
+ scalarization and the symbol of the added scalarization.
1319
+ """
1320
+ # check that classifications have been provided for all objective functions
1321
+ for classifications in classifications_list:
1322
+ if not objective_dict_has_all_symbols(problem, classifications):
1323
+ msg = (
1324
+ f"The given classifications {classifications} do not define "
1325
+ "a classification for all the objective functions."
1326
+ )
1327
+ raise ScalarizationError(msg)
1328
+
1329
+ # check that at least one objective function is allowed to be improved and one is
1330
+ # allowed to worsen
1331
+ if not any(classifications[obj.symbol][0] in ["<", "<="] for obj in problem.objectives) or not any(
1332
+ classifications[obj.symbol][0] in [">=", "0"] for obj in problem.objectives
1333
+ ):
1334
+ msg = (
1335
+ f"The given classifications {classifications} should allow at least one objective function value "
1336
+ "to improve and one to worsen."
1337
+ )
1338
+ raise ScalarizationError(msg)
1339
+
1340
+ # check if ideal point is specified
1341
+ # if not specified, try to calculate corrected ideal point
1342
+ if ideal is not None:
1343
+ ideal_point = ideal
1344
+ elif problem.get_ideal_point() is not None:
1345
+ ideal_point = get_corrected_ideal(problem)
1346
+ else:
1347
+ msg = "Ideal point not defined!"
1348
+ raise ScalarizationError(msg)
1349
+
1350
+ # check if nadir point is specified
1351
+ # if not specified, try to calculate corrected nadir point
1352
+ if nadir is not None:
1353
+ nadir_point = nadir
1354
+ elif problem.get_nadir_point() is not None:
1355
+ nadir_point = get_corrected_nadir(problem)
1356
+ else:
1357
+ msg = "Nadir point not defined!"
1358
+ raise ScalarizationError(msg)
1359
+
1360
+ corrected_current_point = get_corrected_reference_point(problem, current_objective_vector)
1361
+
1362
+ # calculate the weights
1363
+ weights = {
1364
+ obj.symbol: 1 / (nadir_point[obj.symbol] - (ideal_point[obj.symbol] - delta)) for obj in problem.objectives
1365
+ }
1366
+
1367
+ # max term and constraints
1368
+ max_args = []
1369
+ constraints = []
1370
+
1371
+ for i in range(len(classifications_list)):
1372
+ classifications = classifications_list[i]
1373
+ for obj in problem.objectives:
1374
+ _symbol = obj.symbol
1375
+ match classifications[_symbol]:
1376
+ case ("<", _):
1377
+ max_expr = f"{weights[_symbol]} * ({_symbol}_min - {ideal_point[_symbol]})"
1378
+ max_args.append(max_expr)
1379
+
1380
+ con_expr = f"{_symbol}_min - {corrected_current_point[_symbol]}"
1381
+ constraints.append(
1382
+ Constraint(
1383
+ name=f"improvement constraint for {_symbol}",
1384
+ symbol=f"{_symbol}_{i+1}_lt",
1385
+ func=con_expr,
1386
+ cons_type=ConstraintTypeEnum.LTE,
1387
+ is_linear=problem.is_linear,
1388
+ is_convex=problem.is_convex,
1389
+ is_twice_differentiable=problem.is_twice_differentiable,
1390
+ )
1391
+ )
1392
+ case ("<=", aspiration):
1393
+ # if obj is to be maximized, then the current aspiration value needs to be multiplied by -1
1394
+ max_expr = (
1395
+ f"{weights[_symbol]} * ({_symbol}_min - {aspiration * -1 if obj.maximize else aspiration})"
1396
+ )
1397
+ max_args.append(max_expr)
1398
+
1399
+ con_expr = f"{_symbol}_min - {corrected_current_point[_symbol]}"
1400
+ constraints.append(
1401
+ Constraint(
1402
+ name=f"improvement until constraint for {_symbol}",
1403
+ symbol=f"{_symbol}_{i+1}_lte",
1404
+ func=con_expr,
1405
+ cons_type=ConstraintTypeEnum.LTE,
1406
+ is_linear=problem.is_linear,
1407
+ is_convex=problem.is_convex,
1408
+ is_twice_differentiable=problem.is_twice_differentiable,
1409
+ )
1410
+ )
1411
+ case ("=", _):
1412
+ con_expr = f"{_symbol}_min - {corrected_current_point[_symbol]}"
1413
+ constraints.append(
1414
+ Constraint(
1415
+ name=f"Stay at least as good constraint for {_symbol}",
1416
+ symbol=f"{_symbol}_{i+1}_eq",
1417
+ func=con_expr,
1418
+ cons_type=ConstraintTypeEnum.LTE,
1419
+ is_linear=problem.is_linear,
1420
+ is_convex=problem.is_convex,
1421
+ is_twice_differentiable=problem.is_twice_differentiable,
1422
+ )
1423
+ )
1424
+ case (">=", reservation):
1425
+ # if obj is to be maximized, then the current reservation value needs to be multiplied by -1
1426
+ con_expr = f"{_symbol}_min - {-1 * reservation if obj.maximize else reservation}"
1427
+ constraints.append(
1428
+ Constraint(
1429
+ name=f"Worsen until constraint for {_symbol}",
1430
+ symbol=f"{_symbol}_{i+1}_gte",
1431
+ func=con_expr,
1432
+ cons_type=ConstraintTypeEnum.LTE,
1433
+ is_linear=problem.is_linear,
1434
+ is_convex=problem.is_convex,
1435
+ is_twice_differentiable=problem.is_twice_differentiable,
1436
+ )
1437
+ )
1438
+ case ("0", _):
1439
+ # not relevant for this scalarization
1440
+ pass
1441
+ case (c, _):
1442
+ msg = (
1443
+ f"Warning! The classification {c} was supplied, but it is not supported."
1444
+ "Must be one of ['<', '<=', '0', '=', '>=']"
1445
+ )
1446
+ max_expr = f"Max({','.join(max_args)})"
1447
+
1448
+ # form the augmentation term
1449
+ aug_exprs = []
1450
+ for _ in range(len(classifications_list)):
1451
+ aug_expr = " + ".join([f"({weights[obj.symbol]} * {obj.symbol}_min)" for obj in problem.objectives])
1452
+ aug_exprs.append(aug_expr)
1453
+ aug_exprs = " + ".join(aug_exprs)
1454
+
1455
+ func = f"{max_expr} + {rho} * ({aug_exprs})"
1456
+ scalarization = ScalarizationFunction(
1457
+ name="NIMBUS scalarization objective function for multiple decision makers",
1458
+ symbol=symbol,
1459
+ func=func,
1460
+ is_linear=problem.is_linear,
1461
+ is_convex=problem.is_convex,
1462
+ is_twice_differentiable=False,
1463
+ )
1464
+
1465
+ _problem = problem.add_scalarization(scalarization)
1466
+ return _problem.add_constraints(constraints), symbol
1467
+
1468
+
1469
+ def add_group_nimbus_sf_diff( # noqa: PLR0913
1470
+ problem: Problem,
1471
+ symbol: str,
1472
+ classifications_list: list[dict[str, tuple[str, float | None]]],
1473
+ current_objective_vector: dict[str, float],
1474
+ ideal: dict[str, float] | None = None,
1475
+ nadir: dict[str, float] | None = None,
1476
+ delta: float = 0.000001,
1477
+ rho: float = 0.000001,
1478
+ ) -> tuple[Problem, str]:
1479
+ r"""Implements the differentiable variant of the multiple decision maker of the group NIMBUS scalarization function.
1480
+
1481
+ The scalarization function is defined as follows:
1482
+
1483
+ \begin{align}
1484
+ \mbox{minimize} \quad
1485
+ &\alpha +
1486
+ \rho \sum^k_{i=1} \sum^{n_d}_{d=1} w_{id}f_{id}(\mathbf{x})\\
1487
+ \mbox{subject to} \quad & w_{id}(f_{id}(\mathbf{x})-z^{ideal}_{id}) - \alpha \leq 0 \quad & \forall i \in I^<,\\
1488
+ & w_{jd}(f_{jd}(\mathbf{x})-\hat{z}_{jd}) - \alpha \leq 0 \quad & \forall j \in I^\leq ,\\
1489
+ & f_i(\mathbf{x}) - f_i(\mathbf{x_c}) \leq 0 \quad & \forall i \in I^< \cup I^\leq \cup I^= ,\\
1490
+ & f_i(\mathbf{x}) - \epsilon_i \leq 0 \quad & \forall i \in I^\geq ,\\
1491
+ & \mathbf{x} \in \mathbf{X},
1492
+ \end{align}
1493
+
1494
+ where $w_{id} = \frac{1}{z^{nad}_{id} - z^{uto}_{id}}$, and $w_{jd} = \frac{1}{z^{nad}_{jd} - z^{uto}_{jd}}$.
1495
+
1496
+ The $I$-sets are related to the classifications given to each objective function value
1497
+ in respect to the current objective vector (e.g., by a decision maker). They
1498
+ are as follows:
1499
+
1500
+ - $I^{<}$: values that should improve,
1501
+ - $I^{\leq}$: values that should improve until a given aspiration level $\hat{z}_i$,
1502
+ - $I^{=}$: values that are fine as they are,
1503
+ - $I^{\geq}$: values that can be impaired until some reservation level $\varepsilon_i$, and
1504
+ - $I^{\diamond}$: values that are allowed to change freely (not present explicitly in this scalarization function).
1505
+
1506
+ The aspiration levels and the reservation levels are supplied for each classification, when relevant, in
1507
+ the argument `classifications` as follows:
1508
+
1509
+ ```python
1510
+ classifications = {
1511
+ "f_1": ("<", None),
1512
+ "f_2": ("<=", 42.1),
1513
+ "f_3": (">=", 22.2),
1514
+ "f_4": ("0", None)
1515
+ }
1516
+ ```
1517
+
1518
+ Here, we have assumed four objective functions. The key of the dict is a function's symbol, and the tuple
1519
+ consists of a pair where the left element is the classification (self explanatory, '0' is for objective values
1520
+ that may change freely), the right element is either `None` or an aspiration or a reservation level
1521
+ depending on the classification.
1522
+
1523
+ Args:
1524
+ problem (Problem): the problem to be scalarized.
1525
+ symbol (str): the symbol given to the scalarization function, i.e., target of the optimization.
1526
+ classifications_list (list[dict[str, tuple[str, float | None]]]): a list of dicts, where the key is a symbol
1527
+ of an objective function, and the value is a tuple with a classification and an aspiration
1528
+ or a reservation level, or `None`, depending on the classification. See above for an
1529
+ explanation.
1530
+ current_objective_vector (dict[str, float]): the current objective vector that corresponds to
1531
+ a Pareto optimal solution. The classifications are assumed to been given in respect to
1532
+ this vector.
1533
+ ideal (dict[str, float], optional): ideal point values. If not given, attempt will be made
1534
+ to calculate ideal point from problem.
1535
+ nadir (dict[str, float], optional): nadir point values. If not given, attempt will be made
1536
+ to calculate nadir point from problem.
1537
+ delta (float, optional): a small scalar used to define the utopian point. Defaults to 0.000001.
1538
+ rho (float, optional): a small scalar used in the augmentation term. Defaults to 0.000001.
1539
+
1540
+ Raises:
1541
+ ScalarizationError: any of the given classifications do not define a classification
1542
+ for all the objective functions or any of the given classifications do not allow at
1543
+ least one objective function value to improve and one to worsen.
1544
+
1545
+ Returns:
1546
+ tuple[Problem, str]: a tuple with the copy of the problem with the added
1547
+ scalarization and the symbol of the added scalarization.
1548
+ """
1549
+ # check that classifications have been provided for all objective functions
1550
+ for classifications in classifications_list:
1551
+ if not objective_dict_has_all_symbols(problem, classifications):
1552
+ msg = (
1553
+ f"The given classifications {classifications} do not define "
1554
+ "a classification for all the objective functions."
1555
+ )
1556
+ raise ScalarizationError(msg)
1557
+
1558
+ # check that at least one objective function is allowed to be improved and one is
1559
+ # allowed to worsen
1560
+ if not any(classifications[obj.symbol][0] in ["<", "<="] for obj in problem.objectives) or not any(
1561
+ classifications[obj.symbol][0] in [">=", "0"] for obj in problem.objectives
1562
+ ):
1563
+ msg = (
1564
+ f"The given classifications {classifications} should allow at least one objective function value "
1565
+ "to improve and one to worsen."
1566
+ )
1567
+ raise ScalarizationError(msg)
1568
+
1569
+ # check if ideal point is specified
1570
+ # if not specified, try to calculate corrected ideal point
1571
+ if ideal is not None:
1572
+ ideal_point = ideal
1573
+ elif problem.get_ideal_point() is not None:
1574
+ ideal_point = get_corrected_ideal(problem)
1575
+ else:
1576
+ msg = "Ideal point not defined!"
1577
+ raise ScalarizationError(msg)
1578
+
1579
+ # check if nadir point is specified
1580
+ # if not specified, try to calculate corrected nadir point
1581
+ if nadir is not None:
1582
+ nadir_point = nadir
1583
+ elif problem.get_nadir_point() is not None:
1584
+ nadir_point = get_corrected_nadir(problem)
1585
+ else:
1586
+ msg = "Nadir point not defined!"
1587
+ raise ScalarizationError(msg)
1588
+
1589
+ corrected_current_point = get_corrected_reference_point(problem, current_objective_vector)
1590
+
1591
+ # define the auxiliary variable
1592
+ alpha = Variable(
1593
+ name="alpha",
1594
+ symbol="_alpha",
1595
+ variable_type=VariableTypeEnum.real,
1596
+ lowerbound=-float("Inf"),
1597
+ upperbound=float("Inf"),
1598
+ initial_value=1.0,
1599
+ )
1600
+
1601
+ # calculate the weights
1602
+ weights = {
1603
+ obj.symbol: 1 / (nadir_point[obj.symbol] - (ideal_point[obj.symbol] - delta)) for obj in problem.objectives
1604
+ }
1605
+
1606
+ constraints = []
1607
+
1608
+ for i in range(len(classifications_list)):
1609
+ classifications = classifications_list[i]
1610
+ for obj in problem.objectives:
1611
+ _symbol = obj.symbol
1612
+ match classifications[_symbol]:
1613
+ case ("<", _):
1614
+ max_expr = f"{weights[_symbol]} * ({_symbol}_min - {ideal_point[_symbol]}) - _alpha"
1615
+ constraints.append(
1616
+ Constraint(
1617
+ name=f"Max term linearization for {_symbol}",
1618
+ symbol=f"max_con_{_symbol}_{i+1}",
1619
+ func=max_expr,
1620
+ cons_type=ConstraintTypeEnum.LTE,
1621
+ is_linear=problem.is_linear,
1622
+ is_convex=problem.is_convex,
1623
+ is_twice_differentiable=problem.is_twice_differentiable,
1624
+ )
1625
+ )
1626
+ con_expr = f"{_symbol}_min - {corrected_current_point[_symbol]}"
1627
+ constraints.append(
1628
+ Constraint(
1629
+ name=f"improvement constraint for {_symbol}",
1630
+ symbol=f"{_symbol}_{i+1}_lt",
1631
+ func=con_expr,
1632
+ cons_type=ConstraintTypeEnum.LTE,
1633
+ is_linear=problem.is_linear,
1634
+ is_convex=problem.is_convex,
1635
+ is_twice_differentiable=problem.is_twice_differentiable,
1636
+ )
1637
+ )
1638
+ case ("<=", aspiration):
1639
+ # if obj is to be maximized, then the current aspiration value needs to be multiplied by -1
1640
+ max_expr = (
1641
+ f"{weights[_symbol]} * ({_symbol}_min - {aspiration * -1 if obj.maximize else aspiration}) "
1642
+ "- _alpha"
1643
+ )
1644
+ constraints.append(
1645
+ Constraint(
1646
+ name=f"Max term linearization for {_symbol}",
1647
+ symbol=f"max_con_{_symbol}_{i+1}",
1648
+ func=max_expr,
1649
+ cons_type=ConstraintTypeEnum.LTE,
1650
+ is_linear=problem.is_linear,
1651
+ is_convex=problem.is_convex,
1652
+ is_twice_differentiable=problem.is_twice_differentiable,
1653
+ )
1654
+ )
1655
+ con_expr = f"{_symbol}_min - {corrected_current_point[_symbol]}"
1656
+ constraints.append(
1657
+ Constraint(
1658
+ name=f"improvement until constraint for {_symbol}",
1659
+ symbol=f"{_symbol}_{i+1}_lte",
1660
+ func=con_expr,
1661
+ cons_type=ConstraintTypeEnum.LTE,
1662
+ is_linear=problem.is_linear,
1663
+ is_convex=problem.is_convex,
1664
+ is_twice_differentiable=problem.is_twice_differentiable,
1665
+ )
1666
+ )
1667
+ case ("=", _):
1668
+ con_expr = f"{_symbol}_min - {corrected_current_point[_symbol]}"
1669
+ constraints.append(
1670
+ Constraint(
1671
+ name=f"Stay at least as good constraint for {_symbol}",
1672
+ symbol=f"{_symbol}_{i+1}_eq",
1673
+ func=con_expr,
1674
+ cons_type=ConstraintTypeEnum.LTE,
1675
+ is_linear=problem.is_linear,
1676
+ is_convex=problem.is_convex,
1677
+ is_twice_differentiable=problem.is_twice_differentiable,
1678
+ )
1679
+ )
1680
+ case (">=", reservation):
1681
+ # if obj is to be maximized, then the current reservation value needs to be multiplied by -1
1682
+ con_expr = f"{_symbol}_min - {-1 * reservation if obj.maximize else reservation}"
1683
+ constraints.append(
1684
+ Constraint(
1685
+ name=f"Worsen until constraint for {_symbol}",
1686
+ symbol=f"{_symbol}_{i+1}_gte",
1687
+ func=con_expr,
1688
+ cons_type=ConstraintTypeEnum.LTE,
1689
+ is_linear=problem.is_linear,
1690
+ is_convex=problem.is_convex,
1691
+ is_twice_differentiable=problem.is_twice_differentiable,
1692
+ )
1693
+ )
1694
+ case ("0", _):
1695
+ # not relevant for this scalarization
1696
+ pass
1697
+ case (c, _):
1698
+ msg = (
1699
+ f"Warning! The classification {c} was supplied, but it is not supported."
1700
+ "Must be one of ['<', '<=', '0', '=', '>=']"
1701
+ )
1702
+
1703
+ # form the augmentation term
1704
+ aug_exprs = []
1705
+ for _ in range(len(classifications_list)):
1706
+ aug_expr = " + ".join([f"({weights[obj.symbol]} * {obj.symbol}_min)" for obj in problem.objectives])
1707
+ aug_exprs.append(aug_expr)
1708
+ aug_exprs = " + ".join(aug_exprs)
1709
+
1710
+ func = f"_alpha + {rho} * ({aug_exprs})"
1711
+ scalarization_function = ScalarizationFunction(
1712
+ name="Differentiable NIMBUS scalarization objective function for multiple decision makers",
1713
+ symbol=symbol,
1714
+ func=func,
1715
+ is_linear=problem.is_linear,
1716
+ is_convex=problem.is_convex,
1717
+ is_twice_differentiable=problem.is_twice_differentiable,
1718
+ )
1719
+ _problem = problem.add_variables([alpha])
1720
+ _problem = _problem.add_scalarization(scalarization_function)
1721
+ return _problem.add_constraints(constraints), symbol
1722
+
1723
+
1724
+ def add_stom_sf_diff(
1725
+ problem: Problem,
1726
+ symbol: str,
1727
+ reference_point: dict[str, float],
1728
+ ideal: dict[str, float] | None = None,
1729
+ rho: float = 1e-6,
1730
+ delta: float = 1e-6,
1731
+ ) -> tuple[Problem, str]:
1732
+ r"""Adds the differentiable variant of the STOM scalarizing function.
1733
+
1734
+ \begin{align*}
1735
+ \min \quad & \alpha + \rho \sum_{i=1}^k \frac{f_i(\mathbf{x})}{\bar{z}_i - z_i^{\star\star}} \\
1736
+ \text{s.t.} \quad & \frac{f_i(\mathbf{x}) - z_i^{\star\star}}{\bar{z}_i
1737
+ - z_i^{\star\star}} - \alpha \leq 0 \quad & \forall i = 1,\dots,k\\
1738
+ & \mathbf{x} \in S,
1739
+ \end{align*}
1740
+
1741
+ where $f_i$ are objective functions, $z_i^{\star\star} = z_i^\star - \delta$ is
1742
+ a component of the utopian point, $\bar{z}_i$ is a component of the reference point,
1743
+ $\rho$ and $\delta$ are small scalar values, $S$ is the feasible solution
1744
+ space of the original problem, and $\alpha$ is an auxiliary variable.
1745
+
1746
+ References:
1747
+ H. Nakayama, Y. Sawaragi, Satisficing trade-off method for
1748
+ multiobjective programming, in: M. Grauer, A.P. Wierzbicki (Eds.),
1749
+ Interactive Decision Analysis, Springer Verlag, Berlin, 1984, pp.
1750
+ 113-122.
1751
+
1752
+ Args:
1753
+ problem (Problem): the problem the scalarization is added to.
1754
+ symbol (str): the symbol given to the added scalarization.
1755
+ reference_point (dict[str, float]): a dict with keys corresponding to objective
1756
+ function symbols and values to reference point components, i.e.,
1757
+ aspiration levels.
1758
+ ideal (dict[str, float], optional): ideal point values. If not given, attempt will be made
1759
+ to calculate ideal point from problem.
1760
+ rho (float, optional): a small scalar value to scale the sum in the objective
1761
+ function of the scalarization. Defaults to 1e-6.
1762
+ delta (float, optional): a small scalar value to define the utopian point. Defaults to 1e-6.
1763
+
1764
+ Returns:
1765
+ tuple[Problem, str]: a tuple with the copy of the problem with the added
1766
+ scalarization and the symbol of the added scalarization.
1767
+ """
1768
+ # check reference point
1769
+ if not objective_dict_has_all_symbols(problem, reference_point):
1770
+ msg = f"The give reference point {reference_point} is missing value for one or more objectives."
1771
+ raise ScalarizationError(msg)
1772
+
1773
+ # check if ideal point is specified
1774
+ # if not specified, try to calculate corrected ideal point
1775
+ if ideal is not None:
1776
+ ideal_point = ideal
1777
+ elif problem.get_ideal_point() is not None:
1778
+ ideal_point = get_corrected_ideal(problem)
1779
+ else:
1780
+ msg = "Ideal point not defined!"
1781
+ raise ScalarizationError(msg)
1782
+
1783
+ corrected_rp = get_corrected_reference_point(problem, reference_point)
1784
+
1785
+ # define the auxiliary variable
1786
+ alpha = Variable(
1787
+ name="alpha",
1788
+ symbol="_alpha",
1789
+ variable_type=VariableTypeEnum.real,
1790
+ lowerbound=-float("Inf"),
1791
+ upperbound=float("Inf"),
1792
+ initial_value=1.0,
1793
+ )
1794
+
1795
+ # define the objective function of the scalarization
1796
+ aug_expr = " + ".join(
1797
+ [
1798
+ f"{obj.symbol}_min / ({reference_point[obj.symbol]} - {ideal_point[obj.symbol] - delta})"
1799
+ for obj in problem.objectives
1800
+ ]
1801
+ )
1802
+
1803
+ target_expr = f"_alpha + {rho}*" + f"({aug_expr})"
1804
+ scalarization = ScalarizationFunction(
1805
+ name="STOM scalarization objective function",
1806
+ symbol=symbol,
1807
+ func=target_expr,
1808
+ is_twice_differentiable=problem.is_twice_differentiable,
1809
+ is_linear=problem.is_linear,
1810
+ is_convex=problem.is_convex,
1811
+ )
1812
+
1813
+ constraints = []
1814
+
1815
+ for obj in problem.objectives:
1816
+ expr = (
1817
+ f"({obj.symbol}_min - {ideal_point[obj.symbol] - delta}) / "
1818
+ f"({corrected_rp[obj.symbol] - (ideal_point[obj.symbol] - delta)}) - _alpha"
1819
+ )
1820
+ constraints.append(
1821
+ Constraint(
1822
+ name=f"Max constraint for {obj.symbol}",
1823
+ symbol=f"{obj.symbol}_maxcon",
1824
+ func=expr,
1825
+ cons_type=ConstraintTypeEnum.LTE,
1826
+ is_twice_differentiable=obj.is_twice_differentiable,
1827
+ is_linear=obj.is_linear,
1828
+ is_convex=obj.is_convex,
1829
+ )
1830
+ )
1831
+
1832
+ _problem = problem.add_variables([alpha])
1833
+ _problem = _problem.add_scalarization(scalarization)
1834
+ return _problem.add_constraints(constraints), symbol
1835
+
1836
+
1837
+ def add_stom_sf_nondiff(
1838
+ problem: Problem,
1839
+ symbol: str,
1840
+ reference_point: dict[str, float],
1841
+ ideal: dict[str, float] | None = None,
1842
+ rho: float = 1e-6,
1843
+ delta: float = 1e-6,
1844
+ ) -> tuple[Problem, str]:
1845
+ r"""Adds the non-differentiable variant of the STOM scalarizing function.
1846
+
1847
+ \begin{align*}
1848
+ \underset{\mathbf{x}}{\min} \quad & \underset{i=1,\dots,k}{\max}\left[
1849
+ \frac{f_i(\mathbf{x}) - z_i^{\star\star}}{\bar{z}_i - z_i^{\star\star}}
1850
+ \right]
1851
+ + \rho \sum_{i=1}^k \frac{f_i(\mathbf{x})}{\bar{z}_i - z_i^{\star\star}} \\
1852
+ \text{s.t.}\quad & \mathbf{x} \in S,
1853
+ \end{align*}
1854
+
1855
+ where $f_i$ are objective functions, $z_i^{\star\star} = z_i^\star - \delta$ is
1856
+ a component of the utopian point, $\bar{z}_i$ is a component of the reference point,
1857
+ $\rho$ and $\delta$ are small scalar values, and $S$ is the feasible solution
1858
+ space of the original problem.
1859
+
1860
+ References:
1861
+ H. Nakayama, Y. Sawaragi, Satisficing trade-off method for
1862
+ multiobjective programming, in: M. Grauer, A.P. Wierzbicki (Eds.),
1863
+ Interactive Decision Analysis, Springer Verlag, Berlin, 1984, pp.
1864
+ 113-122.
1865
+
1866
+ Args:
1867
+ problem (Problem): the problem the scalarization is added to.
1868
+ symbol (str): the symbol given to the added scalarization.
1869
+ reference_point (dict[str, float]): a dict with keys corresponding to objective
1870
+ function symbols and values to reference point components, i.e.,
1871
+ aspiration levels.
1872
+ ideal (dict[str, float], optional): ideal point values. If not given, attempt will be made
1873
+ to calculate ideal point from problem.
1874
+ rho (float, optional): a small scalar value to scale the sum in the objective
1875
+ function of the scalarization. Defaults to 1e-6.
1876
+ delta (float, optional): a small scalar value to define the utopian point. Defaults to 1e-6.
1877
+
1878
+ Returns:
1879
+ tuple[Problem, str]: a tuple with the copy of the problem with the added
1880
+ scalarization and the symbol of the added scalarization.
1881
+ """
1882
+ # check reference point
1883
+ if not objective_dict_has_all_symbols(problem, reference_point):
1884
+ msg = f"The give reference point {reference_point} is missing value for one or more objectives."
1885
+ raise ScalarizationError(msg)
1886
+
1887
+ # check if ideal point is specified
1888
+ # if not specified, try to calculate corrected ideal point
1889
+ if ideal is not None:
1890
+ ideal_point = ideal
1891
+ elif problem.get_ideal_point() is not None:
1892
+ ideal_point = get_corrected_ideal(problem)
1893
+ else:
1894
+ msg = "Ideal point not defined!"
1895
+ raise ScalarizationError(msg)
1896
+
1897
+ corrected_rp = get_corrected_reference_point(problem, reference_point)
1898
+
1899
+ # define the objective function of the scalarization
1900
+ max_expr = ", ".join(
1901
+ [
1902
+ (
1903
+ f"({obj.symbol}_min - {ideal_point[obj.symbol] - delta}) / "
1904
+ f"({corrected_rp[obj.symbol]} - {ideal_point[obj.symbol] - delta})"
1905
+ )
1906
+ for obj in problem.objectives
1907
+ ]
1908
+ )
1909
+ aug_expr = " + ".join(
1910
+ [
1911
+ f"{obj.symbol}_min / ({reference_point[obj.symbol]} - {ideal_point[obj.symbol] - delta})"
1912
+ for obj in problem.objectives
1913
+ ]
1914
+ )
1915
+
1916
+ target_expr = f"{Op.MAX}({max_expr}) + {rho}*" + f"({aug_expr})"
1917
+ scalarization = ScalarizationFunction(
1918
+ name="STOM scalarization objective function",
1919
+ symbol=symbol,
1920
+ func=target_expr,
1921
+ is_linear=False,
1922
+ is_convex=False,
1923
+ is_twice_differentiable=False,
1924
+ )
1925
+
1926
+ return problem.add_scalarization(scalarization), symbol
1927
+
1928
+
1929
+ def add_group_stom_sf(
1930
+ problem: Problem,
1931
+ symbol: str,
1932
+ reference_points: list[dict[str, float]],
1933
+ ideal: dict[str, float] | None = None,
1934
+ rho: float = 1e-6,
1935
+ delta: float = 1e-6,
1936
+ ) -> tuple[Problem, str]:
1937
+ r"""Adds the multiple decision maker variant of the STOM scalarizing function.
1938
+
1939
+ The scalarization function is defined as follows:
1940
+
1941
+ \begin{align}
1942
+ &\mbox{minimize} &&\max_{i,d} [w_{id}(f_{id}(\mathbf{x})-z^{uto}_{id})] +
1943
+ \rho \sum^k_{i=1} \sum^{n_d}_{d=1} w_{id}f_{id}(\mathbf{x}) \\
1944
+ &\mbox{subject to} &&\mathbf{x} \in \mathbf{X},
1945
+ \end{align}
1946
+
1947
+ where $w_{id} = \frac{1}{\overline{z}_{id} - z^{uto}_{id}}$.
1948
+
1949
+ Args:
1950
+ problem (Problem): the problem the scalarization is added to.
1951
+ symbol (str): the symbol given to the added scalarization.
1952
+ reference_points (list[dict[str, float]]): a list of dicts with keys corresponding to objective
1953
+ function symbols and values to reference point components, i.e.,
1954
+ aspiration levels.
1955
+ ideal (dict[str, float], optional): ideal point values. If not given, attempt will be made
1956
+ to calculate ideal point from problem.
1957
+ rho (float, optional): a small scalar value to scale the sum in the objective
1958
+ function of the scalarization. Defaults to 1e-6.
1959
+ delta (float, optional): a small scalar value to define the utopian point. Defaults to 1e-6.
1960
+
1961
+ Raises:
1962
+ ScalarizationError: there are missing elements in any reference point.
1963
+
1964
+ Returns:
1965
+ tuple[Problem, str]: a tuple with the copy of the problem with the added
1966
+ scalarization and the symbol of the added scalarization.
1967
+ """
1968
+ # check reference points
1969
+ for reference_point in reference_points:
1970
+ if not objective_dict_has_all_symbols(problem, reference_point):
1971
+ msg = f"The give reference point {reference_point} is missing value for one or more objectives."
1972
+ raise ScalarizationError(msg)
1973
+
1974
+ # check if ideal point is specified
1975
+ # if not specified, try to calculate corrected ideal point
1976
+ if ideal is not None:
1977
+ ideal_point = ideal
1978
+ elif problem.get_ideal_point() is not None:
1979
+ ideal_point = get_corrected_ideal(problem)
1980
+ else:
1981
+ msg = "Ideal point not defined!"
1982
+ raise ScalarizationError(msg)
1983
+
1984
+ # calculate the weights
1985
+ weights = []
1986
+ for reference_point in reference_points:
1987
+ corrected_rp = get_corrected_reference_point(problem, reference_point)
1988
+ weights.append(
1989
+ {
1990
+ obj.symbol: 1 / (corrected_rp[obj.symbol] - (ideal_point[obj.symbol] - delta))
1991
+ for obj in problem.objectives
1992
+ }
1993
+ )
1994
+
1995
+ # form the max term
1996
+ max_terms = []
1997
+ for i in range(len(reference_points)):
1998
+ for obj in problem.objectives:
1999
+ max_terms.append(f"{weights[i][obj.symbol]} * ({obj.symbol}_min - {ideal_point[obj.symbol] - delta})")
2000
+ max_terms = ", ".join(max_terms)
2001
+
2002
+ # form the augmentation term
2003
+ aug_exprs = []
2004
+ for i in range(len(reference_points)):
2005
+ aug_expr = " + ".join([f"({weights[i][obj.symbol]} * {obj.symbol}_min)" for obj in problem.objectives])
2006
+ aug_exprs.append(aug_expr)
2007
+ aug_exprs = " + ".join(aug_exprs)
2008
+
2009
+ func = f"{Op.MAX}({max_terms}) + {rho}*({aug_exprs})"
2010
+ scalarization = ScalarizationFunction(
2011
+ name="STOM scalarization objective function for multiple decision makers",
2012
+ symbol=symbol,
2013
+ func=func,
2014
+ is_linear=problem.is_linear,
2015
+ is_convex=problem.is_convex,
2016
+ is_twice_differentiable=False,
2017
+ )
2018
+ return problem.add_scalarization(scalarization), symbol
2019
+
2020
+
2021
+ def add_group_stom_sf_diff(
2022
+ problem: Problem,
2023
+ symbol: str,
2024
+ reference_points: list[dict[str, float]],
2025
+ ideal: dict[str, float] | None = None,
2026
+ rho: float = 1e-6,
2027
+ delta: float = 1e-6,
2028
+ ) -> tuple[Problem, str]:
2029
+ r"""Adds the differentiable variant of the multiple decision maker variant of the STOM scalarizing function.
2030
+
2031
+ The scalarization function is defined as follows:
2032
+
2033
+ \begin{align}
2034
+ &\mbox{minimize} && \alpha +
2035
+ \rho \sum^k_{i=1} \sum^{n_d}_{d=1} w_{id}f_{id}(\mathbf{x}) \\
2036
+ &\mbox{subject to} && w_{id}(f_{id}(\mathbf{x})-z^{uto}_{id}) - \alpha \leq 0,\\
2037
+ &&&\mathbf{x} \in \mathbf{X},
2038
+ \end{align}
2039
+
2040
+ where $w_{id} = \frac{1}{\overline{z}_{id} - z^{uto}_{id}}$.
2041
+
2042
+ Args:
2043
+ problem (Problem): the problem the scalarization is added to.
2044
+ symbol (str): the symbol given to the added scalarization.
2045
+ reference_points (list[dict[str, float]]): a list of dicts with keys corresponding to objective
2046
+ function symbols and values to reference point components, i.e.,
2047
+ aspiration levels.
2048
+ ideal (dict[str, float], optional): ideal point values. If not given, attempt will be made
2049
+ to calculate ideal point from problem.
2050
+ rho (float, optional): a small scalar value to scale the sum in the objective
2051
+ function of the scalarization. Defaults to 1e-6.
2052
+ delta (float, optional): a small scalar value to define the utopian point. Defaults to 1e-6.
2053
+
2054
+ Raises:
2055
+ ScalarizationError: there are missing elements in any reference point.
2056
+
2057
+ Returns:
2058
+ tuple[Problem, str]: a tuple with the copy of the problem with the added
2059
+ scalarization and the symbol of the added scalarization.
2060
+ """
2061
+ # check reference points
2062
+ for reference_point in reference_points:
2063
+ if not objective_dict_has_all_symbols(problem, reference_point):
2064
+ msg = f"The give reference point {reference_point} is missing value for one or more objectives."
2065
+ raise ScalarizationError(msg)
2066
+
2067
+ # check if ideal point is specified
2068
+ # if not specified, try to calculate corrected ideal point
2069
+ if ideal is not None:
2070
+ ideal_point = ideal
2071
+ elif problem.get_ideal_point() is not None:
2072
+ ideal_point = get_corrected_ideal(problem)
2073
+ else:
2074
+ msg = "Ideal point not defined!"
2075
+ raise ScalarizationError(msg)
2076
+
2077
+ # define the auxiliary variable
2078
+ alpha = Variable(
2079
+ name="alpha",
2080
+ symbol="_alpha",
2081
+ variable_type=VariableTypeEnum.real,
2082
+ lowerbound=-float("Inf"),
2083
+ upperbound=float("Inf"),
2084
+ initial_value=1.0,
2085
+ )
2086
+
2087
+ # calculate the weights
2088
+ weights = []
2089
+ for reference_point in reference_points:
2090
+ corrected_rp = get_corrected_reference_point(problem, reference_point)
2091
+ weights.append(
2092
+ {
2093
+ obj.symbol: 1 / (corrected_rp[obj.symbol] - (ideal_point[obj.symbol] - delta))
2094
+ for obj in problem.objectives
2095
+ }
2096
+ )
2097
+
2098
+ # form the max term
2099
+ con_terms = []
2100
+ for i in range(len(reference_points)):
2101
+ rp = {}
2102
+ for obj in problem.objectives:
2103
+ rp[obj.symbol] = (
2104
+ f"{weights[i][obj.symbol]} * ({obj.symbol}_min - {ideal_point[obj.symbol] - delta}) - _alpha"
2105
+ )
2106
+ con_terms.append(rp)
2107
+
2108
+ # form the augmentation term
2109
+ aug_exprs = []
2110
+ for i in range(len(reference_points)):
2111
+ aug_expr = " + ".join([f"({weights[i][obj.symbol]} * {obj.symbol}_min)" for obj in problem.objectives])
2112
+ aug_exprs.append(aug_expr)
2113
+ aug_exprs = " + ".join(aug_exprs)
2114
+
2115
+ constraints = []
2116
+ # loop to create a constraint for every objective of every reference point given
2117
+ for i in range(len(reference_points)):
2118
+ for obj in problem.objectives:
2119
+ # since we are subtracting a constant value, the linearity, convexity,
2120
+ # and differentiability of the objective function, and hence the
2121
+ # constraint, should not change.
2122
+ constraints.append(
2123
+ Constraint(
2124
+ name=f"Constraint for {obj.symbol}",
2125
+ symbol=f"{obj.symbol}_con_{i+1}",
2126
+ func=con_terms[i][obj.symbol],
2127
+ cons_type=ConstraintTypeEnum.LTE,
2128
+ is_linear=obj.is_linear,
2129
+ is_convex=obj.is_convex,
2130
+ is_twice_differentiable=obj.is_twice_differentiable,
2131
+ )
2132
+ )
2133
+
2134
+ func = f"_alpha + {rho}*({aug_exprs})"
2135
+ scalarization = ScalarizationFunction(
2136
+ name="Differentiable STOM scalarization objective function for multiple decision makers",
2137
+ symbol=symbol,
2138
+ func=func,
2139
+ is_linear=problem.is_linear,
2140
+ is_convex=problem.is_convex,
2141
+ is_twice_differentiable=problem.is_twice_differentiable,
2142
+ )
2143
+ _problem = problem.add_variables([alpha])
2144
+ _problem = _problem.add_scalarization(scalarization)
2145
+ return _problem.add_constraints(constraints), symbol
2146
+
2147
+
2148
+ def add_guess_sf_diff(
2149
+ problem: Problem,
2150
+ symbol: str,
2151
+ reference_point: dict[str, float],
2152
+ ideal: dict[str, float] | None = None,
2153
+ nadir: dict[str, float] | None = None,
2154
+ rho: float = 1e-6,
2155
+ delta: float = 1e-6,
2156
+ ) -> tuple[Problem, str]:
2157
+ r"""Adds the differentiable variant of the GUESS scalarizing function.
2158
+
2159
+ \begin{align*}
2160
+ \min \quad & \alpha + \rho \sum_{i=1}^k \frac{f_i(\mathbf{x})}{d_i} \\
2161
+ \text{s.t.} \quad & \frac{f_i(\mathbf{x}) - z_i^{\star\star}}{\bar{z}_i
2162
+ - z_i^{\star\star}} - \alpha \leq 0 \quad & \forall i \notin I^{\diamond},\\
2163
+ & d_i =
2164
+ \begin{cases}
2165
+ z^\text{nad}_i - \bar{z}_i,\quad \forall i \notin I^\diamond,\\
2166
+ z^\text{nad}_i - z^{\star\star}_i,\quad \forall i \in I^\diamond,\\
2167
+ \end{cases}\\
2168
+ & \mathbf{x} \in S,
2169
+ \end{align*}
2170
+
2171
+ where $f_i$ are objective functions, $z_i^{\star\star} = z_i^\star - \delta$ is
2172
+ a component of the utopian point, $\bar{z}_i$ is a component of the reference point,
2173
+ $\rho$ and $\delta$ are small scalar values, $S$ is the feasible solution
2174
+ space of the original problem, and $\alpha$ is an auxiliary variable. The index
2175
+ set $I^\diamond$ represents objective vectors whose values are free to change. The indices
2176
+ belonging to this set are interpreted as those objective vectors whose components in
2177
+ the reference point is set to be the the respective nadir point component of the problem.
2178
+
2179
+ References:
2180
+ Buchanan, J. T. (1997). A naive approach for solving MCDM problems: The
2181
+ GUESS method. Journal of the Operational Research Society, 48, 202-206.
2182
+
2183
+ Args:
2184
+ problem (Problem): the problem the scalarization is added to.
2185
+ symbol (str): the symbol given to the added scalarization.
2186
+ reference_point (dict[str, float]): a dict with keys corresponding to objective
2187
+ function symbols and values to reference point components, i.e.,
2188
+ aspiration levels.
2189
+ ideal (dict[str, float], optional): ideal point values. If not given, attempt will be made
2190
+ to calculate ideal point from problem.
2191
+ nadir (dict[str, float], optional): nadir point values. If not given, attempt will be made
2192
+ to calculate nadir point from problem.
2193
+ rho (float, optional): a small scalar value to scale the sum in the objective
2194
+ function of the scalarization. Defaults to 1e-6.
2195
+ delta (float, optional): a small scalar to define the utopian point. Defaults to 1e-6.
2196
+
2197
+ Returns:
2198
+ tuple[Problem, str]: a tuple with the copy of the problem with the added
2199
+ scalarization and the symbol of the added scalarization.
2200
+ """
2201
+ # check reference point
2202
+ if not objective_dict_has_all_symbols(problem, reference_point):
2203
+ msg = f"The give reference point {reference_point} is missing value for one or more objectives."
2204
+ raise ScalarizationError(msg)
2205
+
2206
+ # check if ideal point is specified
2207
+ # if not specified, try to calculate corrected ideal point
2208
+ if ideal is not None:
2209
+ ideal_point = ideal
2210
+ elif problem.get_ideal_point() is not None:
2211
+ ideal_point = get_corrected_ideal(problem)
2212
+ else:
2213
+ msg = "Ideal point not defined!"
2214
+ raise ScalarizationError(msg)
2215
+
2216
+ # check if nadir point is specified
2217
+ # if not specified, try to calculate corrected nadir point
2218
+ if nadir is not None:
2219
+ nadir_point = nadir
2220
+ elif problem.get_nadir_point() is not None:
2221
+ nadir_point = get_corrected_nadir(problem)
2222
+ else:
2223
+ msg = "Nadir point not defined!"
2224
+ raise ScalarizationError(msg)
2225
+
2226
+ corrected_rp = get_corrected_reference_point(problem, reference_point)
2227
+
2228
+ # the indices that are free to change, set if component of reference point
2229
+ # has the corresponding nadir value, or if it is greater than the nadir value
2230
+ free_to_change = [
2231
+ sym
2232
+ for sym in corrected_rp
2233
+ if np.isclose(corrected_rp[sym], nadir_point[sym]) or corrected_rp[sym] > nadir_point[sym]
2234
+ ]
2235
+
2236
+ # define the auxiliary variable
2237
+ alpha = Variable(
2238
+ name="alpha",
2239
+ symbol="_alpha",
2240
+ variable_type=VariableTypeEnum.real,
2241
+ lowerbound=-float("Inf"),
2242
+ upperbound=float("Inf"),
2243
+ initial_value=1.0,
2244
+ )
2245
+
2246
+ # define the objective function of the scalarization
2247
+ aug_expr = " + ".join(
2248
+ [
2249
+ (
2250
+ f"{obj.symbol}_min / ({nadir_point[obj.symbol]} - "
2251
+ f"{reference_point[obj.symbol] if obj.symbol not in free_to_change else ideal_point[obj.symbol] - delta})" # noqa: E501
2252
+ )
2253
+ for obj in problem.objectives
2254
+ ]
2255
+ )
2256
+
2257
+ target_expr = f"_alpha + {rho}*" + f"({aug_expr})"
2258
+ scalarization = ScalarizationFunction(
2259
+ name="GUESS scalarization objective function",
2260
+ symbol=symbol,
2261
+ func=target_expr,
2262
+ is_convex=problem.is_convex,
2263
+ is_linear=problem.is_linear,
2264
+ is_twice_differentiable=problem.is_twice_differentiable,
2265
+ )
2266
+
2267
+ constraints = []
2268
+
2269
+ for obj in problem.objectives:
2270
+ if obj.symbol in free_to_change:
2271
+ # if free to change, then do not add a constraint
2272
+ continue
2273
+
2274
+ # not free to change, add constraint
2275
+ expr = (
2276
+ f"({obj.symbol}_min - {nadir_point[obj.symbol]}) / "
2277
+ f"({nadir_point[obj.symbol]} - {corrected_rp[obj.symbol]}) - _alpha"
2278
+ )
2279
+
2280
+ constraints.append(
2281
+ Constraint(
2282
+ name=f"Constraint for {obj.symbol}",
2283
+ symbol=f"{obj.symbol}_con",
2284
+ func=expr,
2285
+ cons_type=ConstraintTypeEnum.LTE,
2286
+ is_linear=obj.is_linear,
2287
+ is_convex=obj.is_convex,
2288
+ is_twice_differentiable=obj.is_twice_differentiable,
2289
+ )
2290
+ )
2291
+
2292
+ _problem = problem.add_variables([alpha])
2293
+ _problem = _problem.add_scalarization(scalarization)
2294
+ return _problem.add_constraints(constraints), symbol
2295
+
2296
+
2297
+ def add_guess_sf_nondiff(
2298
+ problem: Problem,
2299
+ symbol: str,
2300
+ reference_point: dict[str, float],
2301
+ ideal: dict[str, float] | None = None,
2302
+ nadir: dict[str, float] | None = None,
2303
+ rho: float = 1e-6,
2304
+ delta: float = 1e-6,
2305
+ ) -> tuple[Problem, str]:
2306
+ r"""Adds the non-differentiable variant of the GUESS scalarizing function.
2307
+
2308
+ \begin{align*}
2309
+ \underset{\mathbf{x}}{\min}\quad & \underset{i \notin I^\diamond}{\max}
2310
+ \left[
2311
+ \frac{f_i(\mathbf{x}) - z_i^{\star\star}}{\bar{z}_i - z_i^{\star\star}}
2312
+ \right]
2313
+ + \rho \sum_{j=1}^k \frac{f_j(\mathbf{x})}{d_j},
2314
+ \quad & \\
2315
+ \text{s.t.}\quad
2316
+ & d_j =
2317
+ \begin{cases}
2318
+ z^\text{nad}_j - \bar{z}_j,\quad \forall j \notin I^\diamond,\\
2319
+ z^\text{nad}_j - z^{\star\star}_j,\quad \forall j \in I^\diamond,\\
2320
+ \end{cases}\\
2321
+ & \mathbf{x} \in S,
2322
+ \end{align*}
2323
+
2324
+ where $f_{i/j}$ are objective functions, $z_{i/j}^{\star\star} =
2325
+ z_{i/j}^\star - \delta$ is a component of the utopian point, $\bar{z}_{i/j}$
2326
+ is a component of the reference point, $\rho$ and $\delta$ are small scalar
2327
+ values, and $S$ is the feasible solution space of the original problem. The
2328
+ index set $I^\diamond$ represents objective vectors whose values are free to
2329
+ change. The indices belonging to this set are interpreted as those objective
2330
+ vectors whose components in the reference point is set to be the the
2331
+ respective nadir point component of the problem.
2332
+
2333
+ References:
2334
+ Buchanan, J. T. (1997). A naive approach for solving MCDM problems: The
2335
+ GUESS method. Journal of the Operational Research Society, 48, 202-206.
2336
+
2337
+ Args:
2338
+ problem (Problem): the problem the scalarization is added to.
2339
+ symbol (str): the symbol given to the added scalarization.
2340
+ reference_point (dict[str, float]): a dict with keys corresponding to objective
2341
+ function symbols and values to reference point components, i.e.,
2342
+ aspiration levels.
2343
+ ideal (dict[str, float], optional): ideal point values. If not given, attempt will be made
2344
+ to calculate ideal point from problem.
2345
+ nadir (dict[str, float], optional): nadir point values. If not given, attempt will be made
2346
+ to calculate nadir point from problem.
2347
+ rho (float, optional): a small scalar value to scale the sum in the objective
2348
+ function of the scalarization. Defaults to 1e-6.
2349
+ delta (float, optional): a small scalar to define the utopian point. Defaults to 1e-6.
2350
+
2351
+ Returns:
2352
+ tuple[Problem, str]: a tuple with the copy of the problem with the added
2353
+ scalarization and the symbol of the added scalarization.
2354
+ """
2355
+ # check reference point
2356
+ if not objective_dict_has_all_symbols(problem, reference_point):
2357
+ msg = f"The give reference point {reference_point} is missing value for one or more objectives."
2358
+ raise ScalarizationError(msg)
2359
+
2360
+ # check if ideal point is specified
2361
+ # if not specified, try to calculate corrected ideal point
2362
+ if ideal is not None:
2363
+ ideal_point = ideal
2364
+ elif problem.get_ideal_point() is not None:
2365
+ ideal_point = get_corrected_ideal(problem)
2366
+ else:
2367
+ msg = "Ideal point not defined!"
2368
+ raise ScalarizationError(msg)
2369
+
2370
+ # check if nadir point is specified
2371
+ # if not specified, try to calculate corrected nadir point
2372
+ if nadir is not None:
2373
+ nadir_point = nadir
2374
+ elif problem.get_nadir_point() is not None:
2375
+ nadir_point = get_corrected_nadir(problem)
2376
+ else:
2377
+ msg = "Nadir point not defined!"
2378
+ raise ScalarizationError(msg)
2379
+
2380
+ corrected_rp = get_corrected_reference_point(problem, reference_point)
2381
+
2382
+ # the indices that are free to change, set if component of reference point
2383
+ # has the corresponding nadir value, or if it is greater than the nadir value
2384
+ free_to_change = [
2385
+ sym
2386
+ for sym in corrected_rp
2387
+ if np.isclose(corrected_rp[sym], nadir_point[sym]) or corrected_rp[sym] > nadir_point[sym]
2388
+ ]
2389
+
2390
+ # define the max expression of the scalarization
2391
+ # if the objective symbol belongs to the class I^diamond, then do not add it
2392
+ # to the max expression
2393
+ max_expr = ", ".join(
2394
+ [
2395
+ (
2396
+ f"({obj.symbol}_min - {(ideal_point[obj.symbol] - delta)}) / "
2397
+ f"({reference_point[obj.symbol]} - {(ideal_point[obj.symbol] - delta)})"
2398
+ )
2399
+ for obj in problem.objectives
2400
+ if obj.symbol not in free_to_change
2401
+ ]
2402
+ )
2403
+
2404
+ # define the augmentation term
2405
+ aug_expr = " + ".join(
2406
+ [
2407
+ (
2408
+ f"{obj.symbol}_min / ({nadir_point[obj.symbol]} - "
2409
+ f"{reference_point[obj.symbol] if obj.symbol not in free_to_change else ideal_point[obj.symbol] - delta})" # noqa: E501
2410
+ )
2411
+ for obj in problem.objectives
2412
+ ]
2413
+ )
2414
+
2415
+ target_expr = f"{Op.MAX}({max_expr}) + {rho}*({aug_expr})"
2416
+ scalarization = ScalarizationFunction(
2417
+ name="GUESS scalarization objective function",
2418
+ symbol=symbol,
2419
+ func=target_expr,
2420
+ is_linear=False,
2421
+ is_convex=False,
2422
+ is_twice_differentiable=False,
2423
+ )
2424
+
2425
+ return problem.add_scalarization(scalarization), symbol
2426
+
2427
+
2428
+ def add_group_guess_sf(
2429
+ problem: Problem,
2430
+ symbol: str,
2431
+ reference_points: list[dict[str, float]],
2432
+ nadir: dict[str, float] | None = None,
2433
+ rho: float = 1e-6,
2434
+ delta: float = 1e-6,
2435
+ ) -> tuple[Problem, str]:
2436
+ r"""Adds the non-differentiable variant of the multiple decision maker variant of the GUESS scalarizing function.
2437
+
2438
+ The scalarization function is defined as follows:
2439
+
2440
+ \begin{align}
2441
+ &\mbox{minimize} &&\max_{i,d} [w_{id}(f_{id}(\mathbf{x})-z^{nad}_{id})] +
2442
+ \rho \sum^k_{i=1} \sum^{n_d}_{d=1} w_{id}f_{id}(\mathbf{x}) \\
2443
+ &\mbox{subject to} &&\mathbf{x} \in \mathbf{X},
2444
+ \end{align}
2445
+
2446
+ where $w_{id} = \frac{1}{z^{nad}_{id} - \overline{z}_{id}}$.
2447
+
2448
+ Args:
2449
+ problem (Problem): the problem the scalarization is added to.
2450
+ symbol (str): the symbol given to the added scalarization.
2451
+ reference_points (list[dict[str, float]]): a list of dicts with keys corresponding to objective
2452
+ function symbols and values to reference point components, i.e.,
2453
+ aspiration levels.
2454
+ nadir (dict[str, float], optional): nadir point values. If not given, attempt will be made
2455
+ to calculate nadir point from problem.
2456
+ rho (float, optional): a small scalar value to scale the sum in the objective
2457
+ function of the scalarization. Defaults to 1e-6.
2458
+ delta (float, optional): a small scalar to define the utopian point. Defaults to 1e-6.
2459
+
2460
+ Raises:
2461
+ ScalarizationError: there are missing elements in any reference point.
2462
+
2463
+ Returns:
2464
+ tuple[Problem, str]: a tuple with the copy of the problem with the added
2465
+ scalarization and the symbol of the added scalarization.
2466
+ """
2467
+ # check reference points
2468
+ for reference_point in reference_points:
2469
+ if not objective_dict_has_all_symbols(problem, reference_point):
2470
+ msg = f"The give reference point {reference_point} is missing value for one or more objectives."
2471
+ raise ScalarizationError(msg)
2472
+
2473
+ # check if nadir point is specified
2474
+ # if not specified, try to calculate corrected nadir point
2475
+ if nadir is not None:
2476
+ nadir_point = nadir
2477
+ elif problem.get_nadir_point() is not None:
2478
+ nadir_point = get_corrected_nadir(problem)
2479
+ else:
2480
+ msg = "Nadir point not defined!"
2481
+ raise ScalarizationError(msg)
2482
+
2483
+ # calculate the weights
2484
+ weights = []
2485
+ for reference_point in reference_points:
2486
+ corrected_rp = get_corrected_reference_point(problem, reference_point)
2487
+ weights.append(
2488
+ {
2489
+ obj.symbol: 1 / ((nadir_point[obj.symbol] + delta) - (corrected_rp[obj.symbol]))
2490
+ for obj in problem.objectives
2491
+ }
2492
+ )
2493
+
2494
+ # form the max term
2495
+ max_terms = []
2496
+ for i in range(len(reference_points)):
2497
+ corrected_rp = get_corrected_reference_point(problem, reference_points[i])
2498
+ for obj in problem.objectives:
2499
+ max_terms.append(f"{weights[i][obj.symbol]} * ({obj.symbol}_min - {nadir_point[obj.symbol]})")
2500
+ max_terms = ", ".join(max_terms)
2501
+
2502
+ # form the augmentation term
2503
+ aug_exprs = []
2504
+ for i in range(len(reference_points)):
2505
+ aug_expr = " + ".join([f"({weights[i][obj.symbol]} * {obj.symbol}_min)" for obj in problem.objectives])
2506
+ aug_exprs.append(aug_expr)
2507
+ aug_exprs = " + ".join(aug_exprs)
2508
+
2509
+ func = f"{Op.MAX}({max_terms}) + {rho}*({aug_exprs})"
2510
+ scalarization = ScalarizationFunction(
2511
+ name="GUESS scalarization objective function for multiple decision makers",
2512
+ symbol=symbol,
2513
+ func=func,
2514
+ is_linear=problem.is_linear,
2515
+ is_convex=problem.is_convex,
2516
+ is_twice_differentiable=False,
2517
+ )
2518
+ return problem.add_scalarization(scalarization), symbol
2519
+
2520
+
2521
+ def add_group_guess_sf_diff(
2522
+ problem: Problem,
2523
+ symbol: str,
2524
+ reference_points: list[dict[str, float]],
2525
+ nadir: dict[str, float] | None = None,
2526
+ rho: float = 1e-6,
2527
+ delta: float = 1e-6,
2528
+ ) -> tuple[Problem, str]:
2529
+ r"""Adds the differentiable variant of the multiple decision maker variant of the GUESS scalarizing function.
2530
+
2531
+ The scalarization function is defined as follows:
2532
+
2533
+ \begin{align}
2534
+ &\mbox{minimize} &&\alpha +
2535
+ \rho \sum^k_{i=1} \sum^{n_d}_{d=1} w_{id}f_{id}(\mathbf{x}) \\
2536
+ &\mbox{subject to} && w_{id}(f_{id}(\mathbf{x})-z^{nad}_{id}) - \alpha \leq 0,\\
2537
+ &&&\mathbf{x} \in \mathbf{X},
2538
+ \end{align}
2539
+
2540
+ where $w_{id} = \frac{1}{z^{nad}_{id} - \overline{z}_{id}}$.
2541
+
2542
+ Args:
2543
+ problem (Problem): the problem the scalarization is added to.
2544
+ symbol (str): the symbol given to the added scalarization.
2545
+ reference_points (list[dict[str, float]]): a list of dicts with keys corresponding to objective
2546
+ function symbols and values to reference point components, i.e.,
2547
+ aspiration levels.
2548
+ nadir (dict[str, float], optional): nadir point values. If not given, attempt will be made
2549
+ to calculate nadir point from problem.
2550
+ rho (float, optional): a small scalar value to scale the sum in the objective
2551
+ function of the scalarization. Defaults to 1e-6.
2552
+ delta (float, optional): a small scalar to define the utopian point. Defaults to 1e-6.
2553
+
2554
+ Raises:
2555
+ ScalarizationError: there are missing elements in any reference point.
2556
+
2557
+ Returns:
2558
+ tuple[Problem, str]: a tuple with the copy of the problem with the added
2559
+ scalarization and the symbol of the added scalarization.
2560
+ """
2561
+ # check reference points
2562
+ for reference_point in reference_points:
2563
+ if not objective_dict_has_all_symbols(problem, reference_point):
2564
+ msg = f"The give reference point {reference_point} is missing value for one or more objectives."
2565
+ raise ScalarizationError(msg)
2566
+
2567
+ # check if nadir point is specified
2568
+ # if not specified, try to calculate corrected nadir point
2569
+ if nadir is not None:
2570
+ nadir_point = nadir
2571
+ elif problem.get_nadir_point() is not None:
2572
+ nadir_point = get_corrected_nadir(problem)
2573
+ else:
2574
+ msg = "Nadir point not defined!"
2575
+ raise ScalarizationError(msg)
2576
+
2577
+ # define the auxiliary variable
2578
+ alpha = Variable(
2579
+ name="alpha",
2580
+ symbol="_alpha",
2581
+ variable_type=VariableTypeEnum.real,
2582
+ lowerbound=-float("Inf"),
2583
+ upperbound=float("Inf"),
2584
+ initial_value=1.0,
2585
+ )
2586
+
2587
+ # calculate the weights
2588
+ weights = []
2589
+ for reference_point in reference_points:
2590
+ corrected_rp = get_corrected_reference_point(problem, reference_point)
2591
+ weights.append(
2592
+ {
2593
+ obj.symbol: 1 / ((nadir_point[obj.symbol] + delta) - (corrected_rp[obj.symbol]))
2594
+ for obj in problem.objectives
2595
+ }
2596
+ )
2597
+
2598
+ # form the max term
2599
+ con_terms = []
2600
+ for i in range(len(reference_points)):
2601
+ corrected_rp = get_corrected_reference_point(problem, reference_points[i])
2602
+ rp = {}
2603
+ for obj in problem.objectives:
2604
+ rp[obj.symbol] = f"{weights[i][obj.symbol]} * ({obj.symbol}_min - {nadir_point[obj.symbol]}) - _alpha"
2605
+ con_terms.append(rp)
2606
+
2607
+ # form the augmentation term
2608
+ aug_exprs = []
2609
+ for i in range(len(reference_points)):
2610
+ aug_expr = " + ".join([f"({weights[i][obj.symbol]} * {obj.symbol}_min)" for obj in problem.objectives])
2611
+ aug_exprs.append(aug_expr)
2612
+ aug_exprs = " + ".join(aug_exprs)
2613
+
2614
+ constraints = []
2615
+ # loop to create a constraint for every objective of every reference point given
2616
+ for i in range(len(reference_points)):
2617
+ for obj in problem.objectives:
2618
+ # since we are subtracting a constant value, the linearity, convexity,
2619
+ # and differentiability of the objective function, and hence the
2620
+ # constraint, should not change.
2621
+ constraints.append(
2622
+ Constraint(
2623
+ name=f"Constraint for {obj.symbol}",
2624
+ symbol=f"{obj.symbol}_con_{i+1}",
2625
+ func=con_terms[i][obj.symbol],
2626
+ cons_type=ConstraintTypeEnum.LTE,
2627
+ is_linear=obj.is_linear,
2628
+ is_convex=obj.is_convex,
2629
+ is_twice_differentiable=obj.is_twice_differentiable,
2630
+ )
2631
+ )
2632
+
2633
+ func = f"_alpha + {rho}*({aug_exprs})"
2634
+ scalarization = ScalarizationFunction(
2635
+ name="Differentiable GUESS scalarization objective function for multiple decision makers",
2636
+ symbol=symbol,
2637
+ func=func,
2638
+ is_linear=problem.is_linear,
2639
+ is_convex=problem.is_convex,
2640
+ is_twice_differentiable=problem.is_twice_differentiable,
2641
+ )
2642
+ _problem = problem.add_variables([alpha])
2643
+ _problem = _problem.add_scalarization(scalarization)
2644
+ return _problem.add_constraints(constraints), symbol
2645
+
2646
+
2647
+ def add_asf_diff(
2648
+ problem: Problem,
2649
+ symbol: str,
2650
+ reference_point: dict[str, float],
2651
+ ideal: dict[str, float] | None = None,
2652
+ nadir: dict[str, float] | None = None,
2653
+ rho: float = 1e-6,
2654
+ delta: float = 1e-6,
2655
+ ) -> tuple[Problem, str]:
2656
+ r"""Adds the differentiable variant of the achievement scalarizing function.
2657
+
2658
+ \begin{align*}
2659
+ \min \quad & \alpha + \rho \sum_{i=1}^k \frac{f_i(\mathbf{x})}{z_i^\text{nad} - z_i^{\star\star}} \\
2660
+ \text{s.t.} \quad & \frac{f_i(\mathbf{x}) - \bar{z}_i}{z_i^\text{nad}
2661
+ - z_i^{\star\star}} - \alpha \leq 0,\\
2662
+ & \mathbf{x} \in S,
2663
+ \end{align*}
2664
+
2665
+ where $f_i$ are objective functions, $z_i^{\star\star} = z_i^\star - \delta$ is
2666
+ a component of the utopian point, $\bar{z}_i$ is a component of the reference point,
2667
+ $\rho$ and $\delta$ are small scalar values, $S$ is the feasible solution
2668
+ space of the original problem, and $\alpha$ is an auxiliary variable.
2669
+
2670
+ References:
2671
+ Wierzbicki, A. P. (1982). A mathematical basis for satisficing decision
2672
+ making. Mathematical modelling, 3(5), 391-405.
2673
+
2674
+ Args:
2675
+ problem (Problem): the problem the scalarization is added to.
2676
+ symbol (str): the symbol given to the added scalarization.
2677
+ reference_point (dict[str, float]): a dict with keys corresponding to objective
2678
+ function symbols and values to reference point components, i.e.,
2679
+ aspiration levels.
2680
+ ideal (dict[str, float], optional): ideal point values. If not given, attempt will be made
2681
+ to calculate ideal point from problem.
2682
+ nadir (dict[str, float], optional): nadir point values. If not given, attempt will be made
2683
+ to calculate nadir point from problem.
2684
+ rho (float, optional): a small scalar value to scale the sum in the objective
2685
+ function of the scalarization. Defaults to 1e-6.
2686
+ delta (float, optional): a small scalar to define the utopian point. Defaults to 1e-6.
2687
+
2688
+ Returns:
2689
+ tuple[Problem, str]: a tuple with the copy of the problem with the added
2690
+ scalarization and the symbol of the added scalarization.
2691
+
2692
+ Todo:
2693
+ Add reference in augmentation term option!
2694
+ """
2695
+ # check reference point
2696
+ if not objective_dict_has_all_symbols(problem, reference_point):
2697
+ msg = f"The give reference point {reference_point} is missing value for one or more objectives."
2698
+ raise ScalarizationError(msg)
2699
+
2700
+ # check if ideal point is specified
2701
+ # if not specified, try to calculate corrected ideal point
2702
+ if ideal is not None:
2703
+ ideal_point = ideal
2704
+ elif problem.get_ideal_point() is not None:
2705
+ ideal_point = get_corrected_ideal(problem)
2706
+ else:
2707
+ msg = "Ideal point not defined!"
2708
+ raise ScalarizationError(msg)
2709
+
2710
+ # check if nadir point is specified
2711
+ # if not specified, try to calculate corrected nadir point
2712
+ if nadir is not None:
2713
+ nadir_point = nadir
2714
+ elif problem.get_nadir_point() is not None:
2715
+ nadir_point = get_corrected_nadir(problem)
2716
+ else:
2717
+ msg = "Nadir point not defined!"
2718
+ raise ScalarizationError(msg)
2719
+
2720
+ corrected_rp = get_corrected_reference_point(problem, reference_point)
2721
+
2722
+ # define the auxiliary variable
2723
+ alpha = Variable(
2724
+ name="alpha",
2725
+ symbol="_alpha",
2726
+ variable_type=VariableTypeEnum.real,
2727
+ lowerbound=-float("Inf"),
2728
+ upperbound=float("Inf"),
2729
+ initial_value=1.0,
2730
+ )
2731
+
2732
+ # define the objective function of the scalarization
2733
+ aug_expr = " + ".join(
2734
+ [
2735
+ (f"{obj.symbol}_min / ({nadir_point[obj.symbol]} - {ideal_point[obj.symbol] - delta})")
2736
+ for obj in problem.objectives
2737
+ ]
2738
+ )
2739
+
2740
+ target_expr = f"_alpha + {rho}*" + f"({aug_expr})"
2741
+ scalarization = ScalarizationFunction(
2742
+ name="ASF scalarization objective function",
2743
+ symbol=symbol,
2744
+ func=target_expr,
2745
+ is_linear=problem.is_linear,
2746
+ is_convex=problem.is_convex,
2747
+ is_twice_differentiable=problem.is_twice_differentiable,
2748
+ )
2749
+
2750
+ constraints = []
2751
+
2752
+ for obj in problem.objectives:
2753
+ expr = (
2754
+ f"({obj.symbol}_min - {corrected_rp[obj.symbol]}) / "
2755
+ f"({nadir_point[obj.symbol]} - {ideal_point[obj.symbol] - delta}) - _alpha"
2756
+ )
2757
+
2758
+ constraints.append(
2759
+ Constraint(
2760
+ name=f"Constraint for {obj.symbol}",
2761
+ symbol=f"{obj.symbol}_con",
2762
+ func=expr,
2763
+ cons_type=ConstraintTypeEnum.LTE,
2764
+ is_linear=obj.is_linear,
2765
+ is_convex=obj.is_convex,
2766
+ is_twice_differentiable=obj.is_twice_differentiable,
2767
+ )
2768
+ )
2769
+
2770
+ _problem = problem.add_variables([alpha])
2771
+ _problem = _problem.add_scalarization(scalarization)
2772
+ return _problem.add_constraints(constraints), symbol
2773
+
2774
+
2775
+ def add_weighted_sums(problem: Problem, symbol: str, weights: dict[str, float]) -> tuple[Problem, str]:
2776
+ r"""Add the weighted sums scalarization to a problem with the given weights.
2777
+
2778
+ It is assumed that the weights add to 1.
2779
+
2780
+ The scalarization is defined as follows:
2781
+
2782
+ \begin{equation}
2783
+ \begin{aligned}
2784
+ & \mathcal{S}_\text{WS}(F(\mathbf{x});\mathbf{w}) = \sum_{i=1}^{k} w_i f_i(\mathbf{x}) \\
2785
+ & \text{s.t.} \sum_{i=1}^{k} w_i = 1,
2786
+ \end{aligned}
2787
+ \end{equation}
2788
+
2789
+ where $\mathbf{w} = [w_1,\dots,w_k]$ are the weights and $k$ is the number of
2790
+ objective functions.
2791
+
2792
+ Warning:
2793
+ The weighted sums scalarization is often not capable of finding most Pareto optimal
2794
+ solutions when optimized. It is advised to utilize some better scalarization
2795
+ functions.
2796
+
2797
+ Args:
2798
+ problem (Problem): the problem to which the scalarization should be added.
2799
+ symbol (str): the symbol to reference the added scalarization function.
2800
+ weights (dict[str, float]): the weights. For the method to work, the weights
2801
+ should sum to 1. However, this is not a condition that is checked.
2802
+
2803
+ Raises:
2804
+ ScalarizationError: if the weights are missing any of the objective components.
2805
+
2806
+ Returns:
2807
+ tuple[Problem, str]: A tuple containing a copy of the problem with the scalarization function added,
2808
+ and the symbol of the added scalarization function.
2809
+ """
2810
+ # check that the weights have all the objective components
2811
+ if not all(obj.symbol in weights for obj in problem.objectives):
2812
+ msg = f"The given weight vector {weights} does not have a component defined for all the objectives."
2813
+ raise ScalarizationError(msg)
2814
+
2815
+ # Build the sum
2816
+ sum_terms = [f"({weights[obj.symbol]} * {obj.symbol}_min)" for obj in problem.objectives]
2817
+
2818
+ # aggregate the terms
2819
+ sf = " + ".join(sum_terms)
2820
+
2821
+ # Add the function to the problem
2822
+ scalarization_function = ScalarizationFunction(
2823
+ name="Weighted sums scalarization function",
2824
+ symbol=symbol,
2825
+ func=sf,
2826
+ is_linear=problem.is_linear,
2827
+ is_convex=problem.is_convex,
2828
+ is_twice_differentiable=problem.is_twice_differentiable,
2829
+ )
2830
+ return problem.add_scalarization(scalarization_function), symbol
2831
+
2832
+
2833
+ def add_objective_as_scalarization(problem: Problem, symbol: str, objective_symbol: str) -> tuple[Problem, str]:
2834
+ r"""Creates a scalarization where one of the problem's objective functions is optimized.
2835
+
2836
+ The scalarization is defined as follows:
2837
+
2838
+ \begin{equation}
2839
+ \operatorname{min}_{\mathbf{x} \in S} f_t(\mathbf{x}),
2840
+ \end{equation}
2841
+
2842
+ where $f_t(\mathbf{x})$ is the objective function to be minimized.
2843
+
2844
+ Args:
2845
+ problem (Problem): the problem to which the scalarization should be added.
2846
+ symbol (str): the symbol to reference the added scalarization function.
2847
+ objective_symbol (str): the symbol of the objective function to be optimized.
2848
+
2849
+ Raises:
2850
+ ScalarizationError: the given objective_symbol does not exist in the problem.
2851
+
2852
+ Returns:
2853
+ tuple[Problem, str]: A tuple containing a copy of the problem with the scalarization function added,
2854
+ and the symbol of the added scalarization function.
2855
+ """
2856
+ # check that symbol exists
2857
+ if problem.get_objective(objective_symbol, copy=False) is None:
2858
+ msg = f"The given objective symbol {objective_symbol} is not defined in the problem.."
2859
+ raise ScalarizationError(msg)
2860
+
2861
+ sf = ["Multiply", 1, f"{objective_symbol}_min"]
2862
+
2863
+ original_objective = problem.get_objective(objective_symbol, copy=False)
2864
+
2865
+ # Add the function to the problem
2866
+ scalarization_function = ScalarizationFunction(
2867
+ name=f"Objective {objective_symbol}",
2868
+ symbol=symbol,
2869
+ func=sf,
2870
+ is_linear=original_objective.is_linear,
2871
+ is_convex=original_objective.is_convex,
2872
+ is_twice_differentiable=original_objective.is_twice_differentiable,
2873
+ )
2874
+ return problem.add_scalarization(scalarization_function), symbol
2875
+
2876
+
2877
+ def add_epsilon_constraints(
2878
+ problem: Problem, symbol: str, constraint_symbols: dict[str, str], objective_symbol: str, epsilons: dict[str, float]
2879
+ ) -> tuple[Problem, str, list[str]]:
2880
+ r"""Creates expressions for an epsilon constraints scalarization and constraints.
2881
+
2882
+ It is assumed that epsilon have been given in a format where each objective is to be minimized.
2883
+
2884
+ The scalarization is defined as follows:
2885
+
2886
+ \begin{equation}
2887
+ \begin{aligned}
2888
+ & \operatorname{min}_{\mathbf{x} \in S}
2889
+ & & f_t(\mathbf{x}) \\
2890
+ & \text{s.t.}
2891
+ & & f_j(\mathbf{x}) \leq \epsilon_j \text{ for all } j = 1, \ldots ,k, \; j \neq t,
2892
+ \end{aligned}
2893
+ \end{equation}
2894
+
2895
+ where $\epsilon_j$ are the epsilon bounds used in the epsilon constraints $f_j(\mathbf{x}) \leq \epsilon_j$,
2896
+ and $k$ is the number of objective functions.
2897
+
2898
+ Args:
2899
+ problem (Problem): the problem to scalarize.
2900
+ symbol (str): the symbol of the added objective function to be optimized.
2901
+ constraint_symbols (dict[str, str]): a dict with the symbols to be used with the added
2902
+ constraints. The key indicates the name of the objective function the constraint
2903
+ is related to, and the value is the symbol to be used when defining the constraint.
2904
+ objective_symbol (str): the objective used as the objective in the epsilon constraint scalarization.
2905
+ epsilons (dict[str, float]): the epsilon constraint values in a dict
2906
+ with each key being an objective's symbol. The corresponding value
2907
+ is then used as the epsilon value for the respective objective function.
2908
+
2909
+ Raises:
2910
+ ScalarizationError: `objective_symbol` not found in problem definition.
2911
+
2912
+ Returns:
2913
+ tuple[Problem, str, list[str]]: A triple with the first element being a copy of the
2914
+ problem with the added epsilon constraints. The second element is the symbol of
2915
+ the objective to be optimized. The last element is a list with the symbols
2916
+ of the added constraints to the problem.
2917
+ """
2918
+ if objective_symbol not in (correct_symbols := [objective.symbol for objective in problem.objectives]):
2919
+ msg = f"The given objective symbol {objective_symbol} should be one of {correct_symbols}."
2920
+ raise ScalarizationError(msg)
2921
+
2922
+ _problem, _ = add_objective_as_scalarization(problem, symbol, objective_symbol)
2923
+
2924
+ # the epsilons must be given such that each objective function is to be minimized
2925
+ constraints = [
2926
+ Constraint(
2927
+ name=f"Epsilon for {obj.symbol}",
2928
+ symbol=constraint_symbols[obj.symbol],
2929
+ func=["Add", f"{obj.symbol}_min", ["Negate", epsilons[obj.symbol]]],
2930
+ cons_type=ConstraintTypeEnum.LTE,
2931
+ is_linear=obj.is_linear,
2932
+ is_convex=obj.is_convex,
2933
+ is_twice_differentiable=obj.is_twice_differentiable,
2934
+ )
2935
+ for obj in problem.objectives
2936
+ if obj.symbol != objective_symbol
2937
+ ]
2938
+
2939
+ _problem = _problem.add_constraints(constraints)
2940
+
2941
+ return _problem, symbol, [con.symbol for con in constraints]
2942
+
2943
+
2944
+ def create_epsilon_constraints_json(
2945
+ problem: Problem, objective_symbol: str, epsilons: dict[str, float]
2946
+ ) -> tuple[list[str | int | float], list[str]]:
2947
+ """Creates JSON expressions for an epsilon constraints scalarization and constraints.
2948
+
2949
+ It is assumed that epsilon have been given in a format where each objective is to be minimized.
2950
+
2951
+ Warning:
2952
+ To be deprecated.
2953
+
2954
+ Args:
2955
+ problem (Problem): the problem to scalarize.
2956
+ objective_symbol (str): the objective used as the objective in the epsilon constraint scalarization.
2957
+ epsilons (dict[str, float]): the epsilon constraint values in a dict
2958
+ with each key being an objective's symbol.
2959
+
2960
+ Raises:
2961
+ ScalarizationError: `objective_symbol` not found in problem definition.
2962
+
2963
+ Returns:
2964
+ tuple[list, list]: the first element is the expression of the scalarized objective expressed in MathJSON format.
2965
+ The second element is a list of expressions of the constraints expressed in MathJSON format.
2966
+ The constraints are in less than or equal format.
2967
+ """
2968
+ correct_symbols = [objective.symbol for objective in problem.objectives]
2969
+ if objective_symbol not in correct_symbols:
2970
+ msg = f"The given objective symbol {objective_symbol} should be one of {correct_symbols}."
2971
+ raise ScalarizationError(msg)
2972
+ correct_symbols.remove(objective_symbol)
2973
+
2974
+ scalarization_expr = ["Multiply", 1, f"{objective_symbol}_min"]
2975
+
2976
+ # the epsilons must be given such that each objective function is to be minimized
2977
+ constraint_exprs = [["Add", f"{obj}_min", ["Negate", epsilons[obj]]] for obj in correct_symbols]
2978
+
2979
+ return scalarization_expr, constraint_exprs
2980
+
2981
+
2982
+ def add_group_scenario_sf_nondiff(
2983
+ problem: Problem,
2984
+ symbol: str,
2985
+ reference_points: list[dict[str, float]],
2986
+ weights: list[dict[str, float]],
2987
+ epsilon: float = 1e-6,
2988
+ ) -> tuple[Problem, str]:
2989
+ r"""Add the non-differentiable scenario based scalarization function.
2990
+
2991
+ Add the following scalarization function:
2992
+ \begin{align}
2993
+ \min_{\mathbf{x}}\quad
2994
+ &\max_{i,p}\bigl[w_{ip}\bigl(f_{ip}(\mathbf{x}) - \bar z_{ip}\bigr)\bigr]
2995
+ \;+\;\varepsilon \sum_{i,p} w_{ip}\bigl(f_{ip}(\mathbf{x}) - \bar z_{ip}\bigr) \\[6pt]
2996
+ \text{s.t.}\quad
2997
+ &\mathbf{x} \in \mathcal{X}\,,
2998
+ \end{align}
2999
+
3000
+ Args:
3001
+ problem (Problem): the problem the scalarization is added to.
3002
+ symbol (str): the symbol given to the added scalarization.
3003
+ reference_points (list[dict[str, float]]): a list of reference points as objective dicts.
3004
+ function symbols and values to reference point components, i.e., aspiration levels.
3005
+ weights (list[dict[str, float]]): the list of weights to be used in the scalarization function.
3006
+ Must be positive.
3007
+ epsilon: small augmentation multiplier ε
3008
+
3009
+ Returns:
3010
+ tuple[Problem, str]: A tuple containing a copy of the problem with the scalarization function added,
3011
+ and the symbol of the added scalarization function.
3012
+ """
3013
+ if len(reference_points) != len(weights):
3014
+ raise ScalarizationError("reference_points and weights must have same length")
3015
+
3016
+ for reference_point, weight in zip(reference_points, weights, strict=True):
3017
+ if not objective_dict_has_all_symbols(problem, reference_point):
3018
+ raise ScalarizationError(
3019
+ f"The give reference point {reference_point} " f"is missing value for one or more objectives."
3020
+ )
3021
+ if not objective_dict_has_all_symbols(problem, weight):
3022
+ raise ScalarizationError(
3023
+ f"The given weight vector {weight} is missing " f"a value for one or more objectives."
3024
+ )
3025
+
3026
+ max_list: list[str] = []
3027
+ sum_list: list[str] = []
3028
+ for reference_point, weight in zip(reference_points, weights, strict=True):
3029
+ corrected_ref_point = get_corrected_reference_point(problem, reference_point)
3030
+
3031
+ for obj in problem.objectives:
3032
+ expr = f"{weight[obj.symbol]}*({obj.symbol}_min - {corrected_ref_point[obj.symbol]})"
3033
+ max_list.append(expr)
3034
+ sum_list.append(expr)
3035
+
3036
+ max_part = f"{Op.MAX}({', '.join(max_list)})"
3037
+ sum_part = " + ".join(sum_list)
3038
+ func = f"{max_part} + {epsilon}*({sum_part})"
3039
+
3040
+ scalar = ScalarizationFunction(
3041
+ name="Group non differentiable scalarization function for scenario based problems.",
3042
+ symbol=symbol,
3043
+ func=func,
3044
+ is_linear=problem.is_linear,
3045
+ is_convex=problem.is_convex,
3046
+ is_twice_differentiable=problem.is_twice_differentiable,
3047
+ )
3048
+ return problem.add_scalarization(scalar), symbol
3049
+
3050
+
3051
+ def add_group_scenario_sf_diff(
3052
+ problem: Problem,
3053
+ symbol: str,
3054
+ reference_points: list[dict[str, float]],
3055
+ weights: list[dict[str, float]],
3056
+ epsilon: float = 1e-6,
3057
+ ) -> tuple[Problem, str]:
3058
+ r"""Add the differentiable scenario-based scalarization.
3059
+
3060
+ Adds the following scalarization function:
3061
+ \begin{align}
3062
+ \min_{x,\alpha}\quad
3063
+ & \alpha \;+\; \varepsilon \sum_{i,p} w_{ip}\bigl(f_{ip}(x) - \bar z_{ip}\bigr) \\
3064
+ \text{s.t.}\quad
3065
+ & w_{ip}\bigl(f_{ip}(x) - \bar z_{ip}\bigr)\;-\;\alpha \;\le\;0
3066
+ \quad\forall\,i,p,\\
3067
+ & x \in \mathcal{X}\,,
3068
+ \end{align}
3069
+
3070
+ Args:
3071
+ problem (Problem): the problem the scalarization is added to.
3072
+ symbol (str): the symbol given to the added scalarization.
3073
+ reference_points (list[dict[str, float]]): a list of reference points as objective dicts.
3074
+ function symbols and values to reference point components, i.e., aspiration levels.
3075
+ weights (list[dict[str, float]]): the list of weights to be used in the scalarization function.
3076
+ Must be positive.
3077
+ epsilon: small augmentation multiplier ε
3078
+
3079
+ Returns:
3080
+ tuple[Problem, str]: A tuple containing a copy of the problem with the scalarization function added,
3081
+ and the symbol of the added scalarization function.
3082
+ """
3083
+ if len(reference_points) != len(weights):
3084
+ raise ScalarizationError("reference_points and weights must have same length")
3085
+
3086
+ for idx, (ref_point, weight) in enumerate(zip(reference_points, weights, strict=True)):
3087
+ if not objective_dict_has_all_symbols(problem, ref_point):
3088
+ raise ScalarizationError(f"reference_points[{idx}] missing some objectives")
3089
+ if not objective_dict_has_all_symbols(problem, weight):
3090
+ raise ScalarizationError(f"weights[{idx}] missing some objectives")
3091
+
3092
+ alpha = Variable(
3093
+ name="alpha",
3094
+ symbol="_alpha",
3095
+ variable_type=VariableTypeEnum.real,
3096
+ lowerbound=-float("Inf"),
3097
+ upperbound=float("Inf"),
3098
+ initial_value=0.0,
3099
+ )
3100
+
3101
+ sum_list = []
3102
+ constraints = []
3103
+
3104
+ for idx, (ref_point, weight) in enumerate(zip(reference_points, weights, strict=True)):
3105
+ corrected_rp = get_corrected_reference_point(problem, ref_point)
3106
+ for obj in problem.objectives:
3107
+ expr = f"{weight[obj.symbol]}*({obj.symbol}_min - {corrected_rp[obj.symbol]})"
3108
+ sum_list.append(expr)
3109
+
3110
+ constraints.append(
3111
+ Constraint(
3112
+ name=f"ssf_con_{obj.symbol}",
3113
+ symbol=f"{obj.symbol}_con_{idx}",
3114
+ func=f"{expr} - {alpha.symbol}",
3115
+ cons_type=ConstraintTypeEnum.LTE,
3116
+ is_linear=obj.is_linear,
3117
+ is_convex=obj.is_convex,
3118
+ is_twice_differentiable=obj.is_twice_differentiable,
3119
+ )
3120
+ )
3121
+
3122
+ sum_part = " + ".join(sum_list)
3123
+
3124
+ func = f"_alpha + {epsilon}*({sum_part})"
3125
+ scalar = ScalarizationFunction(
3126
+ name="Scenario-based differentiable ASF",
3127
+ symbol=symbol,
3128
+ func=func,
3129
+ is_linear=problem.is_linear,
3130
+ is_convex=problem.is_convex,
3131
+ is_twice_differentiable=problem.is_twice_differentiable,
3132
+ )
3133
+
3134
+ problem_ = problem.add_variables([alpha])
3135
+ problem_ = problem_.add_constraints(constraints)
3136
+ problem_ = problem_.add_scalarization(scalar)
3137
+
3138
+ return problem_, symbol