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