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