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