desdeo 1.2__py3-none-any.whl → 2.1.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 (182) hide show
  1. desdeo/__init__.py +8 -8
  2. desdeo/adm/ADMAfsar.py +551 -0
  3. desdeo/adm/ADMChen.py +414 -0
  4. desdeo/adm/BaseADM.py +119 -0
  5. desdeo/adm/__init__.py +11 -0
  6. desdeo/api/README.md +73 -0
  7. desdeo/api/__init__.py +15 -0
  8. desdeo/api/app.py +50 -0
  9. desdeo/api/config.py +90 -0
  10. desdeo/api/config.toml +64 -0
  11. desdeo/api/db.py +27 -0
  12. desdeo/api/db_init.py +85 -0
  13. desdeo/api/db_models.py +164 -0
  14. desdeo/api/malaga_db_init.py +27 -0
  15. desdeo/api/models/__init__.py +266 -0
  16. desdeo/api/models/archive.py +23 -0
  17. desdeo/api/models/emo.py +128 -0
  18. desdeo/api/models/enautilus.py +69 -0
  19. desdeo/api/models/gdm/gdm_aggregate.py +139 -0
  20. desdeo/api/models/gdm/gdm_base.py +69 -0
  21. desdeo/api/models/gdm/gdm_score_bands.py +114 -0
  22. desdeo/api/models/gdm/gnimbus.py +138 -0
  23. desdeo/api/models/generic.py +104 -0
  24. desdeo/api/models/generic_states.py +401 -0
  25. desdeo/api/models/nimbus.py +158 -0
  26. desdeo/api/models/preference.py +128 -0
  27. desdeo/api/models/problem.py +717 -0
  28. desdeo/api/models/reference_point_method.py +18 -0
  29. desdeo/api/models/session.py +49 -0
  30. desdeo/api/models/state.py +463 -0
  31. desdeo/api/models/user.py +52 -0
  32. desdeo/api/models/utopia.py +25 -0
  33. desdeo/api/routers/_EMO.backup +309 -0
  34. desdeo/api/routers/_NAUTILUS.py +245 -0
  35. desdeo/api/routers/_NAUTILUS_navigator.py +233 -0
  36. desdeo/api/routers/_NIMBUS.py +765 -0
  37. desdeo/api/routers/__init__.py +5 -0
  38. desdeo/api/routers/emo.py +497 -0
  39. desdeo/api/routers/enautilus.py +237 -0
  40. desdeo/api/routers/gdm/gdm_aggregate.py +234 -0
  41. desdeo/api/routers/gdm/gdm_base.py +420 -0
  42. desdeo/api/routers/gdm/gdm_score_bands/gdm_score_bands_manager.py +398 -0
  43. desdeo/api/routers/gdm/gdm_score_bands/gdm_score_bands_routers.py +377 -0
  44. desdeo/api/routers/gdm/gnimbus/gnimbus_manager.py +698 -0
  45. desdeo/api/routers/gdm/gnimbus/gnimbus_routers.py +591 -0
  46. desdeo/api/routers/generic.py +233 -0
  47. desdeo/api/routers/nimbus.py +705 -0
  48. desdeo/api/routers/problem.py +307 -0
  49. desdeo/api/routers/reference_point_method.py +93 -0
  50. desdeo/api/routers/session.py +100 -0
  51. desdeo/api/routers/test.py +16 -0
  52. desdeo/api/routers/user_authentication.py +520 -0
  53. desdeo/api/routers/utils.py +187 -0
  54. desdeo/api/routers/utopia.py +230 -0
  55. desdeo/api/schema.py +100 -0
  56. desdeo/api/tests/__init__.py +0 -0
  57. desdeo/api/tests/conftest.py +151 -0
  58. desdeo/api/tests/test_enautilus.py +330 -0
  59. desdeo/api/tests/test_models.py +1179 -0
  60. desdeo/api/tests/test_routes.py +1075 -0
  61. desdeo/api/utils/_database.py +263 -0
  62. desdeo/api/utils/_logger.py +29 -0
  63. desdeo/api/utils/database.py +36 -0
  64. desdeo/api/utils/emo_database.py +40 -0
  65. desdeo/core.py +34 -0
  66. desdeo/emo/__init__.py +159 -0
  67. desdeo/emo/hooks/archivers.py +188 -0
  68. desdeo/emo/methods/EAs.py +541 -0
  69. desdeo/emo/methods/__init__.py +0 -0
  70. desdeo/emo/methods/bases.py +12 -0
  71. desdeo/emo/methods/templates.py +111 -0
  72. desdeo/emo/operators/__init__.py +1 -0
  73. desdeo/emo/operators/crossover.py +1282 -0
  74. desdeo/emo/operators/evaluator.py +114 -0
  75. desdeo/emo/operators/generator.py +459 -0
  76. desdeo/emo/operators/mutation.py +1224 -0
  77. desdeo/emo/operators/scalar_selection.py +202 -0
  78. desdeo/emo/operators/selection.py +1778 -0
  79. desdeo/emo/operators/termination.py +286 -0
  80. desdeo/emo/options/__init__.py +108 -0
  81. desdeo/emo/options/algorithms.py +435 -0
  82. desdeo/emo/options/crossover.py +164 -0
  83. desdeo/emo/options/generator.py +131 -0
  84. desdeo/emo/options/mutation.py +260 -0
  85. desdeo/emo/options/repair.py +61 -0
  86. desdeo/emo/options/scalar_selection.py +66 -0
  87. desdeo/emo/options/selection.py +127 -0
  88. desdeo/emo/options/templates.py +383 -0
  89. desdeo/emo/options/termination.py +143 -0
  90. desdeo/explanations/__init__.py +6 -0
  91. desdeo/explanations/explainer.py +100 -0
  92. desdeo/explanations/utils.py +90 -0
  93. desdeo/gdm/__init__.py +22 -0
  94. desdeo/gdm/gdmtools.py +45 -0
  95. desdeo/gdm/score_bands.py +114 -0
  96. desdeo/gdm/voting_rules.py +50 -0
  97. desdeo/mcdm/__init__.py +41 -0
  98. desdeo/mcdm/enautilus.py +338 -0
  99. desdeo/mcdm/gnimbus.py +484 -0
  100. desdeo/mcdm/nautili.py +345 -0
  101. desdeo/mcdm/nautilus.py +477 -0
  102. desdeo/mcdm/nautilus_navigator.py +656 -0
  103. desdeo/mcdm/nimbus.py +417 -0
  104. desdeo/mcdm/pareto_navigator.py +269 -0
  105. desdeo/mcdm/reference_point_method.py +186 -0
  106. desdeo/problem/__init__.py +83 -0
  107. desdeo/problem/evaluator.py +561 -0
  108. desdeo/problem/external/__init__.py +18 -0
  109. desdeo/problem/external/core.py +356 -0
  110. desdeo/problem/external/pymoo_provider.py +266 -0
  111. desdeo/problem/external/runtime.py +44 -0
  112. desdeo/problem/gurobipy_evaluator.py +562 -0
  113. desdeo/problem/infix_parser.py +341 -0
  114. desdeo/problem/json_parser.py +944 -0
  115. desdeo/problem/pyomo_evaluator.py +487 -0
  116. desdeo/problem/schema.py +1829 -0
  117. desdeo/problem/simulator_evaluator.py +348 -0
  118. desdeo/problem/sympy_evaluator.py +244 -0
  119. desdeo/problem/testproblems/__init__.py +88 -0
  120. desdeo/problem/testproblems/benchmarks_server.py +120 -0
  121. desdeo/problem/testproblems/binh_and_korn_problem.py +88 -0
  122. desdeo/problem/testproblems/cake_problem.py +185 -0
  123. desdeo/problem/testproblems/dmitry_forest_problem_discrete.py +71 -0
  124. desdeo/problem/testproblems/dtlz2_problem.py +102 -0
  125. desdeo/problem/testproblems/forest_problem.py +283 -0
  126. desdeo/problem/testproblems/knapsack_problem.py +163 -0
  127. desdeo/problem/testproblems/mcwb_problem.py +831 -0
  128. desdeo/problem/testproblems/mixed_variable_dimenrions_problem.py +83 -0
  129. desdeo/problem/testproblems/momip_problem.py +172 -0
  130. desdeo/problem/testproblems/multi_valued_constraints.py +119 -0
  131. desdeo/problem/testproblems/nimbus_problem.py +143 -0
  132. desdeo/problem/testproblems/pareto_navigator_problem.py +89 -0
  133. desdeo/problem/testproblems/re_problem.py +492 -0
  134. desdeo/problem/testproblems/river_pollution_problems.py +440 -0
  135. desdeo/problem/testproblems/rocket_injector_design_problem.py +140 -0
  136. desdeo/problem/testproblems/simple_problem.py +351 -0
  137. desdeo/problem/testproblems/simulator_problem.py +92 -0
  138. desdeo/problem/testproblems/single_objective.py +289 -0
  139. desdeo/problem/testproblems/spanish_sustainability_problem.py +945 -0
  140. desdeo/problem/testproblems/zdt_problem.py +274 -0
  141. desdeo/problem/utils.py +245 -0
  142. desdeo/tools/GenerateReferencePoints.py +181 -0
  143. desdeo/tools/__init__.py +120 -0
  144. desdeo/tools/desc_gen.py +22 -0
  145. desdeo/tools/generics.py +165 -0
  146. desdeo/tools/group_scalarization.py +3090 -0
  147. desdeo/tools/gurobipy_solver_interfaces.py +258 -0
  148. desdeo/tools/indicators_binary.py +117 -0
  149. desdeo/tools/indicators_unary.py +362 -0
  150. desdeo/tools/interaction_schema.py +38 -0
  151. desdeo/tools/intersection.py +54 -0
  152. desdeo/tools/iterative_pareto_representer.py +99 -0
  153. desdeo/tools/message.py +265 -0
  154. desdeo/tools/ng_solver_interfaces.py +199 -0
  155. desdeo/tools/non_dominated_sorting.py +134 -0
  156. desdeo/tools/patterns.py +283 -0
  157. desdeo/tools/proximal_solver.py +99 -0
  158. desdeo/tools/pyomo_solver_interfaces.py +477 -0
  159. desdeo/tools/reference_vectors.py +229 -0
  160. desdeo/tools/scalarization.py +2065 -0
  161. desdeo/tools/scipy_solver_interfaces.py +454 -0
  162. desdeo/tools/score_bands.py +627 -0
  163. desdeo/tools/utils.py +388 -0
  164. desdeo/tools/visualizations.py +67 -0
  165. desdeo/utopia_stuff/__init__.py +0 -0
  166. desdeo/utopia_stuff/data/1.json +15 -0
  167. desdeo/utopia_stuff/data/2.json +13 -0
  168. desdeo/utopia_stuff/data/3.json +15 -0
  169. desdeo/utopia_stuff/data/4.json +17 -0
  170. desdeo/utopia_stuff/data/5.json +15 -0
  171. desdeo/utopia_stuff/from_json.py +40 -0
  172. desdeo/utopia_stuff/reinit_user.py +38 -0
  173. desdeo/utopia_stuff/utopia_db_init.py +212 -0
  174. desdeo/utopia_stuff/utopia_problem.py +403 -0
  175. desdeo/utopia_stuff/utopia_problem_old.py +415 -0
  176. desdeo/utopia_stuff/utopia_reference_solutions.py +79 -0
  177. desdeo-2.1.0.dist-info/METADATA +186 -0
  178. desdeo-2.1.0.dist-info/RECORD +180 -0
  179. {desdeo-1.2.dist-info → desdeo-2.1.0.dist-info}/WHEEL +1 -1
  180. desdeo-2.1.0.dist-info/licenses/LICENSE +21 -0
  181. desdeo-1.2.dist-info/METADATA +0 -16
  182. desdeo-1.2.dist-info/RECORD +0 -4
@@ -0,0 +1,2065 @@
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
+ from typing import Literal
10
+
11
+ import numpy as np
12
+
13
+ from desdeo.problem import (
14
+ Constraint,
15
+ ConstraintTypeEnum,
16
+ Problem,
17
+ ScalarizationFunction,
18
+ Variable,
19
+ VariableTypeEnum,
20
+ )
21
+ from desdeo.tools.utils import (
22
+ flip_maximized_objective_values,
23
+ get_corrected_ideal,
24
+ get_corrected_nadir,
25
+ )
26
+
27
+
28
+ class ScalarizationError(Exception):
29
+ """Raised when issues with creating or adding scalarization functions are encountered."""
30
+
31
+
32
+ class Op:
33
+ """Defines the supported operators in the MathJSON format."""
34
+
35
+ # TODO: move this to problem/schema.py, make it use this, and import it here from there
36
+ # Basic arithmetic operators
37
+ NEGATE = "Negate"
38
+ ADD = "Add"
39
+ SUB = "Subtract"
40
+ MUL = "Multiply"
41
+ DIV = "Divide"
42
+
43
+ # Exponentation and logarithms
44
+ EXP = "Exp"
45
+ LN = "Ln"
46
+ LB = "Lb"
47
+ LG = "Lg"
48
+ LOP = "LogOnePlus"
49
+ SQRT = "Sqrt"
50
+ SQUARE = "Square"
51
+ POW = "Power"
52
+
53
+ # Rounding operators
54
+ ABS = "Abs"
55
+ CEIL = "Ceil"
56
+ FLOOR = "Floor"
57
+
58
+ # Trigonometric operations
59
+ ARCCOS = "Arccos"
60
+ ARCCOSH = "Arccosh"
61
+ ARCSIN = "Arcsin"
62
+ ARCSINH = "Arcsinh"
63
+ ARCTAN = "Arctan"
64
+ ARCTANH = "Arctanh"
65
+ COS = "Cos"
66
+ COSH = "Cosh"
67
+ SIN = "Sin"
68
+ SINH = "Sinh"
69
+ TAN = "Tan"
70
+ TANH = "Tanh"
71
+
72
+ # Comparison operators
73
+ EQUAL = "Equal"
74
+ GREATER = "Greater"
75
+ GREATER_EQUAL = "GreaterEqual"
76
+ LESS = "Less"
77
+ LESS_EQUAL = "LessEqual"
78
+ NOT_EQUAL = "NotEqual"
79
+
80
+ # Other operators
81
+ MAX = "Max"
82
+ RATIONAL = "Rational"
83
+
84
+
85
+ def objective_dict_has_all_symbols(problem: Problem, obj_dict: dict[str, float]) -> bool:
86
+ """Check that a dict has all the objective function symbols of a problem as its keys.
87
+
88
+ Args:
89
+ problem (Problem): the problem with the objective symbols.
90
+ obj_dict (dict[str, float]): a dict that should have a key for each objective symbol.
91
+
92
+ Returns:
93
+ bool: whether all the symbols are present or not.
94
+ """
95
+ return all(obj.symbol in obj_dict for obj in problem.objectives)
96
+
97
+
98
+ def add_asf_nondiff( # noqa: PLR0913
99
+ problem: Problem,
100
+ symbol: str,
101
+ reference_point: dict[str, float],
102
+ ideal: dict[str, float] | None = None,
103
+ nadir: dict[str, float] | None = None,
104
+ delta: float = 0.000001,
105
+ rho: float = 0.000001,
106
+ *,
107
+ reference_in_aug=False,
108
+ ) -> tuple[Problem, str]:
109
+ r"""Add the achievement scalarizing function for a problem with the reference point.
110
+
111
+ This is the non-differentiable variant of the achievement scalarizing function, which
112
+ means the resulting scalarization function is non-differentiable.
113
+ Requires that the ideal and nadir point have been defined for the problem.
114
+
115
+ The scalarization is defined as follows:
116
+
117
+ \begin{equation}
118
+ \mathcal{S}_\text{ASF}(F(\mathbf{x}); \mathbf{q}, \mathbf{z}^\star, \mathbf{z}^\text{nad}) =
119
+ \underset{i=1,\ldots,k}{\text{max}}
120
+ \left[
121
+ \frac{f_i(\mathbf{x}) - q_i}{z^\text{nad}_i - (z_i^\star - \delta)}
122
+ \right]
123
+ + \rho\sum_{i=1}^{k} \frac{f_i(\mathbf{x})}{z_i^\text{nad} - (z_i^\star - \delta)},
124
+ \end{equation}
125
+
126
+ where $\mathbf{q} = [q_1,\dots,q_k]$ is a reference point, $\mathbf{z^\star} = [z_1^\star,\dots,z_k^\star]$
127
+ is the ideal point, $\mathbf{z}^\text{nad} = [z_1^\text{nad},\dots,z_k^\text{nad}]$ is the nadir point, $k$
128
+ is the number of objective functions, and $\delta$ and $\rho$ are small scalar values. The summation term
129
+ in the scalarization is known as the _augmentation term_. If the reference point is chosen to
130
+ be used in the augmentation term (`reference_in_aug=True`), then
131
+ the reference point components are subtracted from the objective function values in the nominator
132
+ of the augmentation term. That is:
133
+
134
+ \begin{equation}
135
+ \mathcal{S}_\text{ASF}(F(\mathbf{x}); \mathbf{q}, \mathbf{z}^\star, \mathbf{z}^\text{nad}) =
136
+ \underset{i=1,\ldots,k}{\text{max}}
137
+ \left[
138
+ \frac{f_i(\mathbf{x}) - q_i}{z^\text{nad}_i - (z_i^\star - \delta)}
139
+ \right]
140
+ + \rho\sum_{i=1}^{k} \frac{f_i(\mathbf{x}) - q_i}{z_i^\text{nad} - (z_i^\star - \delta)}.
141
+ \end{equation}
142
+
143
+ Args:
144
+ problem (Problem): the problem to which the scalarization function should be added.
145
+ symbol (str): the symbol to reference the added scalarization function.
146
+ reference_point (dict[str, float]): a reference point as an objective dict.
147
+ ideal (dict[str, float], optional): ideal point values. If not given, attempt will be made
148
+ to calculate ideal point from problem.
149
+ nadir (dict[str, float], optional): nadir point values. If not given, attempt will be made
150
+ to calculate nadir point from problem.
151
+ delta (float, optional): the scalar value used to define the utopian point (ideal - delta).
152
+ Defaults to 0.000001.
153
+ rho (float, optional): the weight factor used in the augmentation term. Defaults to 0.000001.
154
+ reference_in_aug (bool): whether the reference point should be used in
155
+ the augmentation term as well. Defaults to False.
156
+
157
+ Raises:
158
+ ScalarizationError: there are missing elements in the reference point, or if any of the ideal or nadir
159
+ point values are undefined (None).
160
+
161
+ Returns:
162
+ tuple[Problem, str]: A tuple containing a copy of the problem with the scalarization function added,
163
+ and the symbol of the added scalarization function.
164
+ """
165
+ # check that the reference point has all the objective components
166
+ if not objective_dict_has_all_symbols(problem, reference_point):
167
+ msg = f"The given reference point {reference_point} does not have a component defined for all the objectives."
168
+ raise ScalarizationError(msg)
169
+
170
+ # check if ideal point is specified
171
+ # if not specified, try to calculate corrected ideal point
172
+ if ideal is not None:
173
+ ideal_point = ideal
174
+ elif problem.get_ideal_point() is not None:
175
+ ideal_point = get_corrected_ideal(problem)
176
+ else:
177
+ msg = "Ideal point not defined!"
178
+ raise ScalarizationError(msg)
179
+
180
+ # check if nadir point is specified
181
+ # if not specified, try to calculate corrected nadir point
182
+ if nadir is not None:
183
+ nadir_point = nadir
184
+ elif problem.get_nadir_point() is not None:
185
+ nadir_point = get_corrected_nadir(problem)
186
+ else:
187
+ msg = "Nadir point not defined!"
188
+ raise ScalarizationError(msg)
189
+
190
+ if any(value is None for value in ideal_point.values()) or any(value is None for value in nadir_point.values()):
191
+ msg = f"There are undefined values in either the ideal ({ideal_point}) or the nadir point ({nadir_point})."
192
+ raise ScalarizationError(msg)
193
+
194
+ # Build the max term
195
+ max_operands = [
196
+ (
197
+ f"({obj.symbol}_min - {reference_point[obj.symbol]}{' * -1' if obj.maximize else ''}) "
198
+ f"/ ({nadir_point[obj.symbol]} - ({ideal_point[obj.symbol]} - {delta}))"
199
+ )
200
+ for obj in problem.objectives
201
+ ]
202
+ max_term = f"{Op.MAX}({', '.join(max_operands)})"
203
+
204
+ # Build the augmentation term
205
+ if not reference_in_aug:
206
+ aug_operands = [
207
+ f"{obj.symbol}_min / ({nadir_point[obj.symbol]} - ({ideal_point[obj.symbol]} - {delta}))"
208
+ for obj in problem.objectives
209
+ ]
210
+ else:
211
+ aug_operands = [
212
+ (
213
+ f"({obj.symbol}_min - {reference_point[obj.symbol]}{' * -1' if obj.maximize else 1}) "
214
+ f"/ ({nadir_point[obj.symbol]} - ({ideal_point[obj.symbol]} - {delta}))"
215
+ )
216
+ for obj in problem.objectives
217
+ ]
218
+
219
+ aug_term = " + ".join(aug_operands)
220
+
221
+ asf_function = f"{max_term} + {rho} * ({aug_term})"
222
+
223
+ # Add the function to the problem
224
+ scalarization_function = ScalarizationFunction(
225
+ name="Achievement scalarizing function",
226
+ symbol=symbol,
227
+ func=asf_function,
228
+ is_linear=False,
229
+ is_convex=False,
230
+ is_twice_differentiable=False,
231
+ )
232
+ return problem.add_scalarization(scalarization_function), symbol
233
+
234
+
235
+ def add_asf_generic_diff( # noqa: PLR0913
236
+ problem: Problem,
237
+ symbol: str,
238
+ reference_point: dict[str, float],
239
+ weights: dict[str, float],
240
+ reference_point_aug: dict[str, float] | None = None,
241
+ weights_aug: dict[str, float] | None = None,
242
+ rho: float = 1e-6,
243
+ ) -> tuple[Problem, str]:
244
+ r"""Adds the differentiable variant of the generic achievement scalarizing function.
245
+
246
+ \begin{align*}
247
+ \min \quad & \alpha + \rho \sum_{i=1}^k \frac{f_i(\mathbf{x})}{w_i} \\
248
+ \text{s.t.} \quad & \frac{f_i(\mathbf{x}) - q_i}{w_i} - \alpha \leq 0,\\
249
+ & \mathbf{x} \in S,
250
+ \end{align*}
251
+
252
+ where $f_i$ are objective functions, $q_i$ is a component of the reference point,
253
+ and $w_i$ are components of the weight vector (which are assumed to be positive),
254
+ $\rho$ and $\delta$ are small scalar values, $S$ is the feasible solution
255
+ space of the original problem, and $\alpha$ is an auxiliary variable.
256
+ The summation term in the scalarization is known as the _augmentation term_.
257
+ If a reference point is chosen to be used in the augmentation term, e.g., a separate
258
+ reference point for the augmentation term is given (`reference_point_aug`), then
259
+ the reference point components are subtracted from the objective function values
260
+ in the nominator of the augmentation term. That is:
261
+
262
+ \begin{align*}
263
+ \min \quad & \alpha + \rho \sum_{i=1}^k \frac{f_i(\mathbf{x}) - q_i}{w_i} \\
264
+ \text{s.t.} \quad & \frac{f_i(\mathbf{x}) - q_i}{w_i} - \alpha \leq 0,\\
265
+ & \mathbf{x} \in S,
266
+ \end{align*}
267
+
268
+ References:
269
+ Wierzbicki, A. P. (1982). A mathematical basis for satisficing decision
270
+ making. Mathematical modelling, 3(5), 391-405.
271
+
272
+ Args:
273
+ problem (Problem): the problem the scalarization is added to.
274
+ symbol (str): the symbol given to the added scalarization.
275
+ reference_point (dict[str, float]): a dict with keys corresponding to objective
276
+ function symbols and values to reference point components, i.e.,
277
+ aspiration levels.
278
+ weights (dict[str, float]): the weights to be used in the scalarization function. Must be positive.
279
+ reference_point_aug (dict[str, float], optional): a dict with keys corresponding to objective
280
+ function symbols and values to reference point components for the augmentation term, i.e.,
281
+ aspiration levels. Defeults to None.
282
+ weights_aug (dict[str, float], optional): the weights to be used in the scalarization function's
283
+ augmentation term. Must be positive. Defaults to None.
284
+ rho (float, optional): a small scalar value to scale the sum in the objective
285
+ function of the scalarization. Defaults to 1e-6.
286
+
287
+ Returns:
288
+ tuple[Problem, str]: a tuple with the copy of the problem with the added
289
+ scalarization and the symbol of the added scalarization.
290
+ """
291
+ # check reference point
292
+ if not objective_dict_has_all_symbols(problem, reference_point):
293
+ msg = f"The give reference point {reference_point} is missing a value for one or more objectives."
294
+ raise ScalarizationError(msg)
295
+
296
+ # check augmentation term reference point
297
+ if reference_point_aug is not None and not objective_dict_has_all_symbols(problem, reference_point_aug):
298
+ msg = (
299
+ f"The given reference point for the augmentation term {reference_point_aug} "
300
+ "does not have a component defined for all the objectives."
301
+ )
302
+ raise ScalarizationError(msg)
303
+
304
+ # check the weight vector
305
+ if not objective_dict_has_all_symbols(problem, weights):
306
+ msg = f"The given weight vector {weights} is missing a value for one or more objectives."
307
+ raise ScalarizationError(msg)
308
+
309
+ # check the weight vector for the augmentation term
310
+ if weights_aug is not None and not objective_dict_has_all_symbols(problem, weights_aug):
311
+ msg = f"The given weight vector {weights_aug} is missing a value for one or more objectives."
312
+ raise ScalarizationError(msg)
313
+
314
+ corrected_rp = flip_maximized_objective_values(problem, reference_point)
315
+ if reference_point_aug is not None:
316
+ corrected_rp_aug = flip_maximized_objective_values(problem, reference_point_aug)
317
+
318
+ # define the auxiliary variable
319
+ alpha = Variable(
320
+ name="alpha",
321
+ symbol="_alpha",
322
+ variable_type=VariableTypeEnum.real,
323
+ lowerbound=-float("Inf"),
324
+ upperbound=float("Inf"),
325
+ initial_value=1.0,
326
+ )
327
+
328
+ # define the augmentation term
329
+ if reference_point_aug is None and weights_aug is None:
330
+ # no reference point in augmentation term
331
+ # same weights for both terms
332
+ aug_expr = " + ".join([f"({obj.symbol}_min / {weights[obj.symbol]})" for obj in problem.objectives])
333
+ elif reference_point_aug is None and weights_aug is not None:
334
+ # different weights provided for augmentation term
335
+ aug_expr = " + ".join([f"({obj.symbol}_min / {weights_aug[obj.symbol]})" for obj in problem.objectives])
336
+ elif reference_point_aug is not None and weights_aug is None:
337
+ # reference point in augmentation term
338
+ aug_expr = " + ".join(
339
+ [
340
+ f"(({obj.symbol}_min - {corrected_rp_aug[obj.symbol]}) / {weights[obj.symbol]})"
341
+ for obj in problem.objectives
342
+ ]
343
+ )
344
+ else:
345
+ aug_expr = " + ".join(
346
+ [
347
+ f"(({obj.symbol}_min - {corrected_rp_aug[obj.symbol]}) / {weights_aug[obj.symbol]})"
348
+ for obj in problem.objectives
349
+ ]
350
+ )
351
+
352
+ target_expr = f"_alpha + {rho}*" + f"({aug_expr})"
353
+ scalarization = ScalarizationFunction(
354
+ name="Generic ASF scalarization objective function",
355
+ symbol=symbol,
356
+ func=target_expr,
357
+ is_convex=problem.is_convex,
358
+ is_linear=problem.is_linear,
359
+ is_twice_differentiable=problem.is_twice_differentiable,
360
+ )
361
+
362
+ constraints = []
363
+
364
+ for obj in problem.objectives:
365
+ expr = f"({obj.symbol}_min - {corrected_rp[obj.symbol]}) / {weights[obj.symbol]} - _alpha"
366
+
367
+ # since we are subtracting a constant value, the linearity, convexity,
368
+ # and differentiability of the objective function, and hence the
369
+ # constraint, should not change.
370
+ constraints.append(
371
+ Constraint(
372
+ name=f"Constraint for {obj.symbol}",
373
+ symbol=f"{obj.symbol}_con",
374
+ func=expr,
375
+ cons_type=ConstraintTypeEnum.LTE,
376
+ is_linear=obj.is_linear,
377
+ is_convex=obj.is_convex,
378
+ is_twice_differentiable=obj.is_twice_differentiable,
379
+ )
380
+ )
381
+
382
+ _problem = problem.add_variables([alpha])
383
+ _problem = _problem.add_scalarization(scalarization)
384
+ return _problem.add_constraints(constraints), symbol
385
+
386
+
387
+ def add_asf_generic_nondiff( # noqa: PLR0913
388
+ problem: Problem,
389
+ symbol: str,
390
+ reference_point: dict[str, float],
391
+ weights: dict[str, float],
392
+ reference_point_aug: dict[str, float] | None = None,
393
+ weights_aug: dict[str, float] | None = None,
394
+ rho: float = 0.000001,
395
+ ) -> tuple[Problem, str]:
396
+ r"""Adds the generic achievement scalarizing function to a problem with the given reference point, and weights.
397
+
398
+ This is the non-differentiable variant of the generic achievement scalarizing function, which
399
+ means the resulting scalarization function is non-differentiable. Compared to `add_asf_nondiff`, this
400
+ variant is useful, when the problem being scalarized does not have a defined ideal or nadir point,
401
+ or both. The weights should be non-zero to avoid zero division.
402
+
403
+ The scalarization is defined as follows:
404
+
405
+ \begin{equation}
406
+ \mathcal{S}_\text{ASF}(F(\mathbf{x}); \mathbf{q}, \mathbf{w}) =
407
+ \underset{i=1,\ldots,k}{\text{max}}
408
+ \left[
409
+ \frac{f_i(\mathbf{x}) - q_i}{w_i}
410
+ \right]
411
+ + \rho\sum_{i=1}^{k} \frac{f_i(\mathbf{x})}{w_i},
412
+ \end{equation}
413
+
414
+ where $\mathbf{q} = [q_1,\dots,q_k]$ is a reference point, $\mathbf{w} =
415
+ [w_1,\dots,w_k]$ are weights, $k$ is the number of objective functions, and
416
+ $\delta$ and $\rho$ are small scalar values. The summation term in the
417
+ scalarization is known as the _augmentation term_. If a reference point is
418
+ chosen to be used in the augmentation term, e.g., a separate
419
+ reference point for the augmentation term is given (`reference_point_aug`), then
420
+ the reference point components are subtracted from the objective function values
421
+ in the nominator of the augmentation term. That is:
422
+
423
+ \begin{equation}
424
+ \mathcal{S}_\text{ASF}(F(\mathbf{x}); \mathbf{q}, \mathbf{w}) =
425
+ \underset{i=1,\ldots,k}{\text{max}}
426
+ \left[
427
+ \frac{f_i(\mathbf{x}) - q_i}{w_i}
428
+ \right]
429
+ + \rho\sum_{i=1}^{k} \frac{f_i(\mathbf{x}) - q_i}{w_i}.
430
+ \end{equation}
431
+
432
+ Args:
433
+ problem (Problem): the problem to which the scalarization function should be added.
434
+ symbol (str): the symbol to reference the added scalarization function.
435
+ reference_point (dict[str, float]): a reference point with as many components as there are objectives.
436
+ weights (dict[str, float]): the weights to be used in the scalarization function. must be positive.
437
+ reference_point_aug (dict[str, float], optional): a dict with keys corresponding to objective
438
+ function symbols and values to reference point components for the augmentation term, i.e.,
439
+ aspiration levels. Defeults to None.
440
+ weights_aug (dict[str, float], optional): the weights to be used in the scalarization function's
441
+ augmentation term. Must be positive. Defaults to None.
442
+ rho (float, optional): the weight factor used in the augmentation term. Defaults to 0.000001.
443
+
444
+ Raises:
445
+ ScalarizationError: If either the reference point or the weights given are missing any of the objective
446
+ components.
447
+ ScalarizationError: If any of the ideal or nadir point values are undefined (None).
448
+
449
+ Returns:
450
+ tuple[Problem, str]: A tuple containing a copy of the problem with the scalarization function added,
451
+ and the symbol of the added scalarization function.
452
+ """
453
+ # check reference point
454
+ if not objective_dict_has_all_symbols(problem, reference_point):
455
+ msg = f"The give reference point {reference_point} is missing a value for one or more objectives."
456
+ raise ScalarizationError(msg)
457
+
458
+ # check augmentation term reference point
459
+ if reference_point_aug is not None and not objective_dict_has_all_symbols(problem, reference_point_aug):
460
+ msg = (
461
+ f"The given reference point for the augmentation term {reference_point_aug} "
462
+ "does not have a component defined for all the objectives."
463
+ )
464
+ raise ScalarizationError(msg)
465
+
466
+ # check the weight vector
467
+ if not objective_dict_has_all_symbols(problem, weights):
468
+ msg = f"The given weight vector {weights} is missing a value for one or more objectives."
469
+ raise ScalarizationError(msg)
470
+
471
+ # check the weight vector for the augmentation term
472
+ if weights_aug is not None and not objective_dict_has_all_symbols(problem, weights_aug):
473
+ msg = f"The given weight vector {weights_aug} is missing a value for one or more objectives."
474
+ raise ScalarizationError(msg)
475
+
476
+ # get the corrected reference point
477
+ corrected_rp = flip_maximized_objective_values(problem, reference_point)
478
+ if reference_point_aug is not None:
479
+ corrected_rp_aug = flip_maximized_objective_values(problem, reference_point_aug)
480
+
481
+ # Build the max term
482
+ max_operands = [
483
+ (f"({obj.symbol}_min - {corrected_rp[obj.symbol]}) / ({weights[obj.symbol]})") for obj in problem.objectives
484
+ ]
485
+ max_term = f"{Op.MAX}({', '.join(max_operands)})"
486
+
487
+ # Build the augmentation term
488
+ if reference_point_aug is None and weights_aug is None:
489
+ # no reference point in augmentation term
490
+ # same weights for both terms
491
+ aug_expr = " + ".join([f"({obj.symbol}_min / {weights[obj.symbol]})" for obj in problem.objectives])
492
+ elif reference_point_aug is None and weights_aug is not None:
493
+ # different weights provided for augmentation term
494
+ aug_expr = " + ".join([f"({obj.symbol}_min / {weights_aug[obj.symbol]})" for obj in problem.objectives])
495
+ elif reference_point_aug is not None and weights_aug is None:
496
+ # reference point in augmentation term
497
+ aug_expr = " + ".join(
498
+ [
499
+ f"(({obj.symbol}_min - {corrected_rp_aug[obj.symbol]}) / {weights[obj.symbol]})"
500
+ for obj in problem.objectives
501
+ ]
502
+ )
503
+ else:
504
+ aug_expr = " + ".join(
505
+ [
506
+ f"(({obj.symbol}_min - {corrected_rp_aug[obj.symbol]}) / {weights_aug[obj.symbol]})"
507
+ for obj in problem.objectives
508
+ ]
509
+ )
510
+
511
+ # Collect the terms
512
+ sf = f"{max_term} + {rho} * ({aug_expr})"
513
+
514
+ # Add the function to the problem
515
+ scalarization_function = ScalarizationFunction(
516
+ name="Generic achievement scalarizing function",
517
+ symbol=symbol,
518
+ func=sf,
519
+ is_linear=False,
520
+ is_convex=False,
521
+ is_twice_differentiable=False,
522
+ )
523
+ return problem.add_scalarization(scalarization_function), symbol
524
+
525
+
526
+ def add_nimbus_sf_diff( # noqa: PLR0913
527
+ problem: Problem,
528
+ symbol: str,
529
+ classifications: dict[str, tuple[str, float | None]],
530
+ current_objective_vector: dict[str, float],
531
+ ideal: dict[str, float] | None = None,
532
+ nadir: dict[str, float] | None = None,
533
+ delta: float = 0.000001,
534
+ rho: float = 0.000001,
535
+ ) -> Problem:
536
+ r"""Implements the differentiable variant of the NIMBUS scalarization function.
537
+
538
+ \begin{align*}
539
+ \min \quad & \alpha + \rho \sum_{i =1}^k \frac{f_i(\mathbf{x})}{z_i^{nad} - z_i^{\star\star}} \\
540
+ \text{s.t.} \quad & \frac{f_i(\mathbf{x}) - z_i^*}{z_i^{nad} - z_i^{\star\star}} -
541
+ \alpha \leq 0 \quad & \forall i \in I^< \\
542
+ & \frac{f_i(\mathbf{x}) - \hat{z}_i}{z_i^{nad} - z_i^{\star\star}} - \alpha \leq 0 \quad &
543
+ \forall i \in I^\leq \\
544
+ & f_i(\mathbf{x}) - f_i(\mathbf{x_c}) \leq 0 \quad & \forall i \in I^< \cup I^\leq \cup I^= \\
545
+ & f_i(\mathbf{x}) - \epsilon_i \leq 0 \quad & \forall i \in I^\geq \\
546
+ & \mathbf{x} \in S,
547
+ \end{align*}
548
+
549
+ where $f_i$ are objective functions, $f_i(\mathbf{x_c})$ is a component of
550
+ the current objective function, $\hat{z}_i$ is an aspiration level,
551
+ $\varepsilon_i$ is a reservation level, $z_i^\star$ is a component of the
552
+ ideal point, $z_i^{\star\star} = z_i^\star - \delta$ is a component of the
553
+ utopian point, $z_i^\text{nad}$ is a component of the nadir point, $\rho$ is
554
+ a small scalar, $S$ is the feasible solution space of the problem (i.e., it
555
+ means the other constraints of the problem being solved should be accounted
556
+ for as well), and $\alpha$ is an auxiliary variable.
557
+
558
+ The $I$-sets are related to the classifications given to each objective function value
559
+ in respect to the current objective vector (e.g., by a decision maker). They
560
+ are as follows:
561
+
562
+ - $I^{<}$: values that should improve,
563
+ - $I^{\leq}$: values that should improve until a given aspiration level $\hat{z}_i$,
564
+ - $I^{=}$: values that are fine as they are,
565
+ - $I^{\geq}$: values that can be impaired until some reservation level $\varepsilon_i$, and
566
+ - $I^{\diamond}$: values that are allowed to change freely (not present explicitly in this scalarization function).
567
+
568
+ The aspiration levels and the reservation levels are supplied for each classification, when relevant, in
569
+ the argument `classifications` as follows:
570
+
571
+ ```python
572
+ classifications = {
573
+ "f_1": ("<", None),
574
+ "f_2": ("<=", 42.1),
575
+ "f_3": (">=", 22.2),
576
+ "f_4": ("0", None)
577
+ }
578
+ ```
579
+
580
+ Here, we have assumed four objective functions. The key of the dict is a function's symbol, and the tuple
581
+ consists of a pair where the left element is the classification (self explanatory, '0' is for objective values
582
+ that may change freely), the right element is either `None` or an aspiration or a reservation level
583
+ depending on the classification.
584
+
585
+ References:
586
+ Miettinen, K., & Mäkelä, M. M. (2002). On scalarizing functions in
587
+ multiobjective optimization. OR Spectrum, 24(2), 193-213.
588
+
589
+
590
+ Args:
591
+ problem (Problem): the problem to be scalarized.
592
+ symbol (str): the symbol given to the scalarization function, i.e., target of the optimization.
593
+ classifications (dict[str, tuple[str, float | None]]): a dict, where the key is a symbol
594
+ of an objective function, and the value is a tuple with a classification and an aspiration
595
+ or a reservation level, or `None`, depending on the classification. See above for an
596
+ explanation.
597
+ current_objective_vector (dict[str, float]): the current objective vector that corresponds to
598
+ a Pareto optimal solution. The classifications are assumed to been given in respect to
599
+ this vector.
600
+ ideal (dict[str, float], optional): ideal point values. If not given, attempt will be made
601
+ to calculate ideal point from problem.
602
+ nadir (dict[str, float], optional): nadir point values. If not given, attempt will be made
603
+ to calculate nadir point from problem.
604
+ delta (float, optional): a small scalar used to define the utopian point. Defaults to 0.000001.
605
+ rho (float, optional): a small scalar used in the augmentation term. Defaults to 0.000001.
606
+
607
+ Returns:
608
+ tuple[Problem, str]: a tuple with a copy of the problem with the added scalarizations and the
609
+ symbol of the scalarization.
610
+ """
611
+ # check that classifications have been provided for all objective functions
612
+ if not objective_dict_has_all_symbols(problem, classifications):
613
+ msg = (
614
+ f"The given classifications {classifications} do not define "
615
+ "a classification for all the objective functions."
616
+ )
617
+ raise ScalarizationError(msg)
618
+
619
+ # check that at least one objective function is allowed to be improved and one is
620
+ # allowed to worsen
621
+ if not any(classifications[obj.symbol][0] in ["<", "<="] for obj in problem.objectives) or not any(
622
+ classifications[obj.symbol][0] in [">=", "0"] for obj in problem.objectives
623
+ ):
624
+ msg = (
625
+ f"The given classifications {classifications} should allow at least one objective function value "
626
+ "to improve and one to worsen."
627
+ )
628
+ raise ScalarizationError(msg)
629
+
630
+ # check if ideal point is specified
631
+ # if not specified, try to calculate corrected ideal point
632
+ if ideal is not None:
633
+ ideal_point = ideal
634
+ elif problem.get_ideal_point() is not None:
635
+ ideal_point = get_corrected_ideal(problem)
636
+ else:
637
+ msg = "Ideal point not defined!"
638
+ raise ScalarizationError(msg)
639
+
640
+ # check if nadir point is specified
641
+ # if not specified, try to calculate corrected nadir point
642
+ if nadir is not None:
643
+ nadir_point = nadir
644
+ elif problem.get_nadir_point() is not None:
645
+ nadir_point = get_corrected_nadir(problem)
646
+ else:
647
+ msg = "Nadir point not defined!"
648
+ raise ScalarizationError(msg)
649
+
650
+ # define the auxiliary variable
651
+ alpha = Variable(
652
+ name="alpha",
653
+ symbol="_alpha",
654
+ variable_type=VariableTypeEnum.real,
655
+ lowerbound=-float("Inf"),
656
+ upperbound=float("Inf"),
657
+ initial_value=1.0,
658
+ )
659
+
660
+ # define the objective function of the scalarization
661
+ aug_expr = " + ".join(
662
+ [
663
+ f"{obj.symbol}_min / ({nadir_point[obj.symbol]} - {ideal_point[obj.symbol] - delta})"
664
+ for obj in problem.objectives
665
+ ]
666
+ )
667
+
668
+ target_expr = f"_alpha + {rho}*" + f"({aug_expr})"
669
+ scalarization = ScalarizationFunction(
670
+ name="NIMBUS scalarization objective function",
671
+ symbol=symbol,
672
+ func=target_expr,
673
+ is_linear=problem.is_linear,
674
+ is_convex=problem.is_convex,
675
+ is_twice_differentiable=problem.is_twice_differentiable,
676
+ )
677
+
678
+ constraints = []
679
+
680
+ # create all the constraints
681
+ for obj in problem.objectives:
682
+ _symbol = obj.symbol
683
+ match classifications[_symbol]:
684
+ case ("<", _):
685
+ expr = (
686
+ f"({_symbol}_min - {ideal_point[_symbol]}) / "
687
+ f"({nadir_point[_symbol] - (ideal_point[_symbol] - delta)}) - _alpha"
688
+ )
689
+ constraints.append(
690
+ Constraint(
691
+ name=f"improvement constraint for {_symbol}",
692
+ symbol=f"{_symbol}_lt",
693
+ func=expr,
694
+ cons_type=ConstraintTypeEnum.LTE,
695
+ is_linear=problem.is_linear,
696
+ is_convex=problem.is_convex,
697
+ is_twice_differentiable=problem.is_twice_differentiable,
698
+ )
699
+ )
700
+
701
+ # if obj is to be maximized, then the current objective vector value needs to be multiplied by -1
702
+ expr = f"{_symbol}_min - {current_objective_vector[_symbol]}{' * -1' if obj.maximize else ''}"
703
+ constraints.append(
704
+ Constraint(
705
+ name=f"stay at least equal constraint for {_symbol}",
706
+ symbol=f"{_symbol}_eq",
707
+ func=expr,
708
+ cons_type=ConstraintTypeEnum.LTE,
709
+ is_linear=problem.is_linear,
710
+ is_convex=problem.is_convex,
711
+ is_twice_differentiable=problem.is_twice_differentiable,
712
+ )
713
+ )
714
+ case ("<=", aspiration):
715
+ # if obj is to be maximized, then the current reservation value needs to be multiplied by -1
716
+ expr = (
717
+ f"({_symbol}_min - {aspiration}{' * -1' if obj.maximize else ''}) / "
718
+ f"({nadir_point[_symbol]} - {ideal_point[_symbol] - delta}) - _alpha"
719
+ )
720
+ constraints.append(
721
+ Constraint(
722
+ name=f"improvement until constraint for {_symbol}",
723
+ symbol=f"{_symbol}_lte",
724
+ func=expr,
725
+ cons_type=ConstraintTypeEnum.LTE,
726
+ is_linear=problem.is_linear,
727
+ is_convex=problem.is_convex,
728
+ is_twice_differentiable=problem.is_twice_differentiable,
729
+ )
730
+ )
731
+
732
+ # if obj is to be maximized, then the current objective vector value needs to be multiplied by -1
733
+ expr = f"{_symbol}_min - {current_objective_vector[_symbol]}{' * -1' if obj.maximize else ''}"
734
+ constraints.append(
735
+ Constraint(
736
+ name=f"stay at least equal constraint for {_symbol}",
737
+ symbol=f"{_symbol}_eq",
738
+ func=expr,
739
+ cons_type=ConstraintTypeEnum.LTE,
740
+ is_linear=problem.is_linear,
741
+ is_convex=problem.is_convex,
742
+ is_twice_differentiable=problem.is_twice_differentiable,
743
+ )
744
+ )
745
+ case ("=", _):
746
+ # if obj is to be maximized, then the current objective vector value needs to be multiplied by -1
747
+ expr = f"{_symbol}_min - {current_objective_vector[_symbol]}{' * -1' if obj.maximize else ''}"
748
+ constraints.append(
749
+ Constraint(
750
+ name=f"stay at least equal constraint for {_symbol}",
751
+ symbol=f"{_symbol}_eq",
752
+ func=expr,
753
+ cons_type=ConstraintTypeEnum.LTE,
754
+ is_linear=problem.is_linear,
755
+ is_convex=problem.is_convex,
756
+ is_twice_differentiable=problem.is_twice_differentiable,
757
+ )
758
+ )
759
+ case (">=", reservation):
760
+ # if obj is to be maximized, then the reservation value needs to be multiplied by -1
761
+ expr = f"{_symbol}_min - {reservation}{' * -1' if obj.maximize else ''}"
762
+ constraints.append(
763
+ Constraint(
764
+ name=f"worsen until constriant for {_symbol}",
765
+ symbol=f"{_symbol}_gte",
766
+ func=expr,
767
+ cons_type=ConstraintTypeEnum.LTE,
768
+ is_linear=problem.is_linear,
769
+ is_convex=problem.is_convex,
770
+ is_twice_differentiable=problem.is_twice_differentiable,
771
+ )
772
+ )
773
+ case ("0", _):
774
+ # not relevant for this scalarization
775
+ pass
776
+ case (c, _):
777
+ msg = (
778
+ f"Warning! The classification {c} was supplied, but it is not supported."
779
+ "Must be one of ['<', '<=', '0', '=', '>=']"
780
+ )
781
+
782
+ # add the auxiliary variable, scalarization, and constraints
783
+ _problem = problem.add_variables([alpha])
784
+ _problem = _problem.add_scalarization(scalarization)
785
+ return _problem.add_constraints(constraints), symbol
786
+
787
+
788
+ def add_nimbus_sf_nondiff( # noqa: PLR0913
789
+ problem: Problem,
790
+ symbol: str,
791
+ classifications: dict[str, tuple[str, float | None]],
792
+ current_objective_vector: dict[str, float],
793
+ ideal: dict[str, float] | None = None,
794
+ nadir: dict[str, float] | None = None,
795
+ delta: float = 0.000001,
796
+ rho: float = 0.000001,
797
+ ) -> Problem:
798
+ r"""Implements the non-differentiable variant of the NIMBUS scalarization function.
799
+
800
+ \begin{align*}
801
+ \underset{\mathbf{x}}{\min}
802
+ \underset{\substack{j \in I^\leq \\i \in I^<}}{\max}
803
+ &\left[ \frac{f_i(\mathbf{x}) - z_i^\star}{z_i^\text{nad} - z_i^{\star\star}},
804
+ \frac{f_j(\mathbf{x}) - \hat{z}_j}{z_j^\text{nad} - x_j^{\star\star}} \right]
805
+ +\rho \sum_{i =1}^k \frac{f_i(\mathbf{x})}{z_i^{nad} - z_i^{\star\star}} \\
806
+ \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^=,\\
807
+ & f_i(\mathbf{x}) - \epsilon_i \leq 0\quad&\forall i \in I^\geq,\\
808
+ & \mathbf{x} \in S,
809
+ \end{align*}
810
+
811
+ where $f_i$ are objective functions, $f_i(\mathbf{x_c})$ is a component of
812
+ the current objective function, $\hat{z}_i$ is an aspiration level,
813
+ $\varepsilon_i$ is a reservation level, $z_i^\star$ is a component of the
814
+ ideal point, $z_i^{\star\star} = z_i^\star - \delta$ is a component of the
815
+ utopian point, $z_i^\text{nad}$ is a component of the nadir point, $\rho$ is
816
+ a small scalar, and $S$ is the feasible solution space of the problem (i.e., it
817
+ means the other constraints of the problem being solved should be accounted
818
+ for as well).
819
+
820
+ The $I$-sets are related to the classifications given to each objective function value
821
+ in respect to the current objective vector (e.g., by a decision maker). They
822
+ are as follows:
823
+
824
+ - $I^{<}$: values that should improve,
825
+ - $I^{\leq}$: values that should improve until a given aspiration level $\hat{z}_i$,
826
+ - $I^{=}$: values that are fine as they are,
827
+ - $I^{\geq}$: values that can be impaired until some reservation level $\varepsilon_i$, and
828
+ - $I^{\diamond}$: values that are allowed to change freely (not present explicitly in this scalarization function).
829
+
830
+ The aspiration levels and the reservation levels are supplied for each classification, when relevant, in
831
+ the argument `classifications` as follows:
832
+
833
+ ```python
834
+ classifications = {
835
+ "f_1": ("<", None),
836
+ "f_2": ("<=", 42.1),
837
+ "f_3": (">=", 22.2),
838
+ "f_4": ("0", None)
839
+ }
840
+ ```
841
+
842
+ Here, we have assumed four objective functions. The key of the dict is a function's symbol, and the tuple
843
+ consists of a pair where the left element is the classification (self explanatory, '0' is for objective values
844
+ that may change freely), the right element is either `None` or an aspiration or a reservation level
845
+ depending on the classification.
846
+
847
+ References:
848
+ Miettinen, K., & Mäkelä, M. M. (2002). On scalarizing functions in
849
+ multiobjective optimization. OR Spectrum, 24(2), 193-213.
850
+
851
+
852
+ Args:
853
+ problem (Problem): the problem to be scalarized.
854
+ symbol (str): the symbol given to the scalarization function, i.e., target of the optimization.
855
+ classifications (dict[str, tuple[str, float | None]]): a dict, where the key is a symbol
856
+ of an objective function, and the value is a tuple with a classification and an aspiration
857
+ or a reservation level, or `None`, depending on the classification. See above for an
858
+ explanation.
859
+ current_objective_vector (dict[str, float]): the current objective vector that corresponds to
860
+ a Pareto optimal solution. The classifications are assumed to been given in respect to
861
+ this vector.
862
+ ideal (dict[str, float], optional): optional ideal point values. If not given, attempt will be made
863
+ to calculate ideal point from problem.
864
+ nadir (dict[str, float], optional): optional nadir point values. If not given, attempt will be made
865
+ to calculate nadir point from problem.
866
+ delta (float, optional): a small scalar used to define the utopian point. Defaults to 0.000001.
867
+ rho (float, optional): a small scalar used in the augmentation term. Defaults to 0.000001.
868
+
869
+ Returns:
870
+ tuple[Problem, str]: a tuple with a copy of the problem with the added scalarizations and the
871
+ symbol of the scalarization.
872
+ """
873
+ # check that classifications have been provided for all objective functions
874
+ if not objective_dict_has_all_symbols(problem, classifications):
875
+ msg = (
876
+ f"The given classifications {classifications} do not define "
877
+ "a classification for all the objective functions."
878
+ )
879
+ raise ScalarizationError(msg)
880
+
881
+ # check that at least one objective function is allowed to be improved and one is
882
+ # allowed to worsen
883
+ if not any(classifications[obj.symbol][0] in ["<", "<="] for obj in problem.objectives) or not any(
884
+ classifications[obj.symbol][0] in [">=", "0"] for obj in problem.objectives
885
+ ):
886
+ msg = (
887
+ f"The given classifications {classifications} should allow at least one objective function value "
888
+ "to improve and one to worsen."
889
+ )
890
+ raise ScalarizationError(msg)
891
+
892
+ # check if ideal point is specified
893
+ # if not specified, try to calculate corrected ideal point
894
+ if ideal is not None:
895
+ ideal_point = ideal
896
+ elif problem.get_ideal_point() is not None:
897
+ ideal_point = get_corrected_ideal(problem)
898
+ else:
899
+ msg = "Ideal point not defined!"
900
+ raise ScalarizationError(msg)
901
+
902
+ # check if nadir point is specified
903
+ # if not specified, try to calculate corrected nadir point
904
+ if nadir is not None:
905
+ nadir_point = nadir
906
+ elif problem.get_nadir_point() is not None:
907
+ nadir_point = get_corrected_nadir(problem)
908
+ else:
909
+ msg = "Nadir point not defined!"
910
+ raise ScalarizationError(msg)
911
+
912
+ corrected_current_point = flip_maximized_objective_values(problem, current_objective_vector)
913
+
914
+ # max term and constraints
915
+ max_args = []
916
+ constraints = []
917
+
918
+ for obj in problem.objectives:
919
+ _symbol = obj.symbol
920
+ match classifications[_symbol]:
921
+ case ("<", _):
922
+ max_expr = (
923
+ f"({_symbol}_min - {ideal_point[_symbol]}) / "
924
+ f"({nadir_point[_symbol]} - {ideal_point[_symbol] - delta})"
925
+ )
926
+ max_args.append(max_expr)
927
+
928
+ con_expr = f"{_symbol}_min - {corrected_current_point[_symbol]}"
929
+ constraints.append(
930
+ Constraint(
931
+ name=f"improvement constraint for {_symbol}",
932
+ symbol=f"{_symbol}_lt",
933
+ func=con_expr,
934
+ cons_type=ConstraintTypeEnum.LTE,
935
+ is_linear=problem.is_linear,
936
+ is_convex=problem.is_convex,
937
+ is_twice_differentiable=problem.is_twice_differentiable,
938
+ )
939
+ )
940
+
941
+ case ("<=", aspiration):
942
+ # if obj is to be maximized, then the current reservation value needs to be multiplied by -1
943
+ max_expr = (
944
+ f"({_symbol}_min - {aspiration * -1 if obj.maximize else aspiration}) / "
945
+ f"({nadir_point[_symbol]} - {ideal_point[_symbol] - delta})"
946
+ )
947
+ max_args.append(max_expr)
948
+
949
+ con_expr = f"{_symbol}_min - {corrected_current_point[_symbol]}"
950
+ constraints.append(
951
+ Constraint(
952
+ name=f"improvement until constraint for {_symbol}",
953
+ symbol=f"{_symbol}_lte",
954
+ func=con_expr,
955
+ cons_type=ConstraintTypeEnum.LTE,
956
+ is_linear=problem.is_linear,
957
+ is_convex=problem.is_convex,
958
+ is_twice_differentiable=problem.is_twice_differentiable,
959
+ )
960
+ )
961
+
962
+ case ("=", _):
963
+ con_expr = f"{_symbol}_min - {corrected_current_point[_symbol]}"
964
+ constraints.append(
965
+ Constraint(
966
+ name=f"Stay at least as good constraint for {_symbol}",
967
+ symbol=f"{_symbol}_eq",
968
+ func=con_expr,
969
+ cons_type=ConstraintTypeEnum.LTE,
970
+ is_linear=problem.is_linear,
971
+ is_convex=problem.is_convex,
972
+ is_twice_differentiable=problem.is_twice_differentiable,
973
+ )
974
+ )
975
+ case (">=", reservation):
976
+ con_expr = f"{_symbol}_min - {-1 * reservation if obj.maximize else reservation}"
977
+ constraints.append(
978
+ Constraint(
979
+ name=f"Worsen until constraint for {_symbol}",
980
+ symbol=f"{_symbol}_gte",
981
+ func=con_expr,
982
+ cons_type=ConstraintTypeEnum.LTE,
983
+ is_linear=problem.is_linear,
984
+ is_convex=problem.is_convex,
985
+ is_twice_differentiable=problem.is_twice_differentiable,
986
+ )
987
+ )
988
+ case ("0", _):
989
+ # not relevant for this scalarization
990
+ pass
991
+ case (c, _):
992
+ msg = (
993
+ f"Warning! The classification {c} was supplied, but it is not supported."
994
+ "Must be one of ['<', '<=', '0', '=', '>=']"
995
+ )
996
+
997
+ max_expr = f"Max({','.join(max_args)})"
998
+
999
+ # define the objective function of the scalarization
1000
+ aug_expr = " + ".join(
1001
+ [
1002
+ f"{obj.symbol}_min / ({nadir_point[obj.symbol]} - {ideal_point[obj.symbol] - delta})"
1003
+ for obj in problem.objectives
1004
+ ]
1005
+ )
1006
+
1007
+ target_expr = f"{max_expr} + {rho}*({aug_expr})"
1008
+ scalarization = ScalarizationFunction(
1009
+ name="NIMBUS scalarization objective function",
1010
+ symbol=symbol,
1011
+ func=target_expr,
1012
+ is_linear=False,
1013
+ is_convex=False,
1014
+ is_twice_differentiable=False,
1015
+ )
1016
+
1017
+ _problem = problem.add_scalarization(scalarization)
1018
+ return _problem.add_constraints(constraints), symbol
1019
+
1020
+
1021
+ def add_stom_sf_diff(
1022
+ problem: Problem,
1023
+ symbol: str,
1024
+ reference_point: dict[str, float],
1025
+ ideal: dict[str, float] | None = None,
1026
+ rho: float = 1e-6,
1027
+ delta: float = 1e-6,
1028
+ ) -> tuple[Problem, str]:
1029
+ r"""Adds the differentiable variant of the STOM scalarizing function.
1030
+
1031
+ \begin{align*}
1032
+ \min \quad & \alpha + \rho \sum_{i=1}^k \frac{f_i(\mathbf{x})}{\bar{z}_i - z_i^{\star\star}} \\
1033
+ \text{s.t.} \quad & \frac{f_i(\mathbf{x}) - z_i^{\star\star}}{\bar{z}_i
1034
+ - z_i^{\star\star}} - \alpha \leq 0 \quad & \forall i = 1,\dots,k\\
1035
+ & \mathbf{x} \in S,
1036
+ \end{align*}
1037
+
1038
+ where $f_i$ are objective functions, $z_i^{\star\star} = z_i^\star - \delta$ is
1039
+ a component of the utopian point, $\bar{z}_i$ is a component of the reference point,
1040
+ $\rho$ and $\delta$ are small scalar values, $S$ is the feasible solution
1041
+ space of the original problem, and $\alpha$ is an auxiliary variable.
1042
+
1043
+ References:
1044
+ H. Nakayama, Y. Sawaragi, Satisficing trade-off method for
1045
+ multiobjective programming, in: M. Grauer, A.P. Wierzbicki (Eds.),
1046
+ Interactive Decision Analysis, Springer Verlag, Berlin, 1984, pp.
1047
+ 113-122.
1048
+
1049
+ Args:
1050
+ problem (Problem): the problem the scalarization is added to.
1051
+ symbol (str): the symbol given to the added scalarization.
1052
+ reference_point (dict[str, float]): a dict with keys corresponding to objective
1053
+ function symbols and values to reference point components, i.e.,
1054
+ aspiration levels.
1055
+ ideal (dict[str, float], optional): ideal point values. If not given, attempt will be made
1056
+ to calculate ideal point from problem.
1057
+ rho (float, optional): a small scalar value to scale the sum in the objective
1058
+ function of the scalarization. Defaults to 1e-6.
1059
+ delta (float, optional): a small scalar value to define the utopian point. Defaults to 1e-6.
1060
+
1061
+ Returns:
1062
+ tuple[Problem, str]: a tuple with the copy of the problem with the added
1063
+ scalarization and the symbol of the added scalarization.
1064
+ """
1065
+ # check reference point
1066
+ if not objective_dict_has_all_symbols(problem, reference_point):
1067
+ msg = f"The give reference point {reference_point} is missing value for one or more objectives."
1068
+ raise ScalarizationError(msg)
1069
+
1070
+ # check if ideal point is specified
1071
+ # if not specified, try to calculate corrected ideal point
1072
+ if ideal is not None:
1073
+ ideal_point = ideal
1074
+ elif problem.get_ideal_point() is not None:
1075
+ ideal_point = get_corrected_ideal(problem)
1076
+ else:
1077
+ msg = "Ideal point not defined!"
1078
+ raise ScalarizationError(msg)
1079
+
1080
+ corrected_rp = flip_maximized_objective_values(problem, reference_point)
1081
+
1082
+ # define the auxiliary variable
1083
+ alpha = Variable(
1084
+ name="alpha",
1085
+ symbol="_alpha",
1086
+ variable_type=VariableTypeEnum.real,
1087
+ lowerbound=-float("Inf"),
1088
+ upperbound=float("Inf"),
1089
+ initial_value=1.0,
1090
+ )
1091
+
1092
+ # define the objective function of the scalarization
1093
+ aug_expr = " + ".join(
1094
+ [
1095
+ f"{obj.symbol}_min / ({(reference_point[obj.symbol] - ideal_point[obj.symbol]) + delta})"
1096
+ for obj in problem.objectives
1097
+ ]
1098
+ )
1099
+
1100
+ target_expr = f"_alpha + {rho}*" + f"({aug_expr})"
1101
+ scalarization = ScalarizationFunction(
1102
+ name="STOM scalarization objective function",
1103
+ symbol=symbol,
1104
+ func=target_expr,
1105
+ is_twice_differentiable=problem.is_twice_differentiable,
1106
+ is_linear=problem.is_linear,
1107
+ is_convex=problem.is_convex,
1108
+ )
1109
+
1110
+ constraints = []
1111
+
1112
+ for obj in problem.objectives:
1113
+ expr = (
1114
+ f"({obj.symbol}_min - {ideal_point[obj.symbol] - delta}) / "
1115
+ f"({(corrected_rp[obj.symbol] - ideal_point[obj.symbol]) + delta}) - _alpha"
1116
+ )
1117
+ constraints.append(
1118
+ Constraint(
1119
+ name=f"Max constraint for {obj.symbol}",
1120
+ symbol=f"{obj.symbol}_maxcon",
1121
+ func=expr,
1122
+ cons_type=ConstraintTypeEnum.LTE,
1123
+ is_twice_differentiable=obj.is_twice_differentiable,
1124
+ is_linear=obj.is_linear,
1125
+ is_convex=obj.is_convex,
1126
+ )
1127
+ )
1128
+
1129
+ _problem = problem.add_variables([alpha])
1130
+ _problem = _problem.add_scalarization(scalarization)
1131
+ return _problem.add_constraints(constraints), symbol
1132
+
1133
+
1134
+ def add_stom_sf_nondiff(
1135
+ problem: Problem,
1136
+ symbol: str,
1137
+ reference_point: dict[str, float],
1138
+ ideal: dict[str, float] | None = None,
1139
+ rho: float = 1e-6,
1140
+ delta: float = 1e-6,
1141
+ ) -> tuple[Problem, str]:
1142
+ r"""Adds the non-differentiable variant of the STOM scalarizing function.
1143
+
1144
+ \begin{align*}
1145
+ \underset{\mathbf{x}}{\min} \quad & \underset{i=1,\dots,k}{\max}\left[
1146
+ \frac{f_i(\mathbf{x}) - z_i^{\star\star}}{\bar{z}_i - z_i^{\star\star}}
1147
+ \right]
1148
+ + \rho \sum_{i=1}^k \frac{f_i(\mathbf{x})}{\bar{z}_i - z_i^{\star\star}} \\
1149
+ \text{s.t.}\quad & \mathbf{x} \in S,
1150
+ \end{align*}
1151
+
1152
+ where $f_i$ are objective functions, $z_i^{\star\star} = z_i^\star - \delta$ is
1153
+ a component of the utopian point, $\bar{z}_i$ is a component of the reference point,
1154
+ $\rho$ and $\delta$ are small scalar values, and $S$ is the feasible solution
1155
+ space of the original problem.
1156
+
1157
+ References:
1158
+ H. Nakayama, Y. Sawaragi, Satisficing trade-off method for
1159
+ multiobjective programming, in: M. Grauer, A.P. Wierzbicki (Eds.),
1160
+ Interactive Decision Analysis, Springer Verlag, Berlin, 1984, pp.
1161
+ 113-122.
1162
+
1163
+ Args:
1164
+ problem (Problem): the problem the scalarization is added to.
1165
+ symbol (str): the symbol given to the added scalarization.
1166
+ reference_point (dict[str, float]): a dict with keys corresponding to objective
1167
+ function symbols and values to reference point components, i.e.,
1168
+ aspiration levels.
1169
+ ideal (dict[str, float], optional): ideal point values. If not given, attempt will be made
1170
+ to calculate ideal point from problem.
1171
+ rho (float, optional): a small scalar value to scale the sum in the objective
1172
+ function of the scalarization. Defaults to 1e-6.
1173
+ delta (float, optional): a small scalar value to define the utopian point. Defaults to 1e-6.
1174
+
1175
+ Returns:
1176
+ tuple[Problem, str]: a tuple with the copy of the problem with the added
1177
+ scalarization and the symbol of the added scalarization.
1178
+ """
1179
+ # check reference point
1180
+ if not objective_dict_has_all_symbols(problem, reference_point):
1181
+ msg = f"The give reference point {reference_point} is missing value for one or more objectives."
1182
+ raise ScalarizationError(msg)
1183
+
1184
+ # check if ideal point is specified
1185
+ # if not specified, try to calculate corrected ideal point
1186
+ if ideal is not None:
1187
+ ideal_point = ideal
1188
+ elif problem.get_ideal_point() is not None:
1189
+ ideal_point = get_corrected_ideal(problem)
1190
+ else:
1191
+ msg = "Ideal point not defined!"
1192
+ raise ScalarizationError(msg)
1193
+
1194
+ corrected_rp = flip_maximized_objective_values(problem, reference_point)
1195
+
1196
+ # define the objective function of the scalarization
1197
+ max_expr = ", ".join(
1198
+ [
1199
+ (
1200
+ f"({obj.symbol}_min - {ideal_point[obj.symbol] - delta}) / "
1201
+ f"({(corrected_rp[obj.symbol] - ideal_point[obj.symbol]) + delta})"
1202
+ )
1203
+ for obj in problem.objectives
1204
+ ]
1205
+ )
1206
+ aug_expr = " + ".join(
1207
+ [
1208
+ f"{obj.symbol}_min / ({(reference_point[obj.symbol] - ideal_point[obj.symbol]) + delta})"
1209
+ for obj in problem.objectives
1210
+ ]
1211
+ )
1212
+
1213
+ target_expr = f"{Op.MAX}({max_expr}) + {rho}*" + f"({aug_expr})"
1214
+ scalarization = ScalarizationFunction(
1215
+ name="STOM scalarization objective function",
1216
+ symbol=symbol,
1217
+ func=target_expr,
1218
+ is_linear=False,
1219
+ is_convex=False,
1220
+ is_twice_differentiable=False,
1221
+ )
1222
+
1223
+ return problem.add_scalarization(scalarization), symbol
1224
+
1225
+
1226
+ def add_guess_sf_diff(
1227
+ problem: Problem,
1228
+ symbol: str,
1229
+ reference_point: dict[str, float],
1230
+ ideal: dict[str, float] | None = None,
1231
+ nadir: dict[str, float] | None = None,
1232
+ rho: float = 1e-6,
1233
+ delta: float = 1e-6,
1234
+ ) -> tuple[Problem, str]:
1235
+ r"""Adds the differentiable variant of the GUESS scalarizing function.
1236
+
1237
+ \begin{align*}
1238
+ \min \quad & \alpha + \rho \sum_{i=1}^k \frac{f_i(\mathbf{x})}{z_i^{nad} - \bar{z}_i},
1239
+ \quad & \\
1240
+ \text{s.t.} \quad & \frac{f_i(\mathbf{x}) - z_i^{nad}}{z_i^{nad} - \bar{z}_i}
1241
+ - \alpha \leq 0 \quad & \forall i \notin I^{\diamond},\\
1242
+ & \mathbf{x} \in S,
1243
+ \end{align*}
1244
+
1245
+ where $f_{i}$ are objective functions, $z_{i}^{nad}$ is a component of the
1246
+ nadir point, $\bar{z}_{i}$
1247
+ is a component of the reference point, $\rho$ is a small scalar
1248
+ value, and $S$ is the feasible solution space of the original problem. The
1249
+ index set $I^\diamond$ represents objective vectors whose values are free to
1250
+ change. The indices belonging to this set are interpreted as those objective
1251
+ vectors whose components in the reference point is set to be the the
1252
+ respective nadir point component of the problem. Note that in Buchanan (1997),
1253
+ the GUESS method considers all objective functions, i.e. $I^\diamond$ is
1254
+ an empty set. The functionality to have free-to-change objectives was added
1255
+ in Miettinen & Mäkelä (2006).
1256
+
1257
+ References:
1258
+ Buchanan, J. T. (1997). A naive approach for solving MCDM problems: The
1259
+ GUESS method. Journal of the Operational Research Society, 48, 202-206.
1260
+
1261
+ Miettinen, K., & Mäkelä, M. M. (2006). Synchronous approach in interactive
1262
+ multiobjective optimization. European Journal of Operational Research,
1263
+ 170(3), 909-922.
1264
+
1265
+ Args:
1266
+ problem (Problem): the problem the scalarization is added to.
1267
+ symbol (str): the symbol given to the added scalarization.
1268
+ reference_point (dict[str, float]): a dict with keys corresponding to objective
1269
+ function symbols and values to reference point components, i.e.,
1270
+ aspiration levels.
1271
+ ideal (dict[str, float], optional): ideal point values. If not given, attempt will be made
1272
+ to calculate ideal point from problem.
1273
+ nadir (dict[str, float], optional): nadir point values. If not given, attempt will be made
1274
+ to calculate nadir point from problem.
1275
+ rho (float, optional): a small scalar value to scale the sum in the objective
1276
+ function of the scalarization. Defaults to 1e-6.
1277
+ delta (float, optional): a small scalar value to define the utopian point. Defaults to 1e-6.
1278
+
1279
+ Returns:
1280
+ tuple[Problem, str]: a tuple with the copy of the problem with the added
1281
+ scalarization and the symbol of the added scalarization.
1282
+ """
1283
+ # check reference point
1284
+ if not objective_dict_has_all_symbols(problem, reference_point):
1285
+ msg = f"The give reference point {reference_point} is missing value for one or more objectives."
1286
+ raise ScalarizationError(msg)
1287
+
1288
+ # check if ideal point is specified
1289
+ # if not specified, try to calculate corrected ideal point
1290
+ if ideal is not None:
1291
+ ideal_point = ideal
1292
+ elif problem.get_ideal_point() is not None:
1293
+ ideal_point = get_corrected_ideal(problem)
1294
+ else:
1295
+ msg = "Ideal point not defined!"
1296
+ raise ScalarizationError(msg)
1297
+
1298
+ # check if nadir point is specified
1299
+ # if not specified, try to calculate corrected nadir point
1300
+ if nadir is not None:
1301
+ nadir_point = nadir
1302
+ elif problem.get_nadir_point() is not None:
1303
+ nadir_point = get_corrected_nadir(problem)
1304
+ else:
1305
+ msg = "Nadir point not defined!"
1306
+ raise ScalarizationError(msg)
1307
+
1308
+ corrected_rp = flip_maximized_objective_values(problem, reference_point)
1309
+
1310
+ # the indices that are free to change, set if component of reference point
1311
+ # has the corresponding nadir value, or if it is greater than the nadir value
1312
+ free_to_change = [
1313
+ sym
1314
+ for sym in corrected_rp
1315
+ if np.isclose(corrected_rp[sym], nadir_point[sym]) or corrected_rp[sym] > nadir_point[sym]
1316
+ ]
1317
+
1318
+ # define the auxiliary variable
1319
+ alpha = Variable(
1320
+ name="alpha",
1321
+ symbol="_alpha",
1322
+ variable_type=VariableTypeEnum.real,
1323
+ lowerbound=-float("Inf"),
1324
+ upperbound=float("Inf"),
1325
+ initial_value=1.0,
1326
+ )
1327
+
1328
+ # define the objective function of the scalarization
1329
+ aug_expr = " + ".join(
1330
+ [
1331
+ ( # Technically delta should be included (according to the paper), but I'm a rebel and don't want to add it
1332
+ f"{obj.symbol}_min / ({nadir_point[obj.symbol]} - {ideal_point[obj.symbol]})"
1333
+ if obj.symbol in free_to_change
1334
+ else f"{obj.symbol}_min / ({nadir_point[obj.symbol]} - {corrected_rp[obj.symbol]})"
1335
+ )
1336
+ for obj in problem.objectives
1337
+ ]
1338
+ )
1339
+
1340
+ target_expr = f"_alpha + {rho}*" + f"({aug_expr})"
1341
+ scalarization = ScalarizationFunction(
1342
+ name="GUESS scalarization objective function",
1343
+ symbol=symbol,
1344
+ func=target_expr,
1345
+ is_convex=problem.is_convex,
1346
+ is_linear=problem.is_linear,
1347
+ is_twice_differentiable=problem.is_twice_differentiable,
1348
+ )
1349
+
1350
+ constraints = []
1351
+
1352
+ for obj in problem.objectives:
1353
+ if obj.symbol in free_to_change:
1354
+ # if free to change, then do not add a constraint
1355
+ continue
1356
+
1357
+ # not free to change, add constraint
1358
+ expr = (
1359
+ f"({obj.symbol}_min - {nadir_point[obj.symbol]}) / "
1360
+ f"({nadir_point[obj.symbol]} - {corrected_rp[obj.symbol]}) - _alpha"
1361
+ )
1362
+
1363
+ constraints.append(
1364
+ Constraint(
1365
+ name=f"Constraint for {obj.symbol}",
1366
+ symbol=f"{obj.symbol}_con",
1367
+ func=expr,
1368
+ cons_type=ConstraintTypeEnum.LTE,
1369
+ is_linear=obj.is_linear,
1370
+ is_convex=obj.is_convex,
1371
+ is_twice_differentiable=obj.is_twice_differentiable,
1372
+ )
1373
+ )
1374
+
1375
+ _problem = problem.add_variables([alpha])
1376
+ _problem = _problem.add_scalarization(scalarization)
1377
+ return _problem.add_constraints(constraints), symbol
1378
+
1379
+
1380
+ def add_guess_sf_nondiff(
1381
+ problem: Problem,
1382
+ symbol: str,
1383
+ reference_point: dict[str, float],
1384
+ ideal: dict[str, float] | None = None,
1385
+ nadir: dict[str, float] | None = None,
1386
+ rho: float = 1e-6,
1387
+ ) -> tuple[Problem, str]:
1388
+ r"""Adds the non-differentiable variant of the GUESS scalarizing function.
1389
+
1390
+ \begin{align*}
1391
+ \underset{\mathbf{x}}{\min}\quad & \underset{i \notin I^\diamond}{\max}
1392
+ \left[
1393
+ \frac{f_i(\mathbf{x}) - z_i^{nad}}{z_i^{nad} - \bar{z}_i}
1394
+ \right]
1395
+ + \rho \sum_{i=1}^k \frac{f_i(\mathbf{x})}{z_i^{nad} - \bar{z}_i},
1396
+ \quad & \\
1397
+ \text{s.t.}\quad
1398
+ & \mathbf{x} \in S,
1399
+ \end{align*}
1400
+
1401
+ where $f_{i}$ are objective functions, $z_{i}^{nad}$ is a component of the
1402
+ nadir point, $\bar{z}_{i}$
1403
+ is a component of the reference point, $\rho$ is a small scalar
1404
+ value, and $S$ is the feasible solution space of the original problem. The
1405
+ index set $I^\diamond$ represents objective vectors whose values are free to
1406
+ change. The indices belonging to this set are interpreted as those objective
1407
+ vectors whose components in the reference point is set to be the the
1408
+ respective nadir point component of the problem. Note that in Buchanan (1997),
1409
+ the GUESS method considers all objective functions, i.e. $I^\diamond$ is
1410
+ an empty set. The functionality to have free-to-change objectives was added
1411
+ in Miettinen & Mäkelä (2006).
1412
+
1413
+ References:
1414
+ Buchanan, J. T. (1997). A naive approach for solving MCDM problems: The
1415
+ GUESS method. Journal of the Operational Research Society, 48, 202-206.
1416
+
1417
+ Miettinen, K., & Mäkelä, M. M. (2006). Synchronous approach in interactive
1418
+ multiobjective optimization. European Journal of Operational Research,
1419
+ 170(3), 909-922.
1420
+
1421
+ Args:
1422
+ problem (Problem): the problem the scalarization is added to.
1423
+ symbol (str): the symbol given to the added scalarization.
1424
+ reference_point (dict[str, float]): a dict with keys corresponding to objective
1425
+ function symbols and values to reference point components, i.e.,
1426
+ aspiration levels.
1427
+ ideal (dict[str, float], optional): ideal point values. If not given, attempt will be made
1428
+ to calculate ideal point from problem.
1429
+ nadir (dict[str, float], optional): nadir point values. If not given, attempt will be made
1430
+ to calculate nadir point from problem.
1431
+ rho (float, optional): a small scalar value to scale the sum in the objective
1432
+ function of the scalarization. Defaults to 1e-6.
1433
+ delta (float, optional): a small scalar to define the utopian point. Defaults to 1e-6.
1434
+
1435
+ Returns:
1436
+ tuple[Problem, str]: a tuple with the copy of the problem with the added
1437
+ scalarization and the symbol of the added scalarization.
1438
+ """
1439
+ # check reference point
1440
+ if not objective_dict_has_all_symbols(problem, reference_point):
1441
+ msg = f"The give reference point {reference_point} is missing value for one or more objectives."
1442
+ raise ScalarizationError(msg)
1443
+
1444
+ # check if ideal point is specified
1445
+ # if not specified, try to calculate corrected ideal point
1446
+ if ideal is not None:
1447
+ ideal_point = ideal
1448
+ elif problem.get_ideal_point() is not None:
1449
+ ideal_point = get_corrected_ideal(problem)
1450
+ else:
1451
+ msg = "Ideal point not defined!"
1452
+ raise ScalarizationError(msg)
1453
+
1454
+ # check if nadir point is specified
1455
+ # if not specified, try to calculate corrected nadir point
1456
+ if nadir is not None:
1457
+ nadir_point = nadir
1458
+ elif problem.get_nadir_point() is not None:
1459
+ nadir_point = get_corrected_nadir(problem)
1460
+ else:
1461
+ msg = "Nadir point not defined!"
1462
+ raise ScalarizationError(msg)
1463
+
1464
+ corrected_rp = flip_maximized_objective_values(problem, reference_point)
1465
+
1466
+ # the indices that are free to change, set if component of reference point
1467
+ # has the corresponding nadir value, or if it is greater than the nadir value
1468
+ free_to_change = [
1469
+ sym
1470
+ for sym in corrected_rp
1471
+ if np.isclose(corrected_rp[sym], nadir_point[sym]) or corrected_rp[sym] > nadir_point[sym]
1472
+ ]
1473
+
1474
+ # define the max expression of the scalarization
1475
+ # if the objective symbol belongs to the class I^diamond, then do not add it
1476
+ # to the max expression
1477
+ max_expr = ", ".join(
1478
+ [
1479
+ (
1480
+ f"({obj.symbol}_min - {(nadir_point[obj.symbol])}) / "
1481
+ f"({nadir_point[obj.symbol]} - {(corrected_rp[obj.symbol])})"
1482
+ )
1483
+ for obj in problem.objectives
1484
+ if obj.symbol not in free_to_change
1485
+ ]
1486
+ )
1487
+
1488
+ # define the augmentation term
1489
+ aug_expr = " + ".join(
1490
+ [
1491
+ ( # Technically delta should be included (according to the paper), but I'm a rebel and don't want to add it
1492
+ f"{obj.symbol}_min / ({nadir_point[obj.symbol]} - {ideal_point[obj.symbol]})"
1493
+ if obj.symbol in free_to_change
1494
+ else f"{obj.symbol}_min / ({nadir_point[obj.symbol]} - {corrected_rp[obj.symbol]})"
1495
+ )
1496
+ for obj in problem.objectives
1497
+ ]
1498
+ )
1499
+
1500
+ target_expr = f"{Op.MAX}({max_expr}) + {rho}*({aug_expr})"
1501
+ scalarization = ScalarizationFunction(
1502
+ name="GUESS scalarization objective function",
1503
+ symbol=symbol,
1504
+ func=target_expr,
1505
+ is_linear=False,
1506
+ is_convex=False,
1507
+ is_twice_differentiable=False,
1508
+ )
1509
+
1510
+ return problem.add_scalarization(scalarization), symbol
1511
+
1512
+
1513
+ def add_asf_diff(
1514
+ problem: Problem,
1515
+ symbol: str,
1516
+ reference_point: dict[str, float],
1517
+ ideal: dict[str, float] | None = None,
1518
+ nadir: dict[str, float] | None = None,
1519
+ rho: float = 1e-6,
1520
+ delta: float = 1e-6,
1521
+ ) -> tuple[Problem, str]:
1522
+ r"""Adds the differentiable variant of the achievement scalarizing function.
1523
+
1524
+ \begin{align*}
1525
+ \min \quad & \alpha + \rho \sum_{i=1}^k \frac{f_i(\mathbf{x})}{z_i^\text{nad} - z_i^{\star\star}} \\
1526
+ \text{s.t.} \quad & \frac{f_i(\mathbf{x}) - \bar{z}_i}{z_i^\text{nad}
1527
+ - z_i^{\star\star}} - \alpha \leq 0,\\
1528
+ & \mathbf{x} \in S,
1529
+ \end{align*}
1530
+
1531
+ where $f_i$ are objective functions, $z_i^{\star\star} = z_i^\star - \delta$ is
1532
+ a component of the utopian point, $\bar{z}_i$ is a component of the reference point,
1533
+ $\rho$ and $\delta$ are small scalar values, $S$ is the feasible solution
1534
+ space of the original problem, and $\alpha$ is an auxiliary variable.
1535
+
1536
+ References:
1537
+ Wierzbicki, A. P. (1982). A mathematical basis for satisficing decision
1538
+ making. Mathematical modelling, 3(5), 391-405.
1539
+
1540
+ Args:
1541
+ problem (Problem): the problem the scalarization is added to.
1542
+ symbol (str): the symbol given to the added scalarization.
1543
+ reference_point (dict[str, float]): a dict with keys corresponding to objective
1544
+ function symbols and values to reference point components, i.e.,
1545
+ aspiration levels.
1546
+ ideal (dict[str, float], optional): ideal point values. If not given, attempt will be made
1547
+ to calculate ideal point from problem.
1548
+ nadir (dict[str, float], optional): nadir point values. If not given, attempt will be made
1549
+ to calculate nadir point from problem.
1550
+ rho (float, optional): a small scalar value to scale the sum in the objective
1551
+ function of the scalarization. Defaults to 1e-6.
1552
+ delta (float, optional): a small scalar to define the utopian point. Defaults to 1e-6.
1553
+
1554
+ Returns:
1555
+ tuple[Problem, str]: a tuple with the copy of the problem with the added
1556
+ scalarization and the symbol of the added scalarization.
1557
+
1558
+ Todo:
1559
+ Add reference in augmentation term option!
1560
+ """
1561
+ # check reference point
1562
+ if not objective_dict_has_all_symbols(problem, reference_point):
1563
+ msg = f"The give reference point {reference_point} is missing value for one or more objectives."
1564
+ raise ScalarizationError(msg)
1565
+
1566
+ # check if ideal point is specified
1567
+ # if not specified, try to calculate corrected ideal point
1568
+ if ideal is not None:
1569
+ ideal_point = ideal
1570
+ elif problem.get_ideal_point() is not None:
1571
+ ideal_point = get_corrected_ideal(problem)
1572
+ else:
1573
+ msg = "Ideal point not defined!"
1574
+ raise ScalarizationError(msg)
1575
+
1576
+ # check if nadir point is specified
1577
+ # if not specified, try to calculate corrected nadir point
1578
+ if nadir is not None:
1579
+ nadir_point = nadir
1580
+ elif problem.get_nadir_point() is not None:
1581
+ nadir_point = get_corrected_nadir(problem)
1582
+ else:
1583
+ msg = "Nadir point not defined!"
1584
+ raise ScalarizationError(msg)
1585
+
1586
+ corrected_rp = flip_maximized_objective_values(problem, reference_point)
1587
+
1588
+ # define the auxiliary variable
1589
+ alpha = Variable(
1590
+ name="alpha",
1591
+ symbol="_alpha",
1592
+ variable_type=VariableTypeEnum.real,
1593
+ lowerbound=-float("Inf"),
1594
+ upperbound=float("Inf"),
1595
+ initial_value=1.0,
1596
+ )
1597
+
1598
+ # define the objective function of the scalarization
1599
+ aug_expr = " + ".join(
1600
+ [
1601
+ (f"{obj.symbol}_min / ({nadir_point[obj.symbol]} - {ideal_point[obj.symbol] - delta})")
1602
+ for obj in problem.objectives
1603
+ ]
1604
+ )
1605
+
1606
+ target_expr = f"_alpha + {rho}*" + f"({aug_expr})"
1607
+ scalarization = ScalarizationFunction(
1608
+ name="ASF scalarization objective function",
1609
+ symbol=symbol,
1610
+ func=target_expr,
1611
+ is_linear=problem.is_linear,
1612
+ is_convex=problem.is_convex,
1613
+ is_twice_differentiable=problem.is_twice_differentiable,
1614
+ )
1615
+
1616
+ constraints = []
1617
+
1618
+ for obj in problem.objectives:
1619
+ expr = (
1620
+ f"({obj.symbol}_min - {corrected_rp[obj.symbol]}) / "
1621
+ f"({nadir_point[obj.symbol]} - {ideal_point[obj.symbol] - delta}) - _alpha"
1622
+ )
1623
+
1624
+ constraints.append(
1625
+ Constraint(
1626
+ name=f"Constraint for {obj.symbol}",
1627
+ symbol=f"{obj.symbol}_con",
1628
+ func=expr,
1629
+ cons_type=ConstraintTypeEnum.LTE,
1630
+ is_linear=obj.is_linear,
1631
+ is_convex=obj.is_convex,
1632
+ is_twice_differentiable=obj.is_twice_differentiable,
1633
+ )
1634
+ )
1635
+
1636
+ _problem = problem.add_variables([alpha])
1637
+ _problem = _problem.add_scalarization(scalarization)
1638
+ return _problem.add_constraints(constraints), symbol
1639
+
1640
+
1641
+ def add_weighted_sums(problem: Problem, symbol: str, weights: dict[str, float]) -> tuple[Problem, str]:
1642
+ r"""Add the weighted sums scalarization to a problem with the given weights.
1643
+
1644
+ It is assumed that the weights add to 1.
1645
+
1646
+ The scalarization is defined as follows:
1647
+
1648
+ \begin{equation}
1649
+ \begin{aligned}
1650
+ & \mathcal{S}_\text{WS}(F(\mathbf{x});\mathbf{w}) = \sum_{i=1}^{k} w_i f_i(\mathbf{x}) \\
1651
+ & \text{s.t.} \sum_{i=1}^{k} w_i = 1,
1652
+ \end{aligned}
1653
+ \end{equation}
1654
+
1655
+ where $\mathbf{w} = [w_1,\dots,w_k]$ are the weights and $k$ is the number of
1656
+ objective functions.
1657
+
1658
+ Warning:
1659
+ The weighted sums scalarization is often not capable of finding most Pareto optimal
1660
+ solutions when optimized. It is advised to utilize some better scalarization
1661
+ functions.
1662
+
1663
+ Args:
1664
+ problem (Problem): the problem to which the scalarization should be added.
1665
+ symbol (str): the symbol to reference the added scalarization function.
1666
+ weights (dict[str, float]): the weights. For the method to work, the weights
1667
+ should sum to 1. However, this is not a condition that is checked.
1668
+
1669
+ Raises:
1670
+ ScalarizationError: if the weights are missing any of the objective components.
1671
+
1672
+ Returns:
1673
+ tuple[Problem, str]: A tuple containing a copy of the problem with the scalarization function added,
1674
+ and the symbol of the added scalarization function.
1675
+ """
1676
+ # check that the weights have all the objective components
1677
+ if not all(obj.symbol in weights for obj in problem.objectives):
1678
+ msg = f"The given weight vector {weights} does not have a component defined for all the objectives."
1679
+ raise ScalarizationError(msg)
1680
+
1681
+ # Build the sum
1682
+ sum_terms = [f"({weights[obj.symbol]} * {obj.symbol}_min)" for obj in problem.objectives]
1683
+
1684
+ # aggregate the terms
1685
+ sf = " + ".join(sum_terms)
1686
+
1687
+ # Add the function to the problem
1688
+ scalarization_function = ScalarizationFunction(
1689
+ name="Weighted sums scalarization function",
1690
+ symbol=symbol,
1691
+ func=sf,
1692
+ is_linear=problem.is_linear,
1693
+ is_convex=problem.is_convex,
1694
+ is_twice_differentiable=problem.is_twice_differentiable,
1695
+ )
1696
+ return problem.add_scalarization(scalarization_function), symbol
1697
+
1698
+
1699
+ def add_objective_as_scalarization(problem: Problem, symbol: str, objective_symbol: str) -> tuple[Problem, str]:
1700
+ r"""Creates a scalarization where one of the problem's objective functions is optimized.
1701
+
1702
+ The scalarization is defined as follows:
1703
+
1704
+ \begin{equation}
1705
+ \operatorname{min}_{\mathbf{x} \in S} f_t(\mathbf{x}),
1706
+ \end{equation}
1707
+
1708
+ where $f_t(\mathbf{x})$ is the objective function to be minimized.
1709
+
1710
+ Args:
1711
+ problem (Problem): the problem to which the scalarization should be added.
1712
+ symbol (str): the symbol to reference the added scalarization function.
1713
+ objective_symbol (str): the symbol of the objective function to be optimized.
1714
+
1715
+ Raises:
1716
+ ScalarizationError: the given objective_symbol does not exist in the problem.
1717
+
1718
+ Returns:
1719
+ tuple[Problem, str]: A tuple containing a copy of the problem with the scalarization function added,
1720
+ and the symbol of the added scalarization function.
1721
+ """
1722
+ # check that symbol exists
1723
+ if problem.get_objective(objective_symbol, copy=False) is None:
1724
+ msg = f"The given objective symbol {objective_symbol} is not defined in the problem.."
1725
+ raise ScalarizationError(msg)
1726
+
1727
+ sf = ["Multiply", 1, f"{objective_symbol}_min"]
1728
+
1729
+ original_objective = problem.get_objective(objective_symbol, copy=False)
1730
+
1731
+ # Add the function to the problem
1732
+ scalarization_function = ScalarizationFunction(
1733
+ name=f"Objective {objective_symbol}",
1734
+ symbol=symbol,
1735
+ func=sf,
1736
+ is_linear=original_objective.is_linear,
1737
+ is_convex=original_objective.is_convex,
1738
+ is_twice_differentiable=original_objective.is_twice_differentiable,
1739
+ )
1740
+ return problem.add_scalarization(scalarization_function), symbol
1741
+
1742
+
1743
+ def add_epsilon_constraints(
1744
+ problem: Problem, symbol: str, constraint_symbols: dict[str, str], objective_symbol: str, epsilons: dict[str, float]
1745
+ ) -> tuple[Problem, str, list[str]]:
1746
+ r"""Creates expressions for an epsilon constraints scalarization and constraints.
1747
+
1748
+ It is assumed that epsilon have been given in a format where each objective is to be minimized.
1749
+
1750
+ The scalarization is defined as follows:
1751
+
1752
+ \begin{equation}
1753
+ \begin{aligned}
1754
+ & \operatorname{min}_{\mathbf{x} \in S}
1755
+ & & f_t(\mathbf{x}) \\
1756
+ & \text{s.t.}
1757
+ & & f_j(\mathbf{x}) \leq \epsilon_j \text{ for all } j = 1, \ldots ,k, \; j \neq t,
1758
+ \end{aligned}
1759
+ \end{equation}
1760
+
1761
+ where $\epsilon_j$ are the epsilon bounds used in the epsilon constraints $f_j(\mathbf{x}) \leq \epsilon_j$,
1762
+ and $k$ is the number of objective functions.
1763
+
1764
+ Args:
1765
+ problem (Problem): the problem to scalarize.
1766
+ symbol (str): the symbol of the added objective function to be optimized.
1767
+ constraint_symbols (dict[str, str]): a dict with the symbols to be used with the added
1768
+ constraints. The key indicates the name of the objective function the constraint
1769
+ is related to, and the value is the symbol to be used when defining the constraint.
1770
+ objective_symbol (str): the objective used as the objective in the epsilon constraint scalarization.
1771
+ epsilons (dict[str, float]): the epsilon constraint values in a dict
1772
+ with each key being an objective's symbol. The corresponding value
1773
+ is then used as the epsilon value for the respective objective function.
1774
+
1775
+ Raises:
1776
+ ScalarizationError: `objective_symbol` not found in problem definition.
1777
+
1778
+ Returns:
1779
+ tuple[Problem, str, list[str]]: A triple with the first element being a copy of the
1780
+ problem with the added epsilon constraints. The second element is the symbol of
1781
+ the objective to be optimized. The last element is a list with the symbols
1782
+ of the added constraints to the problem.
1783
+ """
1784
+ if objective_symbol not in (correct_symbols := [objective.symbol for objective in problem.objectives]):
1785
+ msg = f"The given objective symbol {objective_symbol} should be one of {correct_symbols}."
1786
+ raise ScalarizationError(msg)
1787
+
1788
+ _problem, _ = add_objective_as_scalarization(problem, symbol, objective_symbol)
1789
+
1790
+ # the epsilons must be given such that each objective function is to be minimized
1791
+ constraints = [
1792
+ Constraint(
1793
+ name=f"Epsilon for {obj.symbol}",
1794
+ symbol=constraint_symbols[obj.symbol],
1795
+ func=["Add", f"{obj.symbol}_min", ["Negate", epsilons[obj.symbol]]],
1796
+ cons_type=ConstraintTypeEnum.LTE,
1797
+ is_linear=obj.is_linear,
1798
+ is_convex=obj.is_convex,
1799
+ is_twice_differentiable=obj.is_twice_differentiable,
1800
+ )
1801
+ for obj in problem.objectives
1802
+ if obj.symbol != objective_symbol
1803
+ ]
1804
+
1805
+ _problem = _problem.add_constraints(constraints)
1806
+
1807
+ return _problem, symbol, [con.symbol for con in constraints]
1808
+
1809
+
1810
+ def create_epsilon_constraints_json(
1811
+ problem: Problem, objective_symbol: str, epsilons: dict[str, float]
1812
+ ) -> tuple[list[str | int | float], list[str]]:
1813
+ """Creates JSON expressions for an epsilon constraints scalarization and constraints.
1814
+
1815
+ It is assumed that epsilon have been given in a format where each objective is to be minimized.
1816
+
1817
+ Warning:
1818
+ To be deprecated.
1819
+
1820
+ Args:
1821
+ problem (Problem): the problem to scalarize.
1822
+ objective_symbol (str): the objective used as the objective in the epsilon constraint scalarization.
1823
+ epsilons (dict[str, float]): the epsilon constraint values in a dict
1824
+ with each key being an objective's symbol.
1825
+
1826
+ Raises:
1827
+ ScalarizationError: `objective_symbol` not found in problem definition.
1828
+
1829
+ Returns:
1830
+ tuple[list, list]: the first element is the expression of the scalarized objective expressed in MathJSON format.
1831
+ The second element is a list of expressions of the constraints expressed in MathJSON format.
1832
+ The constraints are in less than or equal format.
1833
+ """
1834
+ correct_symbols = [objective.symbol for objective in problem.objectives]
1835
+ if objective_symbol not in correct_symbols:
1836
+ msg = f"The given objective symbol {objective_symbol} should be one of {correct_symbols}."
1837
+ raise ScalarizationError(msg)
1838
+ correct_symbols.remove(objective_symbol)
1839
+
1840
+ scalarization_expr = ["Multiply", 1, f"{objective_symbol}_min"]
1841
+
1842
+ # the epsilons must be given such that each objective function is to be minimized
1843
+ constraint_exprs = [["Add", f"{obj}_min", ["Negate", epsilons[obj]]] for obj in correct_symbols]
1844
+
1845
+ return scalarization_expr, constraint_exprs
1846
+
1847
+
1848
+ def __create_HDF(
1849
+ y: str,
1850
+ a: float,
1851
+ r: float,
1852
+ d1: float = 0.9,
1853
+ d2: float = 0.1,
1854
+ ) -> str:
1855
+ r"""Create a Harrington's one-sided desirability function.
1856
+
1857
+ Harrington's desirability function is used to compute the desirability of a
1858
+ given value of an objective function based on its aspiration and reservation levels.
1859
+
1860
+ The desirability function is defined as follows:
1861
+ \begin{equation}
1862
+ D(y) = \exp\left(-\exp\left(-b_0 - b_1 y\right)\right),
1863
+ \end{equation}
1864
+
1865
+ where
1866
+ \begin{align*}
1867
+ b_0 &= -\log(-\log(d_1)) - b_1 a, \\
1868
+ b_1 &= \frac{\log(-\log(d_2)) - \log(-\log(d_1))}{r - a}.
1869
+ \end{align*}
1870
+
1871
+ The desirability function returns a value between 0 and 1, where higher values indicate
1872
+ more desirable outcomes. I took the equations from the following source:
1873
+ Wagner, T., and Trautmann, H. Integration of preference in hypervolume-based
1874
+ multiobjective evolutionary algorithms by means of desirability functions.
1875
+ IEEE Transactions on Evolutionary Computation 14, 5 (2010), 688-701.
1876
+
1877
+ Args:
1878
+ y (str): The objective value to compute the desirability for.
1879
+ a (float): Aspiration level for the objective.
1880
+ r (float): Reservation level for the objective.
1881
+ d1 (float): The desirability for the aspiration level.
1882
+ d2 (float): The desirability for the reservation level.
1883
+
1884
+ Returns:
1885
+ callable (Function): A function that computes the desirability for a given value.
1886
+ """
1887
+ if not (0 < d1 < 1 and 0 < d2 < 1):
1888
+ raise ValueError("Desirability values must be between 0 and 1 (exclusive).")
1889
+ if not (a < r):
1890
+ raise ValueError("a must be less than r.")
1891
+ if not d2 < d1:
1892
+ raise ValueError("d2 must be less than d1. Higher desirability should correspond to lower values of y.")
1893
+ b1: float = -np.log(-np.log(d2)) + np.log(-np.log(d1)) / (r - a)
1894
+ b0: float = -np.log(-np.log(d1)) - b1 * a
1895
+
1896
+ def __HDF(y: float):
1897
+ """Compute the desirability for a given value."""
1898
+ return np.exp(-np.exp(-(b0 + b1 * y)))
1899
+
1900
+ func = f"Exp(-Exp(-({b0} + {b1} * {y})))"
1901
+ return func
1902
+
1903
+
1904
+ def __create_MDF(y: str, a: float, r: float, d1: float = 0.9, d2: float = 0.1) -> str:
1905
+ """Create MaoMao's desirability function.
1906
+
1907
+ Distinctions form MaoMao's original function:
1908
+ - The upper and lower bounds of desirability are fixed to 0 and 1, respectively.
1909
+
1910
+ Args:
1911
+ y (str): The objective value to compute the desirability for.
1912
+ a (float): Aspiration level for the objective.
1913
+ r (float): Reservation level for the objective.
1914
+ d1 (float): The desirability for the aspiration level.
1915
+ d2 (float): The desirability for the reservation level.
1916
+
1917
+ Returns:
1918
+ callable (Function): A function that computes the desirability for a given value.
1919
+ """
1920
+ if not (0 < d1 < 1 and 0 < d2 < 1):
1921
+ raise ValueError("Desirability values must be between 0 and 1 (exclusive).")
1922
+ if not (a < r):
1923
+ raise ValueError("a must be less than r.")
1924
+ if not d2 < d1:
1925
+ raise ValueError("d2 must be less than d1. Higher desirability should correspond to lower values of y.")
1926
+ ea = 1 - d1
1927
+ er = d2
1928
+ m1 = -ea * ea * (a - r) / (d1 - d2)
1929
+ b1 = -a + ea * (a - r) / (d1 - d2)
1930
+ m2 = (d1 - d2) / (a - r)
1931
+ b2 = (d2 * a - d1 * r) / (a - r)
1932
+ m3 = -er * er * (a - r) / (d1 - d2)
1933
+ b3 = -r - er * (a - r) / (d1 - d2)
1934
+
1935
+ def MDF1(y):
1936
+ """Compute the desirability for a given value."""
1937
+ if isinstance(y, np.ndarray):
1938
+ return np.array([MDF1(yi) for yi in y])
1939
+ if y < a:
1940
+ return 1 + m1 / (y + b1)
1941
+ elif a <= y <= r:
1942
+ return m2 * y + b2
1943
+ else:
1944
+ return m3 / (y + b3)
1945
+
1946
+ def MDF(y):
1947
+ """Compute the desirability for a given value."""
1948
+ # Same but without the if statements
1949
+ if isinstance(y, np.ndarray):
1950
+ return np.array([MDF(yi) for yi in y])
1951
+ return (
1952
+ max(a - y, 0) * (1 + m1 / (y + b1)) / (a - y)
1953
+ + max(y - r, 0) * (m3 / (y + b3)) / (y - r)
1954
+ + max(y - a, 0) * max(r - y, 0) * (m2 * y + b2) / ((y - a) * (r - y))
1955
+ )
1956
+
1957
+ func = (
1958
+ f"Max({a} - {y}, 0) * (1 + {m1} / ({y} + {b1})) / ({a} - {y}) + "
1959
+ f"Max({y} - {r}, 0) * ({m3} / ({y} + {b3})) / ({y} - {r}) + "
1960
+ f"Max({y} - {a}, 0) * Max({r} - {y}, 0) * ({m2} * {y} + {b2}) / "
1961
+ f"(({y} - {a}) * ({r} - {y}))"
1962
+ )
1963
+ return func
1964
+
1965
+
1966
+ def add_desirability_funcs(
1967
+ problem: Problem,
1968
+ aspiration_levels: dict[str, float],
1969
+ reservation_levels: dict[str, float],
1970
+ desirability_levels: dict[str, tuple[float, float]] | None = None,
1971
+ desirability_func: Literal["Harrington", "MaoMao"] = "Harrington",
1972
+ ) -> tuple[Problem, list[str]]:
1973
+ """Adds desirability functions to the problem based on the given aspiration and reservation levels.
1974
+
1975
+ Note that the desirability functions are added as scalarization functions to the problem. They are also multiplied
1976
+ by -1 to ensure that "desirability" values can be minimized, as is assumed by the optimizers.
1977
+
1978
+ Args:
1979
+ problem (Problem): The problem to which the desirability functions should be added.
1980
+ aspiration_levels (dict[str, float]): A dictionary with keys corresponding to objective function symbols
1981
+ and values to aspiration levels.
1982
+ reservation_levels (dict[str, float]): A dictionary with keys corresponding to objective function symbols
1983
+ and values to reservation levels.
1984
+ desirability_levels (dict[str, tuple[float, float]] | None, optional): A dictionary with keys corresponding to
1985
+ objective function symbols and values to desirability levels, where each value is a tuple of (d1, d2). If
1986
+ not given, the default values for d1 and d2 are used, which are 0.9 and 0.1 respectively. Defaults to None.
1987
+ desirability_func (str, optional): The type of desirability function to use. Currently, only "Harrington" or
1988
+ "MaoMao" is supported. Defaults to "Harrington".
1989
+
1990
+ Returns:
1991
+ Problem: A copy of the problem with the added desirability functions as scalarization functions.
1992
+ list[str]: A list of symbols of the added desirability functions.
1993
+ """
1994
+ if desirability_func == "Harrington":
1995
+ create_func = __create_HDF
1996
+ elif desirability_func == "MaoMao":
1997
+ create_func = __create_MDF
1998
+ else:
1999
+ raise ScalarizationError(f"Desirability function {desirability_func} is not supported.")
2000
+
2001
+ if desirability_levels is None:
2002
+ desirability_levels = {obj.symbol: (0.9, 0.1) for obj in problem.objectives}
2003
+
2004
+ # check that all objectives have aspiration and reservation levels defined
2005
+ for obj in problem.objectives:
2006
+ if obj.symbol not in aspiration_levels or obj.symbol not in reservation_levels:
2007
+ raise ScalarizationError(
2008
+ f"Objective {obj.symbol} does not have both aspiration and reservation levels defined."
2009
+ )
2010
+ maximize: dict[str, int] = {obj.symbol: -1 if obj.maximize else 1 for obj in problem.objectives}
2011
+ symbols = []
2012
+ problem_: Problem = problem.model_copy(deep=True)
2013
+ for obj in problem.objectives:
2014
+ d1, d2 = desirability_levels[obj.symbol]
2015
+ func = (
2016
+ "- ("
2017
+ + create_func(
2018
+ obj.symbol + "_min",
2019
+ aspiration_levels[obj.symbol] * maximize[obj.symbol],
2020
+ reservation_levels[obj.symbol] * maximize[obj.symbol],
2021
+ d1,
2022
+ d2,
2023
+ )
2024
+ + ")"
2025
+ )
2026
+ symbols.append(f"{obj.symbol}_d")
2027
+ scalarization = ScalarizationFunction(
2028
+ name=f"Desirability function for {obj.symbol}",
2029
+ symbol=f"{obj.symbol}_d",
2030
+ func=func,
2031
+ is_linear=False,
2032
+ is_convex=False,
2033
+ is_twice_differentiable=obj.is_twice_differentiable,
2034
+ )
2035
+ problem_ = problem_.add_scalarization(scalarization)
2036
+
2037
+ return problem_, symbols
2038
+
2039
+
2040
+ def add_iopis_funcs(
2041
+ problem: Problem,
2042
+ reference_point: dict[str, float],
2043
+ ideal: dict[str, float] | None = None,
2044
+ nadir: dict[str, float] | None = None,
2045
+ rho: float = 1e-6,
2046
+ delta: float = 1e-6,
2047
+ ) -> tuple[Problem, list[str]]:
2048
+ symbols = ["iopis_guess", "iopis_stom"]
2049
+ _problem, _ = add_guess_sf_nondiff(
2050
+ problem=problem,
2051
+ symbol=symbols[0],
2052
+ reference_point=reference_point,
2053
+ ideal=ideal,
2054
+ nadir=nadir,
2055
+ rho=rho,
2056
+ )
2057
+
2058
+ _problem, _ = add_stom_sf_nondiff(
2059
+ problem=_problem,
2060
+ symbol=symbols[1],
2061
+ reference_point=reference_point,
2062
+ ideal=ideal,
2063
+ delta=delta,
2064
+ )
2065
+ return _problem, symbols