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
desdeo/tools/scalarization.py
CHANGED
|
@@ -6,6 +6,8 @@ or minimization of the corresponding objective functions may be correctly
|
|
|
6
6
|
accounted for when computing scalarization function values.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
from typing import Literal
|
|
10
|
+
|
|
9
11
|
import numpy as np
|
|
10
12
|
|
|
11
13
|
from desdeo.problem import (
|
|
@@ -17,9 +19,9 @@ from desdeo.problem import (
|
|
|
17
19
|
VariableTypeEnum,
|
|
18
20
|
)
|
|
19
21
|
from desdeo.tools.utils import (
|
|
22
|
+
flip_maximized_objective_values,
|
|
20
23
|
get_corrected_ideal,
|
|
21
24
|
get_corrected_nadir,
|
|
22
|
-
get_corrected_reference_point,
|
|
23
25
|
)
|
|
24
26
|
|
|
25
27
|
|
|
@@ -192,7 +194,7 @@ def add_asf_nondiff( # noqa: PLR0913
|
|
|
192
194
|
# Build the max term
|
|
193
195
|
max_operands = [
|
|
194
196
|
(
|
|
195
|
-
f"({obj.symbol}_min - {reference_point[obj.symbol]}{
|
|
197
|
+
f"({obj.symbol}_min - {reference_point[obj.symbol]}{' * -1' if obj.maximize else ''}) "
|
|
196
198
|
f"/ ({nadir_point[obj.symbol]} - ({ideal_point[obj.symbol]} - {delta}))"
|
|
197
199
|
)
|
|
198
200
|
for obj in problem.objectives
|
|
@@ -208,7 +210,7 @@ def add_asf_nondiff( # noqa: PLR0913
|
|
|
208
210
|
else:
|
|
209
211
|
aug_operands = [
|
|
210
212
|
(
|
|
211
|
-
f"({obj.symbol}_min - {reference_point[obj.symbol]}{
|
|
213
|
+
f"({obj.symbol}_min - {reference_point[obj.symbol]}{' * -1' if obj.maximize else 1}) "
|
|
212
214
|
f"/ ({nadir_point[obj.symbol]} - ({ideal_point[obj.symbol]} - {delta}))"
|
|
213
215
|
)
|
|
214
216
|
for obj in problem.objectives
|
|
@@ -230,231 +232,6 @@ def add_asf_nondiff( # noqa: PLR0913
|
|
|
230
232
|
return problem.add_scalarization(scalarization_function), symbol
|
|
231
233
|
|
|
232
234
|
|
|
233
|
-
def add_group_asf(
|
|
234
|
-
problem: Problem,
|
|
235
|
-
symbol: str,
|
|
236
|
-
reference_points: list[dict[str, float]],
|
|
237
|
-
ideal: dict[str, float] | None = None,
|
|
238
|
-
nadir: dict[str, float] | None = None,
|
|
239
|
-
delta: float = 1e-6,
|
|
240
|
-
rho: float = 1e-6,
|
|
241
|
-
) -> tuple[Problem, str]:
|
|
242
|
-
r"""Add the achievement scalarizing function for multiple decision makers.
|
|
243
|
-
|
|
244
|
-
The scalarization function is defined as follows:
|
|
245
|
-
|
|
246
|
-
\begin{align}
|
|
247
|
-
&\mbox{minimize} &&\max_{i,d} [w_{id}(f_{id}(\mathbf{x})-\overline{z}_{id})] +
|
|
248
|
-
\rho \sum^k_{i=1} \sum^{n_d}_{d=1} w_{id}f_{id}(\mathbf{x}) \\
|
|
249
|
-
&\mbox{subject to} &&\mathbf{x} \in \mathbf{X},
|
|
250
|
-
\end{align}
|
|
251
|
-
|
|
252
|
-
where $w_{id} = \frac{1}{z^{nad}_{id} - z^{uto}_{id}}$.
|
|
253
|
-
|
|
254
|
-
Args:
|
|
255
|
-
problem (Problem): the problem to which the scalarization function should be added.
|
|
256
|
-
symbol (str): the symbol to reference the added scalarization function.
|
|
257
|
-
reference_points (list[dict[str, float]]): a list of reference points as objective dicts.
|
|
258
|
-
ideal (dict[str, float], optional): ideal point values. If not given, attempt will be made
|
|
259
|
-
to calculate ideal point from problem.
|
|
260
|
-
nadir (dict[str, float], optional): nadir point values. If not given, attempt will be made
|
|
261
|
-
to calculate nadir point from problem.
|
|
262
|
-
delta (float, optional): a small scalar used to define the utopian point. Defaults to 1e-6.
|
|
263
|
-
rho (float, optional): the weight factor used in the augmentation term. Defaults to 1e-6.
|
|
264
|
-
|
|
265
|
-
Raises:
|
|
266
|
-
ScalarizationError: there are missing elements in any reference point.
|
|
267
|
-
|
|
268
|
-
Returns:
|
|
269
|
-
tuple[Problem, str]: A tuple containing a copy of the problem with the scalarization function added,
|
|
270
|
-
and the symbol of the added scalarization function.
|
|
271
|
-
"""
|
|
272
|
-
# check reference points
|
|
273
|
-
for reference_point in reference_points:
|
|
274
|
-
if not objective_dict_has_all_symbols(problem, reference_point):
|
|
275
|
-
msg = f"The give reference point {reference_point} is missing a value for one or more objectives."
|
|
276
|
-
raise ScalarizationError(msg)
|
|
277
|
-
|
|
278
|
-
# check if ideal point is specified
|
|
279
|
-
# if not specified, try to calculate corrected ideal point
|
|
280
|
-
if ideal is not None:
|
|
281
|
-
ideal_point = ideal
|
|
282
|
-
elif problem.get_ideal_point() is not None:
|
|
283
|
-
ideal_point = get_corrected_ideal(problem)
|
|
284
|
-
else:
|
|
285
|
-
msg = "Ideal point not defined!"
|
|
286
|
-
raise ScalarizationError(msg)
|
|
287
|
-
|
|
288
|
-
# check if nadir point is specified
|
|
289
|
-
# if not specified, try to calculate corrected nadir point
|
|
290
|
-
if nadir is not None:
|
|
291
|
-
nadir_point = nadir
|
|
292
|
-
elif problem.get_nadir_point() is not None:
|
|
293
|
-
nadir_point = get_corrected_nadir(problem)
|
|
294
|
-
else:
|
|
295
|
-
msg = "Nadir point not defined!"
|
|
296
|
-
raise ScalarizationError(msg)
|
|
297
|
-
|
|
298
|
-
# calculate the weights
|
|
299
|
-
weights = {
|
|
300
|
-
obj.symbol: 1 / (nadir_point[obj.symbol] - (ideal_point[obj.symbol] - delta)) for obj in problem.objectives
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
# form the max and augmentation terms
|
|
304
|
-
max_terms = []
|
|
305
|
-
aug_exprs = []
|
|
306
|
-
for i in range(len(reference_points)):
|
|
307
|
-
corrected_rp = get_corrected_reference_point(problem, reference_points[i])
|
|
308
|
-
for obj in problem.objectives:
|
|
309
|
-
max_terms.append(f"({weights[obj.symbol]}) * ({obj.symbol}_min - {corrected_rp[obj.symbol]})")
|
|
310
|
-
|
|
311
|
-
aug_expr = " + ".join([f"({weights[obj.symbol]} * {obj.symbol}_min)" for obj in problem.objectives])
|
|
312
|
-
aug_exprs.append(aug_expr)
|
|
313
|
-
max_terms = ", ".join(max_terms)
|
|
314
|
-
aug_exprs = " + ".join(aug_exprs)
|
|
315
|
-
|
|
316
|
-
func = f"{Op.MAX}({max_terms}) + {rho} * ({aug_exprs})"
|
|
317
|
-
|
|
318
|
-
scalarization_function = ScalarizationFunction(
|
|
319
|
-
name="Achievement scalarizing function for multiple decision makers",
|
|
320
|
-
symbol=symbol,
|
|
321
|
-
func=func,
|
|
322
|
-
is_convex=problem.is_convex,
|
|
323
|
-
is_linear=problem.is_linear,
|
|
324
|
-
is_twice_differentiable=False,
|
|
325
|
-
)
|
|
326
|
-
return problem.add_scalarization(scalarization_function), symbol
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
def add_group_asf_diff(
|
|
330
|
-
problem: Problem,
|
|
331
|
-
symbol: str,
|
|
332
|
-
reference_points: list[dict[str, float]],
|
|
333
|
-
ideal: dict[str, float] | None = None,
|
|
334
|
-
nadir: dict[str, float] | None = None,
|
|
335
|
-
delta: float = 1e-6,
|
|
336
|
-
rho: float = 1e-6,
|
|
337
|
-
) -> tuple[Problem, str]:
|
|
338
|
-
r"""Add the differentiable variant of the achievement scalarizing function for multiple decision makers.
|
|
339
|
-
|
|
340
|
-
The scalarization function is defined as follows:
|
|
341
|
-
|
|
342
|
-
\begin{align}
|
|
343
|
-
&\mbox{minimize} &&\alpha +
|
|
344
|
-
\rho \sum^k_{i=1} \sum^{n_d}_{d=1} w_{id}f_{id}(\mathbf{x}) \\
|
|
345
|
-
&\mbox{subject to} && w_{id}(f_{id}(\mathbf{x})-\overline{z}_{id}) - \alpha \leq 0,\\
|
|
346
|
-
&&&\mathbf{x} \in \mathbf{X},
|
|
347
|
-
\end{align}
|
|
348
|
-
|
|
349
|
-
where $w_{id} = \frac{1}{z^{nad}_{id} - z^{uto}_{id}}$.
|
|
350
|
-
|
|
351
|
-
Args:
|
|
352
|
-
problem (Problem): the problem to which the scalarization function should be added.
|
|
353
|
-
symbol (str): the symbol to reference the added scalarization function.
|
|
354
|
-
reference_points (list[dict[str, float]]): a list of reference points as objective dicts.
|
|
355
|
-
ideal (dict[str, float], optional): ideal point values. If not given, attempt will be made
|
|
356
|
-
to calculate ideal point from problem.
|
|
357
|
-
nadir (dict[str, float], optional): nadir point values. If not given, attempt will be made
|
|
358
|
-
to calculate nadir point from problem.
|
|
359
|
-
delta (float, optional): a small scalar used to define the utopian point. Defaults to 1e-6.
|
|
360
|
-
rho (float, optional): the weight factor used in the augmentation term. Defaults to 1e-6.
|
|
361
|
-
|
|
362
|
-
Raises:
|
|
363
|
-
ScalarizationError: there are missing elements in any reference point.
|
|
364
|
-
|
|
365
|
-
Returns:
|
|
366
|
-
tuple[Problem, str]: A tuple containing a copy of the problem with the scalarization function added,
|
|
367
|
-
and the symbol of the added scalarization function.
|
|
368
|
-
"""
|
|
369
|
-
# check reference points
|
|
370
|
-
for reference_point in reference_points:
|
|
371
|
-
if not objective_dict_has_all_symbols(problem, reference_point):
|
|
372
|
-
msg = f"The give reference point {reference_point} is missing a value for one or more objectives."
|
|
373
|
-
raise ScalarizationError(msg)
|
|
374
|
-
|
|
375
|
-
# check if ideal point is specified
|
|
376
|
-
# if not specified, try to calculate corrected ideal point
|
|
377
|
-
if ideal is not None:
|
|
378
|
-
ideal_point = ideal
|
|
379
|
-
elif problem.get_ideal_point() is not None:
|
|
380
|
-
ideal_point = get_corrected_ideal(problem)
|
|
381
|
-
else:
|
|
382
|
-
msg = "Ideal point not defined!"
|
|
383
|
-
raise ScalarizationError(msg)
|
|
384
|
-
|
|
385
|
-
# check if nadir point is specified
|
|
386
|
-
# if not specified, try to calculate corrected nadir point
|
|
387
|
-
if nadir is not None:
|
|
388
|
-
nadir_point = nadir
|
|
389
|
-
elif problem.get_nadir_point() is not None:
|
|
390
|
-
nadir_point = get_corrected_nadir(problem)
|
|
391
|
-
else:
|
|
392
|
-
msg = "Nadir point not defined!"
|
|
393
|
-
raise ScalarizationError(msg)
|
|
394
|
-
|
|
395
|
-
# define the auxiliary variable
|
|
396
|
-
alpha = Variable(
|
|
397
|
-
name="alpha",
|
|
398
|
-
symbol="_alpha",
|
|
399
|
-
variable_type=VariableTypeEnum.real,
|
|
400
|
-
lowerbound=-float("Inf"),
|
|
401
|
-
upperbound=float("Inf"),
|
|
402
|
-
initial_value=1.0,
|
|
403
|
-
)
|
|
404
|
-
|
|
405
|
-
# calculate the weights
|
|
406
|
-
weights = {
|
|
407
|
-
obj.symbol: 1 / (nadir_point[obj.symbol] - (ideal_point[obj.symbol] - delta)) for obj in problem.objectives
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
# form the constaint and augmentation expressions
|
|
411
|
-
# constraint expressions are formed into a list of lists
|
|
412
|
-
con_terms = []
|
|
413
|
-
aug_exprs = []
|
|
414
|
-
for i in range(len(reference_points)):
|
|
415
|
-
corrected_rp = get_corrected_reference_point(problem, reference_points[i])
|
|
416
|
-
rp = {}
|
|
417
|
-
for obj in problem.objectives:
|
|
418
|
-
rp[obj.symbol] = f"(({weights[obj.symbol]}) * ({obj.symbol}_min - {corrected_rp[obj.symbol]})) - _alpha"
|
|
419
|
-
con_terms.append(rp)
|
|
420
|
-
aug_expr = " + ".join([f"({weights[obj.symbol]} * {obj.symbol}_min)" for obj in problem.objectives])
|
|
421
|
-
aug_exprs.append(aug_expr)
|
|
422
|
-
aug_exprs = " + ".join(aug_exprs)
|
|
423
|
-
|
|
424
|
-
func = f"_alpha + {rho} * ({aug_exprs})"
|
|
425
|
-
|
|
426
|
-
scalarization_function = ScalarizationFunction(
|
|
427
|
-
name="Differentiable achievement scalarizing function for multiple decision makers",
|
|
428
|
-
symbol=symbol,
|
|
429
|
-
func=func,
|
|
430
|
-
is_convex=problem.is_convex,
|
|
431
|
-
is_linear=problem.is_linear,
|
|
432
|
-
is_twice_differentiable=problem.is_twice_differentiable,
|
|
433
|
-
)
|
|
434
|
-
|
|
435
|
-
constraints = []
|
|
436
|
-
# loop to create a constraint for every objective of every reference point given
|
|
437
|
-
for i in range(len(reference_points)):
|
|
438
|
-
for obj in problem.objectives:
|
|
439
|
-
# since we are subtracting a constant value, the linearity, convexity,
|
|
440
|
-
# and differentiability of the objective function, and hence the
|
|
441
|
-
# constraint, should not change.
|
|
442
|
-
constraints.append(
|
|
443
|
-
Constraint(
|
|
444
|
-
name=f"Constraint for {obj.symbol}",
|
|
445
|
-
symbol=f"{obj.symbol}_con_{i+1}",
|
|
446
|
-
func=con_terms[i][obj.symbol],
|
|
447
|
-
cons_type=ConstraintTypeEnum.LTE,
|
|
448
|
-
is_linear=obj.is_linear,
|
|
449
|
-
is_convex=obj.is_convex,
|
|
450
|
-
is_twice_differentiable=obj.is_twice_differentiable,
|
|
451
|
-
)
|
|
452
|
-
)
|
|
453
|
-
_problem = problem.add_variables([alpha])
|
|
454
|
-
_problem = _problem.add_scalarization(scalarization_function)
|
|
455
|
-
return _problem.add_constraints(constraints), symbol
|
|
456
|
-
|
|
457
|
-
|
|
458
235
|
def add_asf_generic_diff( # noqa: PLR0913
|
|
459
236
|
problem: Problem,
|
|
460
237
|
symbol: str,
|
|
@@ -534,9 +311,9 @@ def add_asf_generic_diff( # noqa: PLR0913
|
|
|
534
311
|
msg = f"The given weight vector {weights_aug} is missing a value for one or more objectives."
|
|
535
312
|
raise ScalarizationError(msg)
|
|
536
313
|
|
|
537
|
-
corrected_rp =
|
|
314
|
+
corrected_rp = flip_maximized_objective_values(problem, reference_point)
|
|
538
315
|
if reference_point_aug is not None:
|
|
539
|
-
corrected_rp_aug =
|
|
316
|
+
corrected_rp_aug = flip_maximized_objective_values(problem, reference_point_aug)
|
|
540
317
|
|
|
541
318
|
# define the auxiliary variable
|
|
542
319
|
alpha = Variable(
|
|
@@ -697,9 +474,9 @@ def add_asf_generic_nondiff( # noqa: PLR0913
|
|
|
697
474
|
raise ScalarizationError(msg)
|
|
698
475
|
|
|
699
476
|
# get the corrected reference point
|
|
700
|
-
corrected_rp =
|
|
477
|
+
corrected_rp = flip_maximized_objective_values(problem, reference_point)
|
|
701
478
|
if reference_point_aug is not None:
|
|
702
|
-
corrected_rp_aug =
|
|
479
|
+
corrected_rp_aug = flip_maximized_objective_values(problem, reference_point_aug)
|
|
703
480
|
|
|
704
481
|
# Build the max term
|
|
705
482
|
max_operands = [
|
|
@@ -1132,7 +909,7 @@ def add_nimbus_sf_nondiff( # noqa: PLR0913
|
|
|
1132
909
|
msg = "Nadir point not defined!"
|
|
1133
910
|
raise ScalarizationError(msg)
|
|
1134
911
|
|
|
1135
|
-
corrected_current_point =
|
|
912
|
+
corrected_current_point = flip_maximized_objective_values(problem, current_objective_vector)
|
|
1136
913
|
|
|
1137
914
|
# max term and constraints
|
|
1138
915
|
max_args = []
|
|
@@ -1199,7 +976,7 @@ def add_nimbus_sf_nondiff( # noqa: PLR0913
|
|
|
1199
976
|
con_expr = f"{_symbol}_min - {-1 * reservation if obj.maximize else reservation}"
|
|
1200
977
|
constraints.append(
|
|
1201
978
|
Constraint(
|
|
1202
|
-
name=f"Worsen until
|
|
979
|
+
name=f"Worsen until constraint for {_symbol}",
|
|
1203
980
|
symbol=f"{_symbol}_gte",
|
|
1204
981
|
func=con_expr,
|
|
1205
982
|
cons_type=ConstraintTypeEnum.LTE,
|
|
@@ -1241,101 +1018,54 @@ def add_nimbus_sf_nondiff( # noqa: PLR0913
|
|
|
1241
1018
|
return _problem.add_constraints(constraints), symbol
|
|
1242
1019
|
|
|
1243
1020
|
|
|
1244
|
-
def
|
|
1021
|
+
def add_stom_sf_diff(
|
|
1245
1022
|
problem: Problem,
|
|
1246
1023
|
symbol: str,
|
|
1247
|
-
|
|
1248
|
-
current_objective_vector: dict[str, float],
|
|
1024
|
+
reference_point: dict[str, float],
|
|
1249
1025
|
ideal: dict[str, float] | None = None,
|
|
1250
|
-
|
|
1251
|
-
delta: float =
|
|
1252
|
-
rho: float = 0.000001,
|
|
1026
|
+
rho: float = 1e-6,
|
|
1027
|
+
delta: float = 1e-6,
|
|
1253
1028
|
) -> tuple[Problem, str]:
|
|
1254
|
-
r"""
|
|
1255
|
-
|
|
1256
|
-
The scalarization function is defined as follows:
|
|
1257
|
-
|
|
1258
|
-
\begin{align}
|
|
1259
|
-
&\mbox{minimize} &&\max_{i\in I^<,j\in I^\leq,d} [w_{id}(f_{id}(\mathbf{x})-z^{ideal}_{id}),
|
|
1260
|
-
w_{jd}(f_{jd}(\mathbf{x})-\hat{z}_{jd})] +
|
|
1261
|
-
\rho \sum^k_{i=1} \sum^{n_d}_{d=1} w_{id}f_{id}(\mathbf{x}) \\
|
|
1262
|
-
&\mbox{subject to} &&\mathbf{x} \in \mathbf{X},
|
|
1263
|
-
\end{align}
|
|
1264
|
-
|
|
1265
|
-
where $w_{id} = \frac{1}{z^{nad}_{id} - z^{uto}_{id}}$, and $w_{jd} = \frac{1}{z^{nad}_{jd} - z^{uto}_{jd}}$.
|
|
1266
|
-
|
|
1267
|
-
The $I$-sets are related to the classifications given to each objective function value
|
|
1268
|
-
in respect to the current objective vector (e.g., by a decision maker). They
|
|
1269
|
-
are as follows:
|
|
1270
|
-
|
|
1271
|
-
- $I^{<}$: values that should improve,
|
|
1272
|
-
- $I^{\leq}$: values that should improve until a given aspiration level $\hat{z}_i$,
|
|
1273
|
-
- $I^{=}$: values that are fine as they are,
|
|
1274
|
-
- $I^{\geq}$: values that can be impaired until some reservation level $\varepsilon_i$, and
|
|
1275
|
-
- $I^{\diamond}$: values that are allowed to change freely (not present explicitly in this scalarization function).
|
|
1029
|
+
r"""Adds the differentiable variant of the STOM scalarizing function.
|
|
1276
1030
|
|
|
1277
|
-
|
|
1278
|
-
|
|
1031
|
+
\begin{align*}
|
|
1032
|
+
\min \quad & \alpha + \rho \sum_{i=1}^k \frac{f_i(\mathbf{x})}{\bar{z}_i - z_i^{\star\star}} \\
|
|
1033
|
+
\text{s.t.} \quad & \frac{f_i(\mathbf{x}) - z_i^{\star\star}}{\bar{z}_i
|
|
1034
|
+
- z_i^{\star\star}} - \alpha \leq 0 \quad & \forall i = 1,\dots,k\\
|
|
1035
|
+
& \mathbf{x} \in S,
|
|
1036
|
+
\end{align*}
|
|
1279
1037
|
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
"f_3": (">=", 22.2),
|
|
1285
|
-
"f_4": ("0", None)
|
|
1286
|
-
}
|
|
1287
|
-
```
|
|
1038
|
+
where $f_i$ are objective functions, $z_i^{\star\star} = z_i^\star - \delta$ is
|
|
1039
|
+
a component of the utopian point, $\bar{z}_i$ is a component of the reference point,
|
|
1040
|
+
$\rho$ and $\delta$ are small scalar values, $S$ is the feasible solution
|
|
1041
|
+
space of the original problem, and $\alpha$ is an auxiliary variable.
|
|
1288
1042
|
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1043
|
+
References:
|
|
1044
|
+
H. Nakayama, Y. Sawaragi, Satisficing trade-off method for
|
|
1045
|
+
multiobjective programming, in: M. Grauer, A.P. Wierzbicki (Eds.),
|
|
1046
|
+
Interactive Decision Analysis, Springer Verlag, Berlin, 1984, pp.
|
|
1047
|
+
113-122.
|
|
1293
1048
|
|
|
1294
1049
|
Args:
|
|
1295
|
-
problem (Problem): the problem
|
|
1296
|
-
symbol (str): the symbol given to the scalarization
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
explanation.
|
|
1301
|
-
current_objective_vector (dict[str, float]): the current objective vector that corresponds to
|
|
1302
|
-
a Pareto optimal solution. The classifications are assumed to been given in respect to
|
|
1303
|
-
this vector.
|
|
1050
|
+
problem (Problem): the problem the scalarization is added to.
|
|
1051
|
+
symbol (str): the symbol given to the added scalarization.
|
|
1052
|
+
reference_point (dict[str, float]): a dict with keys corresponding to objective
|
|
1053
|
+
function symbols and values to reference point components, i.e.,
|
|
1054
|
+
aspiration levels.
|
|
1304
1055
|
ideal (dict[str, float], optional): ideal point values. If not given, attempt will be made
|
|
1305
1056
|
to calculate ideal point from problem.
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
delta (float, optional): a small scalar
|
|
1309
|
-
rho (float, optional): a small scalar used in the augmentation term. Defaults to 0.000001.
|
|
1310
|
-
|
|
1311
|
-
Raises:
|
|
1312
|
-
ScalarizationError: any of the given classifications do not define a classification
|
|
1313
|
-
for all the objective functions or any of the given classifications do not allow at
|
|
1314
|
-
least one objective function value to improve and one to worsen.
|
|
1057
|
+
rho (float, optional): a small scalar value to scale the sum in the objective
|
|
1058
|
+
function of the scalarization. Defaults to 1e-6.
|
|
1059
|
+
delta (float, optional): a small scalar value to define the utopian point. Defaults to 1e-6.
|
|
1315
1060
|
|
|
1316
1061
|
Returns:
|
|
1317
1062
|
tuple[Problem, str]: a tuple with the copy of the problem with the added
|
|
1318
1063
|
scalarization and the symbol of the added scalarization.
|
|
1319
1064
|
"""
|
|
1320
|
-
# check
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
f"The given classifications {classifications} do not define "
|
|
1325
|
-
"a classification for all the objective functions."
|
|
1326
|
-
)
|
|
1327
|
-
raise ScalarizationError(msg)
|
|
1328
|
-
|
|
1329
|
-
# check that at least one objective function is allowed to be improved and one is
|
|
1330
|
-
# allowed to worsen
|
|
1331
|
-
if not any(classifications[obj.symbol][0] in ["<", "<="] for obj in problem.objectives) or not any(
|
|
1332
|
-
classifications[obj.symbol][0] in [">=", "0"] for obj in problem.objectives
|
|
1333
|
-
):
|
|
1334
|
-
msg = (
|
|
1335
|
-
f"The given classifications {classifications} should allow at least one objective function value "
|
|
1336
|
-
"to improve and one to worsen."
|
|
1337
|
-
)
|
|
1338
|
-
raise ScalarizationError(msg)
|
|
1065
|
+
# check reference point
|
|
1066
|
+
if not objective_dict_has_all_symbols(problem, reference_point):
|
|
1067
|
+
msg = f"The give reference point {reference_point} is missing value for one or more objectives."
|
|
1068
|
+
raise ScalarizationError(msg)
|
|
1339
1069
|
|
|
1340
1070
|
# check if ideal point is specified
|
|
1341
1071
|
# if not specified, try to calculate corrected ideal point
|
|
@@ -1347,475 +1077,42 @@ def add_group_nimbus_sf( # noqa: PLR0913
|
|
|
1347
1077
|
msg = "Ideal point not defined!"
|
|
1348
1078
|
raise ScalarizationError(msg)
|
|
1349
1079
|
|
|
1350
|
-
|
|
1351
|
-
# if not specified, try to calculate corrected nadir point
|
|
1352
|
-
if nadir is not None:
|
|
1353
|
-
nadir_point = nadir
|
|
1354
|
-
elif problem.get_nadir_point() is not None:
|
|
1355
|
-
nadir_point = get_corrected_nadir(problem)
|
|
1356
|
-
else:
|
|
1357
|
-
msg = "Nadir point not defined!"
|
|
1358
|
-
raise ScalarizationError(msg)
|
|
1359
|
-
|
|
1360
|
-
corrected_current_point = get_corrected_reference_point(problem, current_objective_vector)
|
|
1361
|
-
|
|
1362
|
-
# calculate the weights
|
|
1363
|
-
weights = {
|
|
1364
|
-
obj.symbol: 1 / (nadir_point[obj.symbol] - (ideal_point[obj.symbol] - delta)) for obj in problem.objectives
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
# max term and constraints
|
|
1368
|
-
max_args = []
|
|
1369
|
-
constraints = []
|
|
1080
|
+
corrected_rp = flip_maximized_objective_values(problem, reference_point)
|
|
1370
1081
|
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
con_expr = f"{_symbol}_min - {corrected_current_point[_symbol]}"
|
|
1381
|
-
constraints.append(
|
|
1382
|
-
Constraint(
|
|
1383
|
-
name=f"improvement constraint for {_symbol}",
|
|
1384
|
-
symbol=f"{_symbol}_{i+1}_lt",
|
|
1385
|
-
func=con_expr,
|
|
1386
|
-
cons_type=ConstraintTypeEnum.LTE,
|
|
1387
|
-
is_linear=problem.is_linear,
|
|
1388
|
-
is_convex=problem.is_convex,
|
|
1389
|
-
is_twice_differentiable=problem.is_twice_differentiable,
|
|
1390
|
-
)
|
|
1391
|
-
)
|
|
1392
|
-
case ("<=", aspiration):
|
|
1393
|
-
# if obj is to be maximized, then the current aspiration value needs to be multiplied by -1
|
|
1394
|
-
max_expr = (
|
|
1395
|
-
f"{weights[_symbol]} * ({_symbol}_min - {aspiration * -1 if obj.maximize else aspiration})"
|
|
1396
|
-
)
|
|
1397
|
-
max_args.append(max_expr)
|
|
1398
|
-
|
|
1399
|
-
con_expr = f"{_symbol}_min - {corrected_current_point[_symbol]}"
|
|
1400
|
-
constraints.append(
|
|
1401
|
-
Constraint(
|
|
1402
|
-
name=f"improvement until constraint for {_symbol}",
|
|
1403
|
-
symbol=f"{_symbol}_{i+1}_lte",
|
|
1404
|
-
func=con_expr,
|
|
1405
|
-
cons_type=ConstraintTypeEnum.LTE,
|
|
1406
|
-
is_linear=problem.is_linear,
|
|
1407
|
-
is_convex=problem.is_convex,
|
|
1408
|
-
is_twice_differentiable=problem.is_twice_differentiable,
|
|
1409
|
-
)
|
|
1410
|
-
)
|
|
1411
|
-
case ("=", _):
|
|
1412
|
-
con_expr = f"{_symbol}_min - {corrected_current_point[_symbol]}"
|
|
1413
|
-
constraints.append(
|
|
1414
|
-
Constraint(
|
|
1415
|
-
name=f"Stay at least as good constraint for {_symbol}",
|
|
1416
|
-
symbol=f"{_symbol}_{i+1}_eq",
|
|
1417
|
-
func=con_expr,
|
|
1418
|
-
cons_type=ConstraintTypeEnum.LTE,
|
|
1419
|
-
is_linear=problem.is_linear,
|
|
1420
|
-
is_convex=problem.is_convex,
|
|
1421
|
-
is_twice_differentiable=problem.is_twice_differentiable,
|
|
1422
|
-
)
|
|
1423
|
-
)
|
|
1424
|
-
case (">=", reservation):
|
|
1425
|
-
# if obj is to be maximized, then the current reservation value needs to be multiplied by -1
|
|
1426
|
-
con_expr = f"{_symbol}_min - {-1 * reservation if obj.maximize else reservation}"
|
|
1427
|
-
constraints.append(
|
|
1428
|
-
Constraint(
|
|
1429
|
-
name=f"Worsen until constraint for {_symbol}",
|
|
1430
|
-
symbol=f"{_symbol}_{i+1}_gte",
|
|
1431
|
-
func=con_expr,
|
|
1432
|
-
cons_type=ConstraintTypeEnum.LTE,
|
|
1433
|
-
is_linear=problem.is_linear,
|
|
1434
|
-
is_convex=problem.is_convex,
|
|
1435
|
-
is_twice_differentiable=problem.is_twice_differentiable,
|
|
1436
|
-
)
|
|
1437
|
-
)
|
|
1438
|
-
case ("0", _):
|
|
1439
|
-
# not relevant for this scalarization
|
|
1440
|
-
pass
|
|
1441
|
-
case (c, _):
|
|
1442
|
-
msg = (
|
|
1443
|
-
f"Warning! The classification {c} was supplied, but it is not supported."
|
|
1444
|
-
"Must be one of ['<', '<=', '0', '=', '>=']"
|
|
1445
|
-
)
|
|
1446
|
-
max_expr = f"Max({','.join(max_args)})"
|
|
1082
|
+
# define the auxiliary variable
|
|
1083
|
+
alpha = Variable(
|
|
1084
|
+
name="alpha",
|
|
1085
|
+
symbol="_alpha",
|
|
1086
|
+
variable_type=VariableTypeEnum.real,
|
|
1087
|
+
lowerbound=-float("Inf"),
|
|
1088
|
+
upperbound=float("Inf"),
|
|
1089
|
+
initial_value=1.0,
|
|
1090
|
+
)
|
|
1447
1091
|
|
|
1448
|
-
#
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1092
|
+
# define the objective function of the scalarization
|
|
1093
|
+
aug_expr = " + ".join(
|
|
1094
|
+
[
|
|
1095
|
+
f"{obj.symbol}_min / ({(reference_point[obj.symbol] - ideal_point[obj.symbol]) + delta})"
|
|
1096
|
+
for obj in problem.objectives
|
|
1097
|
+
]
|
|
1098
|
+
)
|
|
1454
1099
|
|
|
1455
|
-
|
|
1100
|
+
target_expr = f"_alpha + {rho}*" + f"({aug_expr})"
|
|
1456
1101
|
scalarization = ScalarizationFunction(
|
|
1457
|
-
name="
|
|
1102
|
+
name="STOM scalarization objective function",
|
|
1458
1103
|
symbol=symbol,
|
|
1459
|
-
func=
|
|
1104
|
+
func=target_expr,
|
|
1105
|
+
is_twice_differentiable=problem.is_twice_differentiable,
|
|
1460
1106
|
is_linear=problem.is_linear,
|
|
1461
1107
|
is_convex=problem.is_convex,
|
|
1462
|
-
is_twice_differentiable=False,
|
|
1463
1108
|
)
|
|
1464
1109
|
|
|
1465
|
-
|
|
1466
|
-
return _problem.add_constraints(constraints), symbol
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
def add_group_nimbus_sf_diff( # noqa: PLR0913
|
|
1470
|
-
problem: Problem,
|
|
1471
|
-
symbol: str,
|
|
1472
|
-
classifications_list: list[dict[str, tuple[str, float | None]]],
|
|
1473
|
-
current_objective_vector: dict[str, float],
|
|
1474
|
-
ideal: dict[str, float] | None = None,
|
|
1475
|
-
nadir: dict[str, float] | None = None,
|
|
1476
|
-
delta: float = 0.000001,
|
|
1477
|
-
rho: float = 0.000001,
|
|
1478
|
-
) -> tuple[Problem, str]:
|
|
1479
|
-
r"""Implements the differentiable variant of the multiple decision maker of the group NIMBUS scalarization function.
|
|
1480
|
-
|
|
1481
|
-
The scalarization function is defined as follows:
|
|
1482
|
-
|
|
1483
|
-
\begin{align}
|
|
1484
|
-
\mbox{minimize} \quad
|
|
1485
|
-
&\alpha +
|
|
1486
|
-
\rho \sum^k_{i=1} \sum^{n_d}_{d=1} w_{id}f_{id}(\mathbf{x})\\
|
|
1487
|
-
\mbox{subject to} \quad & w_{id}(f_{id}(\mathbf{x})-z^{ideal}_{id}) - \alpha \leq 0 \quad & \forall i \in I^<,\\
|
|
1488
|
-
& w_{jd}(f_{jd}(\mathbf{x})-\hat{z}_{jd}) - \alpha \leq 0 \quad & \forall j \in I^\leq ,\\
|
|
1489
|
-
& f_i(\mathbf{x}) - f_i(\mathbf{x_c}) \leq 0 \quad & \forall i \in I^< \cup I^\leq \cup I^= ,\\
|
|
1490
|
-
& f_i(\mathbf{x}) - \epsilon_i \leq 0 \quad & \forall i \in I^\geq ,\\
|
|
1491
|
-
& \mathbf{x} \in \mathbf{X},
|
|
1492
|
-
\end{align}
|
|
1493
|
-
|
|
1494
|
-
where $w_{id} = \frac{1}{z^{nad}_{id} - z^{uto}_{id}}$, and $w_{jd} = \frac{1}{z^{nad}_{jd} - z^{uto}_{jd}}$.
|
|
1495
|
-
|
|
1496
|
-
The $I$-sets are related to the classifications given to each objective function value
|
|
1497
|
-
in respect to the current objective vector (e.g., by a decision maker). They
|
|
1498
|
-
are as follows:
|
|
1499
|
-
|
|
1500
|
-
- $I^{<}$: values that should improve,
|
|
1501
|
-
- $I^{\leq}$: values that should improve until a given aspiration level $\hat{z}_i$,
|
|
1502
|
-
- $I^{=}$: values that are fine as they are,
|
|
1503
|
-
- $I^{\geq}$: values that can be impaired until some reservation level $\varepsilon_i$, and
|
|
1504
|
-
- $I^{\diamond}$: values that are allowed to change freely (not present explicitly in this scalarization function).
|
|
1505
|
-
|
|
1506
|
-
The aspiration levels and the reservation levels are supplied for each classification, when relevant, in
|
|
1507
|
-
the argument `classifications` as follows:
|
|
1508
|
-
|
|
1509
|
-
```python
|
|
1510
|
-
classifications = {
|
|
1511
|
-
"f_1": ("<", None),
|
|
1512
|
-
"f_2": ("<=", 42.1),
|
|
1513
|
-
"f_3": (">=", 22.2),
|
|
1514
|
-
"f_4": ("0", None)
|
|
1515
|
-
}
|
|
1516
|
-
```
|
|
1517
|
-
|
|
1518
|
-
Here, we have assumed four objective functions. The key of the dict is a function's symbol, and the tuple
|
|
1519
|
-
consists of a pair where the left element is the classification (self explanatory, '0' is for objective values
|
|
1520
|
-
that may change freely), the right element is either `None` or an aspiration or a reservation level
|
|
1521
|
-
depending on the classification.
|
|
1522
|
-
|
|
1523
|
-
Args:
|
|
1524
|
-
problem (Problem): the problem to be scalarized.
|
|
1525
|
-
symbol (str): the symbol given to the scalarization function, i.e., target of the optimization.
|
|
1526
|
-
classifications_list (list[dict[str, tuple[str, float | None]]]): a list of dicts, where the key is a symbol
|
|
1527
|
-
of an objective function, and the value is a tuple with a classification and an aspiration
|
|
1528
|
-
or a reservation level, or `None`, depending on the classification. See above for an
|
|
1529
|
-
explanation.
|
|
1530
|
-
current_objective_vector (dict[str, float]): the current objective vector that corresponds to
|
|
1531
|
-
a Pareto optimal solution. The classifications are assumed to been given in respect to
|
|
1532
|
-
this vector.
|
|
1533
|
-
ideal (dict[str, float], optional): ideal point values. If not given, attempt will be made
|
|
1534
|
-
to calculate ideal point from problem.
|
|
1535
|
-
nadir (dict[str, float], optional): nadir point values. If not given, attempt will be made
|
|
1536
|
-
to calculate nadir point from problem.
|
|
1537
|
-
delta (float, optional): a small scalar used to define the utopian point. Defaults to 0.000001.
|
|
1538
|
-
rho (float, optional): a small scalar used in the augmentation term. Defaults to 0.000001.
|
|
1539
|
-
|
|
1540
|
-
Raises:
|
|
1541
|
-
ScalarizationError: any of the given classifications do not define a classification
|
|
1542
|
-
for all the objective functions or any of the given classifications do not allow at
|
|
1543
|
-
least one objective function value to improve and one to worsen.
|
|
1544
|
-
|
|
1545
|
-
Returns:
|
|
1546
|
-
tuple[Problem, str]: a tuple with the copy of the problem with the added
|
|
1547
|
-
scalarization and the symbol of the added scalarization.
|
|
1548
|
-
"""
|
|
1549
|
-
# check that classifications have been provided for all objective functions
|
|
1550
|
-
for classifications in classifications_list:
|
|
1551
|
-
if not objective_dict_has_all_symbols(problem, classifications):
|
|
1552
|
-
msg = (
|
|
1553
|
-
f"The given classifications {classifications} do not define "
|
|
1554
|
-
"a classification for all the objective functions."
|
|
1555
|
-
)
|
|
1556
|
-
raise ScalarizationError(msg)
|
|
1557
|
-
|
|
1558
|
-
# check that at least one objective function is allowed to be improved and one is
|
|
1559
|
-
# allowed to worsen
|
|
1560
|
-
if not any(classifications[obj.symbol][0] in ["<", "<="] for obj in problem.objectives) or not any(
|
|
1561
|
-
classifications[obj.symbol][0] in [">=", "0"] for obj in problem.objectives
|
|
1562
|
-
):
|
|
1563
|
-
msg = (
|
|
1564
|
-
f"The given classifications {classifications} should allow at least one objective function value "
|
|
1565
|
-
"to improve and one to worsen."
|
|
1566
|
-
)
|
|
1567
|
-
raise ScalarizationError(msg)
|
|
1568
|
-
|
|
1569
|
-
# check if ideal point is specified
|
|
1570
|
-
# if not specified, try to calculate corrected ideal point
|
|
1571
|
-
if ideal is not None:
|
|
1572
|
-
ideal_point = ideal
|
|
1573
|
-
elif problem.get_ideal_point() is not None:
|
|
1574
|
-
ideal_point = get_corrected_ideal(problem)
|
|
1575
|
-
else:
|
|
1576
|
-
msg = "Ideal point not defined!"
|
|
1577
|
-
raise ScalarizationError(msg)
|
|
1578
|
-
|
|
1579
|
-
# check if nadir point is specified
|
|
1580
|
-
# if not specified, try to calculate corrected nadir point
|
|
1581
|
-
if nadir is not None:
|
|
1582
|
-
nadir_point = nadir
|
|
1583
|
-
elif problem.get_nadir_point() is not None:
|
|
1584
|
-
nadir_point = get_corrected_nadir(problem)
|
|
1585
|
-
else:
|
|
1586
|
-
msg = "Nadir point not defined!"
|
|
1587
|
-
raise ScalarizationError(msg)
|
|
1588
|
-
|
|
1589
|
-
corrected_current_point = get_corrected_reference_point(problem, current_objective_vector)
|
|
1590
|
-
|
|
1591
|
-
# define the auxiliary variable
|
|
1592
|
-
alpha = Variable(
|
|
1593
|
-
name="alpha",
|
|
1594
|
-
symbol="_alpha",
|
|
1595
|
-
variable_type=VariableTypeEnum.real,
|
|
1596
|
-
lowerbound=-float("Inf"),
|
|
1597
|
-
upperbound=float("Inf"),
|
|
1598
|
-
initial_value=1.0,
|
|
1599
|
-
)
|
|
1600
|
-
|
|
1601
|
-
# calculate the weights
|
|
1602
|
-
weights = {
|
|
1603
|
-
obj.symbol: 1 / (nadir_point[obj.symbol] - (ideal_point[obj.symbol] - delta)) for obj in problem.objectives
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
constraints = []
|
|
1607
|
-
|
|
1608
|
-
for i in range(len(classifications_list)):
|
|
1609
|
-
classifications = classifications_list[i]
|
|
1610
|
-
for obj in problem.objectives:
|
|
1611
|
-
_symbol = obj.symbol
|
|
1612
|
-
match classifications[_symbol]:
|
|
1613
|
-
case ("<", _):
|
|
1614
|
-
max_expr = f"{weights[_symbol]} * ({_symbol}_min - {ideal_point[_symbol]}) - _alpha"
|
|
1615
|
-
constraints.append(
|
|
1616
|
-
Constraint(
|
|
1617
|
-
name=f"Max term linearization for {_symbol}",
|
|
1618
|
-
symbol=f"max_con_{_symbol}_{i+1}",
|
|
1619
|
-
func=max_expr,
|
|
1620
|
-
cons_type=ConstraintTypeEnum.LTE,
|
|
1621
|
-
is_linear=problem.is_linear,
|
|
1622
|
-
is_convex=problem.is_convex,
|
|
1623
|
-
is_twice_differentiable=problem.is_twice_differentiable,
|
|
1624
|
-
)
|
|
1625
|
-
)
|
|
1626
|
-
con_expr = f"{_symbol}_min - {corrected_current_point[_symbol]}"
|
|
1627
|
-
constraints.append(
|
|
1628
|
-
Constraint(
|
|
1629
|
-
name=f"improvement constraint for {_symbol}",
|
|
1630
|
-
symbol=f"{_symbol}_{i+1}_lt",
|
|
1631
|
-
func=con_expr,
|
|
1632
|
-
cons_type=ConstraintTypeEnum.LTE,
|
|
1633
|
-
is_linear=problem.is_linear,
|
|
1634
|
-
is_convex=problem.is_convex,
|
|
1635
|
-
is_twice_differentiable=problem.is_twice_differentiable,
|
|
1636
|
-
)
|
|
1637
|
-
)
|
|
1638
|
-
case ("<=", aspiration):
|
|
1639
|
-
# if obj is to be maximized, then the current aspiration value needs to be multiplied by -1
|
|
1640
|
-
max_expr = (
|
|
1641
|
-
f"{weights[_symbol]} * ({_symbol}_min - {aspiration * -1 if obj.maximize else aspiration}) "
|
|
1642
|
-
"- _alpha"
|
|
1643
|
-
)
|
|
1644
|
-
constraints.append(
|
|
1645
|
-
Constraint(
|
|
1646
|
-
name=f"Max term linearization for {_symbol}",
|
|
1647
|
-
symbol=f"max_con_{_symbol}_{i+1}",
|
|
1648
|
-
func=max_expr,
|
|
1649
|
-
cons_type=ConstraintTypeEnum.LTE,
|
|
1650
|
-
is_linear=problem.is_linear,
|
|
1651
|
-
is_convex=problem.is_convex,
|
|
1652
|
-
is_twice_differentiable=problem.is_twice_differentiable,
|
|
1653
|
-
)
|
|
1654
|
-
)
|
|
1655
|
-
con_expr = f"{_symbol}_min - {corrected_current_point[_symbol]}"
|
|
1656
|
-
constraints.append(
|
|
1657
|
-
Constraint(
|
|
1658
|
-
name=f"improvement until constraint for {_symbol}",
|
|
1659
|
-
symbol=f"{_symbol}_{i+1}_lte",
|
|
1660
|
-
func=con_expr,
|
|
1661
|
-
cons_type=ConstraintTypeEnum.LTE,
|
|
1662
|
-
is_linear=problem.is_linear,
|
|
1663
|
-
is_convex=problem.is_convex,
|
|
1664
|
-
is_twice_differentiable=problem.is_twice_differentiable,
|
|
1665
|
-
)
|
|
1666
|
-
)
|
|
1667
|
-
case ("=", _):
|
|
1668
|
-
con_expr = f"{_symbol}_min - {corrected_current_point[_symbol]}"
|
|
1669
|
-
constraints.append(
|
|
1670
|
-
Constraint(
|
|
1671
|
-
name=f"Stay at least as good constraint for {_symbol}",
|
|
1672
|
-
symbol=f"{_symbol}_{i+1}_eq",
|
|
1673
|
-
func=con_expr,
|
|
1674
|
-
cons_type=ConstraintTypeEnum.LTE,
|
|
1675
|
-
is_linear=problem.is_linear,
|
|
1676
|
-
is_convex=problem.is_convex,
|
|
1677
|
-
is_twice_differentiable=problem.is_twice_differentiable,
|
|
1678
|
-
)
|
|
1679
|
-
)
|
|
1680
|
-
case (">=", reservation):
|
|
1681
|
-
# if obj is to be maximized, then the current reservation value needs to be multiplied by -1
|
|
1682
|
-
con_expr = f"{_symbol}_min - {-1 * reservation if obj.maximize else reservation}"
|
|
1683
|
-
constraints.append(
|
|
1684
|
-
Constraint(
|
|
1685
|
-
name=f"Worsen until constraint for {_symbol}",
|
|
1686
|
-
symbol=f"{_symbol}_{i+1}_gte",
|
|
1687
|
-
func=con_expr,
|
|
1688
|
-
cons_type=ConstraintTypeEnum.LTE,
|
|
1689
|
-
is_linear=problem.is_linear,
|
|
1690
|
-
is_convex=problem.is_convex,
|
|
1691
|
-
is_twice_differentiable=problem.is_twice_differentiable,
|
|
1692
|
-
)
|
|
1693
|
-
)
|
|
1694
|
-
case ("0", _):
|
|
1695
|
-
# not relevant for this scalarization
|
|
1696
|
-
pass
|
|
1697
|
-
case (c, _):
|
|
1698
|
-
msg = (
|
|
1699
|
-
f"Warning! The classification {c} was supplied, but it is not supported."
|
|
1700
|
-
"Must be one of ['<', '<=', '0', '=', '>=']"
|
|
1701
|
-
)
|
|
1702
|
-
|
|
1703
|
-
# form the augmentation term
|
|
1704
|
-
aug_exprs = []
|
|
1705
|
-
for _ in range(len(classifications_list)):
|
|
1706
|
-
aug_expr = " + ".join([f"({weights[obj.symbol]} * {obj.symbol}_min)" for obj in problem.objectives])
|
|
1707
|
-
aug_exprs.append(aug_expr)
|
|
1708
|
-
aug_exprs = " + ".join(aug_exprs)
|
|
1709
|
-
|
|
1710
|
-
func = f"_alpha + {rho} * ({aug_exprs})"
|
|
1711
|
-
scalarization_function = ScalarizationFunction(
|
|
1712
|
-
name="Differentiable NIMBUS scalarization objective function for multiple decision makers",
|
|
1713
|
-
symbol=symbol,
|
|
1714
|
-
func=func,
|
|
1715
|
-
is_linear=problem.is_linear,
|
|
1716
|
-
is_convex=problem.is_convex,
|
|
1717
|
-
is_twice_differentiable=problem.is_twice_differentiable,
|
|
1718
|
-
)
|
|
1719
|
-
_problem = problem.add_variables([alpha])
|
|
1720
|
-
_problem = _problem.add_scalarization(scalarization_function)
|
|
1721
|
-
return _problem.add_constraints(constraints), symbol
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
def add_stom_sf_diff(
|
|
1725
|
-
problem: Problem,
|
|
1726
|
-
symbol: str,
|
|
1727
|
-
reference_point: dict[str, float],
|
|
1728
|
-
ideal: dict[str, float] | None = None,
|
|
1729
|
-
rho: float = 1e-6,
|
|
1730
|
-
delta: float = 1e-6,
|
|
1731
|
-
) -> tuple[Problem, str]:
|
|
1732
|
-
r"""Adds the differentiable variant of the STOM scalarizing function.
|
|
1733
|
-
|
|
1734
|
-
\begin{align*}
|
|
1735
|
-
\min \quad & \alpha + \rho \sum_{i=1}^k \frac{f_i(\mathbf{x})}{\bar{z}_i - z_i^{\star\star}} \\
|
|
1736
|
-
\text{s.t.} \quad & \frac{f_i(\mathbf{x}) - z_i^{\star\star}}{\bar{z}_i
|
|
1737
|
-
- z_i^{\star\star}} - \alpha \leq 0 \quad & \forall i = 1,\dots,k\\
|
|
1738
|
-
& \mathbf{x} \in S,
|
|
1739
|
-
\end{align*}
|
|
1740
|
-
|
|
1741
|
-
where $f_i$ are objective functions, $z_i^{\star\star} = z_i^\star - \delta$ is
|
|
1742
|
-
a component of the utopian point, $\bar{z}_i$ is a component of the reference point,
|
|
1743
|
-
$\rho$ and $\delta$ are small scalar values, $S$ is the feasible solution
|
|
1744
|
-
space of the original problem, and $\alpha$ is an auxiliary variable.
|
|
1745
|
-
|
|
1746
|
-
References:
|
|
1747
|
-
H. Nakayama, Y. Sawaragi, Satisficing trade-off method for
|
|
1748
|
-
multiobjective programming, in: M. Grauer, A.P. Wierzbicki (Eds.),
|
|
1749
|
-
Interactive Decision Analysis, Springer Verlag, Berlin, 1984, pp.
|
|
1750
|
-
113-122.
|
|
1751
|
-
|
|
1752
|
-
Args:
|
|
1753
|
-
problem (Problem): the problem the scalarization is added to.
|
|
1754
|
-
symbol (str): the symbol given to the added scalarization.
|
|
1755
|
-
reference_point (dict[str, float]): a dict with keys corresponding to objective
|
|
1756
|
-
function symbols and values to reference point components, i.e.,
|
|
1757
|
-
aspiration levels.
|
|
1758
|
-
ideal (dict[str, float], optional): ideal point values. If not given, attempt will be made
|
|
1759
|
-
to calculate ideal point from problem.
|
|
1760
|
-
rho (float, optional): a small scalar value to scale the sum in the objective
|
|
1761
|
-
function of the scalarization. Defaults to 1e-6.
|
|
1762
|
-
delta (float, optional): a small scalar value to define the utopian point. Defaults to 1e-6.
|
|
1763
|
-
|
|
1764
|
-
Returns:
|
|
1765
|
-
tuple[Problem, str]: a tuple with the copy of the problem with the added
|
|
1766
|
-
scalarization and the symbol of the added scalarization.
|
|
1767
|
-
"""
|
|
1768
|
-
# check reference point
|
|
1769
|
-
if not objective_dict_has_all_symbols(problem, reference_point):
|
|
1770
|
-
msg = f"The give reference point {reference_point} is missing value for one or more objectives."
|
|
1771
|
-
raise ScalarizationError(msg)
|
|
1772
|
-
|
|
1773
|
-
# check if ideal point is specified
|
|
1774
|
-
# if not specified, try to calculate corrected ideal point
|
|
1775
|
-
if ideal is not None:
|
|
1776
|
-
ideal_point = ideal
|
|
1777
|
-
elif problem.get_ideal_point() is not None:
|
|
1778
|
-
ideal_point = get_corrected_ideal(problem)
|
|
1779
|
-
else:
|
|
1780
|
-
msg = "Ideal point not defined!"
|
|
1781
|
-
raise ScalarizationError(msg)
|
|
1782
|
-
|
|
1783
|
-
corrected_rp = get_corrected_reference_point(problem, reference_point)
|
|
1784
|
-
|
|
1785
|
-
# define the auxiliary variable
|
|
1786
|
-
alpha = Variable(
|
|
1787
|
-
name="alpha",
|
|
1788
|
-
symbol="_alpha",
|
|
1789
|
-
variable_type=VariableTypeEnum.real,
|
|
1790
|
-
lowerbound=-float("Inf"),
|
|
1791
|
-
upperbound=float("Inf"),
|
|
1792
|
-
initial_value=1.0,
|
|
1793
|
-
)
|
|
1794
|
-
|
|
1795
|
-
# define the objective function of the scalarization
|
|
1796
|
-
aug_expr = " + ".join(
|
|
1797
|
-
[
|
|
1798
|
-
f"{obj.symbol}_min / ({reference_point[obj.symbol]} - {ideal_point[obj.symbol] - delta})"
|
|
1799
|
-
for obj in problem.objectives
|
|
1800
|
-
]
|
|
1801
|
-
)
|
|
1802
|
-
|
|
1803
|
-
target_expr = f"_alpha + {rho}*" + f"({aug_expr})"
|
|
1804
|
-
scalarization = ScalarizationFunction(
|
|
1805
|
-
name="STOM scalarization objective function",
|
|
1806
|
-
symbol=symbol,
|
|
1807
|
-
func=target_expr,
|
|
1808
|
-
is_twice_differentiable=problem.is_twice_differentiable,
|
|
1809
|
-
is_linear=problem.is_linear,
|
|
1810
|
-
is_convex=problem.is_convex,
|
|
1811
|
-
)
|
|
1812
|
-
|
|
1813
|
-
constraints = []
|
|
1110
|
+
constraints = []
|
|
1814
1111
|
|
|
1815
1112
|
for obj in problem.objectives:
|
|
1816
1113
|
expr = (
|
|
1817
1114
|
f"({obj.symbol}_min - {ideal_point[obj.symbol] - delta}) / "
|
|
1818
|
-
f"({corrected_rp[obj.symbol] -
|
|
1115
|
+
f"({(corrected_rp[obj.symbol] - ideal_point[obj.symbol]) + delta}) - _alpha"
|
|
1819
1116
|
)
|
|
1820
1117
|
constraints.append(
|
|
1821
1118
|
Constraint(
|
|
@@ -1857,192 +1154,16 @@ def add_stom_sf_nondiff(
|
|
|
1857
1154
|
$\rho$ and $\delta$ are small scalar values, and $S$ is the feasible solution
|
|
1858
1155
|
space of the original problem.
|
|
1859
1156
|
|
|
1860
|
-
References:
|
|
1861
|
-
H. Nakayama, Y. Sawaragi, Satisficing trade-off method for
|
|
1862
|
-
multiobjective programming, in: M. Grauer, A.P. Wierzbicki (Eds.),
|
|
1863
|
-
Interactive Decision Analysis, Springer Verlag, Berlin, 1984, pp.
|
|
1864
|
-
113-122.
|
|
1865
|
-
|
|
1866
|
-
Args:
|
|
1867
|
-
problem (Problem): the problem the scalarization is added to.
|
|
1868
|
-
symbol (str): the symbol given to the added scalarization.
|
|
1869
|
-
reference_point (dict[str, float]): a dict with keys corresponding to objective
|
|
1870
|
-
function symbols and values to reference point components, i.e.,
|
|
1871
|
-
aspiration levels.
|
|
1872
|
-
ideal (dict[str, float], optional): ideal point values. If not given, attempt will be made
|
|
1873
|
-
to calculate ideal point from problem.
|
|
1874
|
-
rho (float, optional): a small scalar value to scale the sum in the objective
|
|
1875
|
-
function of the scalarization. Defaults to 1e-6.
|
|
1876
|
-
delta (float, optional): a small scalar value to define the utopian point. Defaults to 1e-6.
|
|
1877
|
-
|
|
1878
|
-
Returns:
|
|
1879
|
-
tuple[Problem, str]: a tuple with the copy of the problem with the added
|
|
1880
|
-
scalarization and the symbol of the added scalarization.
|
|
1881
|
-
"""
|
|
1882
|
-
# check reference point
|
|
1883
|
-
if not objective_dict_has_all_symbols(problem, reference_point):
|
|
1884
|
-
msg = f"The give reference point {reference_point} is missing value for one or more objectives."
|
|
1885
|
-
raise ScalarizationError(msg)
|
|
1886
|
-
|
|
1887
|
-
# check if ideal point is specified
|
|
1888
|
-
# if not specified, try to calculate corrected ideal point
|
|
1889
|
-
if ideal is not None:
|
|
1890
|
-
ideal_point = ideal
|
|
1891
|
-
elif problem.get_ideal_point() is not None:
|
|
1892
|
-
ideal_point = get_corrected_ideal(problem)
|
|
1893
|
-
else:
|
|
1894
|
-
msg = "Ideal point not defined!"
|
|
1895
|
-
raise ScalarizationError(msg)
|
|
1896
|
-
|
|
1897
|
-
corrected_rp = get_corrected_reference_point(problem, reference_point)
|
|
1898
|
-
|
|
1899
|
-
# define the objective function of the scalarization
|
|
1900
|
-
max_expr = ", ".join(
|
|
1901
|
-
[
|
|
1902
|
-
(
|
|
1903
|
-
f"({obj.symbol}_min - {ideal_point[obj.symbol] - delta}) / "
|
|
1904
|
-
f"({corrected_rp[obj.symbol]} - {ideal_point[obj.symbol] - delta})"
|
|
1905
|
-
)
|
|
1906
|
-
for obj in problem.objectives
|
|
1907
|
-
]
|
|
1908
|
-
)
|
|
1909
|
-
aug_expr = " + ".join(
|
|
1910
|
-
[
|
|
1911
|
-
f"{obj.symbol}_min / ({reference_point[obj.symbol]} - {ideal_point[obj.symbol] - delta})"
|
|
1912
|
-
for obj in problem.objectives
|
|
1913
|
-
]
|
|
1914
|
-
)
|
|
1915
|
-
|
|
1916
|
-
target_expr = f"{Op.MAX}({max_expr}) + {rho}*" + f"({aug_expr})"
|
|
1917
|
-
scalarization = ScalarizationFunction(
|
|
1918
|
-
name="STOM scalarization objective function",
|
|
1919
|
-
symbol=symbol,
|
|
1920
|
-
func=target_expr,
|
|
1921
|
-
is_linear=False,
|
|
1922
|
-
is_convex=False,
|
|
1923
|
-
is_twice_differentiable=False,
|
|
1924
|
-
)
|
|
1925
|
-
|
|
1926
|
-
return problem.add_scalarization(scalarization), symbol
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
def add_group_stom_sf(
|
|
1930
|
-
problem: Problem,
|
|
1931
|
-
symbol: str,
|
|
1932
|
-
reference_points: list[dict[str, float]],
|
|
1933
|
-
ideal: dict[str, float] | None = None,
|
|
1934
|
-
rho: float = 1e-6,
|
|
1935
|
-
delta: float = 1e-6,
|
|
1936
|
-
) -> tuple[Problem, str]:
|
|
1937
|
-
r"""Adds the multiple decision maker variant of the STOM scalarizing function.
|
|
1938
|
-
|
|
1939
|
-
The scalarization function is defined as follows:
|
|
1940
|
-
|
|
1941
|
-
\begin{align}
|
|
1942
|
-
&\mbox{minimize} &&\max_{i,d} [w_{id}(f_{id}(\mathbf{x})-z^{uto}_{id})] +
|
|
1943
|
-
\rho \sum^k_{i=1} \sum^{n_d}_{d=1} w_{id}f_{id}(\mathbf{x}) \\
|
|
1944
|
-
&\mbox{subject to} &&\mathbf{x} \in \mathbf{X},
|
|
1945
|
-
\end{align}
|
|
1946
|
-
|
|
1947
|
-
where $w_{id} = \frac{1}{\overline{z}_{id} - z^{uto}_{id}}$.
|
|
1948
|
-
|
|
1949
|
-
Args:
|
|
1950
|
-
problem (Problem): the problem the scalarization is added to.
|
|
1951
|
-
symbol (str): the symbol given to the added scalarization.
|
|
1952
|
-
reference_points (list[dict[str, float]]): a list of dicts with keys corresponding to objective
|
|
1953
|
-
function symbols and values to reference point components, i.e.,
|
|
1954
|
-
aspiration levels.
|
|
1955
|
-
ideal (dict[str, float], optional): ideal point values. If not given, attempt will be made
|
|
1956
|
-
to calculate ideal point from problem.
|
|
1957
|
-
rho (float, optional): a small scalar value to scale the sum in the objective
|
|
1958
|
-
function of the scalarization. Defaults to 1e-6.
|
|
1959
|
-
delta (float, optional): a small scalar value to define the utopian point. Defaults to 1e-6.
|
|
1960
|
-
|
|
1961
|
-
Raises:
|
|
1962
|
-
ScalarizationError: there are missing elements in any reference point.
|
|
1963
|
-
|
|
1964
|
-
Returns:
|
|
1965
|
-
tuple[Problem, str]: a tuple with the copy of the problem with the added
|
|
1966
|
-
scalarization and the symbol of the added scalarization.
|
|
1967
|
-
"""
|
|
1968
|
-
# check reference points
|
|
1969
|
-
for reference_point in reference_points:
|
|
1970
|
-
if not objective_dict_has_all_symbols(problem, reference_point):
|
|
1971
|
-
msg = f"The give reference point {reference_point} is missing value for one or more objectives."
|
|
1972
|
-
raise ScalarizationError(msg)
|
|
1973
|
-
|
|
1974
|
-
# check if ideal point is specified
|
|
1975
|
-
# if not specified, try to calculate corrected ideal point
|
|
1976
|
-
if ideal is not None:
|
|
1977
|
-
ideal_point = ideal
|
|
1978
|
-
elif problem.get_ideal_point() is not None:
|
|
1979
|
-
ideal_point = get_corrected_ideal(problem)
|
|
1980
|
-
else:
|
|
1981
|
-
msg = "Ideal point not defined!"
|
|
1982
|
-
raise ScalarizationError(msg)
|
|
1983
|
-
|
|
1984
|
-
# calculate the weights
|
|
1985
|
-
weights = []
|
|
1986
|
-
for reference_point in reference_points:
|
|
1987
|
-
corrected_rp = get_corrected_reference_point(problem, reference_point)
|
|
1988
|
-
weights.append(
|
|
1989
|
-
{
|
|
1990
|
-
obj.symbol: 1 / (corrected_rp[obj.symbol] - (ideal_point[obj.symbol] - delta))
|
|
1991
|
-
for obj in problem.objectives
|
|
1992
|
-
}
|
|
1993
|
-
)
|
|
1994
|
-
|
|
1995
|
-
# form the max term
|
|
1996
|
-
max_terms = []
|
|
1997
|
-
for i in range(len(reference_points)):
|
|
1998
|
-
for obj in problem.objectives:
|
|
1999
|
-
max_terms.append(f"{weights[i][obj.symbol]} * ({obj.symbol}_min - {ideal_point[obj.symbol] - delta})")
|
|
2000
|
-
max_terms = ", ".join(max_terms)
|
|
2001
|
-
|
|
2002
|
-
# form the augmentation term
|
|
2003
|
-
aug_exprs = []
|
|
2004
|
-
for i in range(len(reference_points)):
|
|
2005
|
-
aug_expr = " + ".join([f"({weights[i][obj.symbol]} * {obj.symbol}_min)" for obj in problem.objectives])
|
|
2006
|
-
aug_exprs.append(aug_expr)
|
|
2007
|
-
aug_exprs = " + ".join(aug_exprs)
|
|
2008
|
-
|
|
2009
|
-
func = f"{Op.MAX}({max_terms}) + {rho}*({aug_exprs})"
|
|
2010
|
-
scalarization = ScalarizationFunction(
|
|
2011
|
-
name="STOM scalarization objective function for multiple decision makers",
|
|
2012
|
-
symbol=symbol,
|
|
2013
|
-
func=func,
|
|
2014
|
-
is_linear=problem.is_linear,
|
|
2015
|
-
is_convex=problem.is_convex,
|
|
2016
|
-
is_twice_differentiable=False,
|
|
2017
|
-
)
|
|
2018
|
-
return problem.add_scalarization(scalarization), symbol
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
def add_group_stom_sf_diff(
|
|
2022
|
-
problem: Problem,
|
|
2023
|
-
symbol: str,
|
|
2024
|
-
reference_points: list[dict[str, float]],
|
|
2025
|
-
ideal: dict[str, float] | None = None,
|
|
2026
|
-
rho: float = 1e-6,
|
|
2027
|
-
delta: float = 1e-6,
|
|
2028
|
-
) -> tuple[Problem, str]:
|
|
2029
|
-
r"""Adds the differentiable variant of the multiple decision maker variant of the STOM scalarizing function.
|
|
2030
|
-
|
|
2031
|
-
The scalarization function is defined as follows:
|
|
2032
|
-
|
|
2033
|
-
\begin{align}
|
|
2034
|
-
&\mbox{minimize} && \alpha +
|
|
2035
|
-
\rho \sum^k_{i=1} \sum^{n_d}_{d=1} w_{id}f_{id}(\mathbf{x}) \\
|
|
2036
|
-
&\mbox{subject to} && w_{id}(f_{id}(\mathbf{x})-z^{uto}_{id}) - \alpha \leq 0,\\
|
|
2037
|
-
&&&\mathbf{x} \in \mathbf{X},
|
|
2038
|
-
\end{align}
|
|
2039
|
-
|
|
2040
|
-
where $w_{id} = \frac{1}{\overline{z}_{id} - z^{uto}_{id}}$.
|
|
1157
|
+
References:
|
|
1158
|
+
H. Nakayama, Y. Sawaragi, Satisficing trade-off method for
|
|
1159
|
+
multiobjective programming, in: M. Grauer, A.P. Wierzbicki (Eds.),
|
|
1160
|
+
Interactive Decision Analysis, Springer Verlag, Berlin, 1984, pp.
|
|
1161
|
+
113-122.
|
|
2041
1162
|
|
|
2042
1163
|
Args:
|
|
2043
1164
|
problem (Problem): the problem the scalarization is added to.
|
|
2044
1165
|
symbol (str): the symbol given to the added scalarization.
|
|
2045
|
-
|
|
1166
|
+
reference_point (dict[str, float]): a dict with keys corresponding to objective
|
|
2046
1167
|
function symbols and values to reference point components, i.e.,
|
|
2047
1168
|
aspiration levels.
|
|
2048
1169
|
ideal (dict[str, float], optional): ideal point values. If not given, attempt will be made
|
|
@@ -2051,18 +1172,14 @@ def add_group_stom_sf_diff(
|
|
|
2051
1172
|
function of the scalarization. Defaults to 1e-6.
|
|
2052
1173
|
delta (float, optional): a small scalar value to define the utopian point. Defaults to 1e-6.
|
|
2053
1174
|
|
|
2054
|
-
Raises:
|
|
2055
|
-
ScalarizationError: there are missing elements in any reference point.
|
|
2056
|
-
|
|
2057
1175
|
Returns:
|
|
2058
1176
|
tuple[Problem, str]: a tuple with the copy of the problem with the added
|
|
2059
1177
|
scalarization and the symbol of the added scalarization.
|
|
2060
1178
|
"""
|
|
2061
|
-
# check reference
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
raise ScalarizationError(msg)
|
|
1179
|
+
# check reference point
|
|
1180
|
+
if not objective_dict_has_all_symbols(problem, reference_point):
|
|
1181
|
+
msg = f"The give reference point {reference_point} is missing value for one or more objectives."
|
|
1182
|
+
raise ScalarizationError(msg)
|
|
2066
1183
|
|
|
2067
1184
|
# check if ideal point is specified
|
|
2068
1185
|
# if not specified, try to calculate corrected ideal point
|
|
@@ -2074,75 +1191,36 @@ def add_group_stom_sf_diff(
|
|
|
2074
1191
|
msg = "Ideal point not defined!"
|
|
2075
1192
|
raise ScalarizationError(msg)
|
|
2076
1193
|
|
|
2077
|
-
|
|
2078
|
-
alpha = Variable(
|
|
2079
|
-
name="alpha",
|
|
2080
|
-
symbol="_alpha",
|
|
2081
|
-
variable_type=VariableTypeEnum.real,
|
|
2082
|
-
lowerbound=-float("Inf"),
|
|
2083
|
-
upperbound=float("Inf"),
|
|
2084
|
-
initial_value=1.0,
|
|
2085
|
-
)
|
|
2086
|
-
|
|
2087
|
-
# calculate the weights
|
|
2088
|
-
weights = []
|
|
2089
|
-
for reference_point in reference_points:
|
|
2090
|
-
corrected_rp = get_corrected_reference_point(problem, reference_point)
|
|
2091
|
-
weights.append(
|
|
2092
|
-
{
|
|
2093
|
-
obj.symbol: 1 / (corrected_rp[obj.symbol] - (ideal_point[obj.symbol] - delta))
|
|
2094
|
-
for obj in problem.objectives
|
|
2095
|
-
}
|
|
2096
|
-
)
|
|
2097
|
-
|
|
2098
|
-
# form the max term
|
|
2099
|
-
con_terms = []
|
|
2100
|
-
for i in range(len(reference_points)):
|
|
2101
|
-
rp = {}
|
|
2102
|
-
for obj in problem.objectives:
|
|
2103
|
-
rp[obj.symbol] = (
|
|
2104
|
-
f"{weights[i][obj.symbol]} * ({obj.symbol}_min - {ideal_point[obj.symbol] - delta}) - _alpha"
|
|
2105
|
-
)
|
|
2106
|
-
con_terms.append(rp)
|
|
2107
|
-
|
|
2108
|
-
# form the augmentation term
|
|
2109
|
-
aug_exprs = []
|
|
2110
|
-
for i in range(len(reference_points)):
|
|
2111
|
-
aug_expr = " + ".join([f"({weights[i][obj.symbol]} * {obj.symbol}_min)" for obj in problem.objectives])
|
|
2112
|
-
aug_exprs.append(aug_expr)
|
|
2113
|
-
aug_exprs = " + ".join(aug_exprs)
|
|
1194
|
+
corrected_rp = flip_maximized_objective_values(problem, reference_point)
|
|
2114
1195
|
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
# constraint, should not change.
|
|
2122
|
-
constraints.append(
|
|
2123
|
-
Constraint(
|
|
2124
|
-
name=f"Constraint for {obj.symbol}",
|
|
2125
|
-
symbol=f"{obj.symbol}_con_{i+1}",
|
|
2126
|
-
func=con_terms[i][obj.symbol],
|
|
2127
|
-
cons_type=ConstraintTypeEnum.LTE,
|
|
2128
|
-
is_linear=obj.is_linear,
|
|
2129
|
-
is_convex=obj.is_convex,
|
|
2130
|
-
is_twice_differentiable=obj.is_twice_differentiable,
|
|
2131
|
-
)
|
|
1196
|
+
# define the objective function of the scalarization
|
|
1197
|
+
max_expr = ", ".join(
|
|
1198
|
+
[
|
|
1199
|
+
(
|
|
1200
|
+
f"({obj.symbol}_min - {ideal_point[obj.symbol] - delta}) / "
|
|
1201
|
+
f"({(corrected_rp[obj.symbol] - ideal_point[obj.symbol]) + delta})"
|
|
2132
1202
|
)
|
|
1203
|
+
for obj in problem.objectives
|
|
1204
|
+
]
|
|
1205
|
+
)
|
|
1206
|
+
aug_expr = " + ".join(
|
|
1207
|
+
[
|
|
1208
|
+
f"{obj.symbol}_min / ({(reference_point[obj.symbol] - ideal_point[obj.symbol]) + delta})"
|
|
1209
|
+
for obj in problem.objectives
|
|
1210
|
+
]
|
|
1211
|
+
)
|
|
2133
1212
|
|
|
2134
|
-
|
|
1213
|
+
target_expr = f"{Op.MAX}({max_expr}) + {rho}*" + f"({aug_expr})"
|
|
2135
1214
|
scalarization = ScalarizationFunction(
|
|
2136
|
-
name="
|
|
1215
|
+
name="STOM scalarization objective function",
|
|
2137
1216
|
symbol=symbol,
|
|
2138
|
-
func=
|
|
2139
|
-
is_linear=
|
|
2140
|
-
is_convex=
|
|
2141
|
-
is_twice_differentiable=
|
|
1217
|
+
func=target_expr,
|
|
1218
|
+
is_linear=False,
|
|
1219
|
+
is_convex=False,
|
|
1220
|
+
is_twice_differentiable=False,
|
|
2142
1221
|
)
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
return _problem.add_constraints(constraints), symbol
|
|
1222
|
+
|
|
1223
|
+
return problem.add_scalarization(scalarization), symbol
|
|
2146
1224
|
|
|
2147
1225
|
|
|
2148
1226
|
def add_guess_sf_diff(
|
|
@@ -2157,29 +1235,33 @@ def add_guess_sf_diff(
|
|
|
2157
1235
|
r"""Adds the differentiable variant of the GUESS scalarizing function.
|
|
2158
1236
|
|
|
2159
1237
|
\begin{align*}
|
|
2160
|
-
\min \quad & \alpha + \rho \sum_{i=1}^k \frac{f_i(\mathbf{x})}{
|
|
2161
|
-
\
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
\begin{cases}
|
|
2165
|
-
z^\text{nad}_i - \bar{z}_i,\quad \forall i \notin I^\diamond,\\
|
|
2166
|
-
z^\text{nad}_i - z^{\star\star}_i,\quad \forall i \in I^\diamond,\\
|
|
2167
|
-
\end{cases}\\
|
|
1238
|
+
\min \quad & \alpha + \rho \sum_{i=1}^k \frac{f_i(\mathbf{x})}{z_i^{nad} - \bar{z}_i},
|
|
1239
|
+
\quad & \\
|
|
1240
|
+
\text{s.t.} \quad & \frac{f_i(\mathbf{x}) - z_i^{nad}}{z_i^{nad} - \bar{z}_i}
|
|
1241
|
+
- \alpha \leq 0 \quad & \forall i \notin I^{\diamond},\\
|
|
2168
1242
|
& \mathbf{x} \in S,
|
|
2169
1243
|
\end{align*}
|
|
2170
1244
|
|
|
2171
|
-
where $
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
set $I^\diamond$ represents objective vectors whose values are free to
|
|
2176
|
-
belonging to this set are interpreted as those objective
|
|
2177
|
-
the reference point is set to be the the
|
|
1245
|
+
where $f_{i}$ are objective functions, $z_{i}^{nad}$ is a component of the
|
|
1246
|
+
nadir point, $\bar{z}_{i}$
|
|
1247
|
+
is a component of the reference point, $\rho$ is a small scalar
|
|
1248
|
+
value, and $S$ is the feasible solution space of the original problem. The
|
|
1249
|
+
index set $I^\diamond$ represents objective vectors whose values are free to
|
|
1250
|
+
change. The indices belonging to this set are interpreted as those objective
|
|
1251
|
+
vectors whose components in the reference point is set to be the the
|
|
1252
|
+
respective nadir point component of the problem. Note that in Buchanan (1997),
|
|
1253
|
+
the GUESS method considers all objective functions, i.e. $I^\diamond$ is
|
|
1254
|
+
an empty set. The functionality to have free-to-change objectives was added
|
|
1255
|
+
in Miettinen & Mäkelä (2006).
|
|
2178
1256
|
|
|
2179
1257
|
References:
|
|
2180
1258
|
Buchanan, J. T. (1997). A naive approach for solving MCDM problems: The
|
|
2181
1259
|
GUESS method. Journal of the Operational Research Society, 48, 202-206.
|
|
2182
1260
|
|
|
1261
|
+
Miettinen, K., & Mäkelä, M. M. (2006). Synchronous approach in interactive
|
|
1262
|
+
multiobjective optimization. European Journal of Operational Research,
|
|
1263
|
+
170(3), 909-922.
|
|
1264
|
+
|
|
2183
1265
|
Args:
|
|
2184
1266
|
problem (Problem): the problem the scalarization is added to.
|
|
2185
1267
|
symbol (str): the symbol given to the added scalarization.
|
|
@@ -2192,7 +1274,7 @@ def add_guess_sf_diff(
|
|
|
2192
1274
|
to calculate nadir point from problem.
|
|
2193
1275
|
rho (float, optional): a small scalar value to scale the sum in the objective
|
|
2194
1276
|
function of the scalarization. Defaults to 1e-6.
|
|
2195
|
-
delta (float, optional): a small scalar to define the utopian point. Defaults to 1e-6.
|
|
1277
|
+
delta (float, optional): a small scalar value to define the utopian point. Defaults to 1e-6.
|
|
2196
1278
|
|
|
2197
1279
|
Returns:
|
|
2198
1280
|
tuple[Problem, str]: a tuple with the copy of the problem with the added
|
|
@@ -2223,7 +1305,7 @@ def add_guess_sf_diff(
|
|
|
2223
1305
|
msg = "Nadir point not defined!"
|
|
2224
1306
|
raise ScalarizationError(msg)
|
|
2225
1307
|
|
|
2226
|
-
corrected_rp =
|
|
1308
|
+
corrected_rp = flip_maximized_objective_values(problem, reference_point)
|
|
2227
1309
|
|
|
2228
1310
|
# the indices that are free to change, set if component of reference point
|
|
2229
1311
|
# has the corresponding nadir value, or if it is greater than the nadir value
|
|
@@ -2246,9 +1328,10 @@ def add_guess_sf_diff(
|
|
|
2246
1328
|
# define the objective function of the scalarization
|
|
2247
1329
|
aug_expr = " + ".join(
|
|
2248
1330
|
[
|
|
2249
|
-
(
|
|
2250
|
-
f"{obj.symbol}_min / ({nadir_point[obj.symbol]} - "
|
|
2251
|
-
|
|
1331
|
+
( # Technically delta should be included (according to the paper), but I'm a rebel and don't want to add it
|
|
1332
|
+
f"{obj.symbol}_min / ({nadir_point[obj.symbol]} - {ideal_point[obj.symbol]})"
|
|
1333
|
+
if obj.symbol in free_to_change
|
|
1334
|
+
else f"{obj.symbol}_min / ({nadir_point[obj.symbol]} - {corrected_rp[obj.symbol]})"
|
|
2252
1335
|
)
|
|
2253
1336
|
for obj in problem.objectives
|
|
2254
1337
|
]
|
|
@@ -2301,39 +1384,40 @@ def add_guess_sf_nondiff(
|
|
|
2301
1384
|
ideal: dict[str, float] | None = None,
|
|
2302
1385
|
nadir: dict[str, float] | None = None,
|
|
2303
1386
|
rho: float = 1e-6,
|
|
2304
|
-
delta: float = 1e-6,
|
|
2305
1387
|
) -> tuple[Problem, str]:
|
|
2306
1388
|
r"""Adds the non-differentiable variant of the GUESS scalarizing function.
|
|
2307
1389
|
|
|
2308
1390
|
\begin{align*}
|
|
2309
1391
|
\underset{\mathbf{x}}{\min}\quad & \underset{i \notin I^\diamond}{\max}
|
|
2310
1392
|
\left[
|
|
2311
|
-
\frac{f_i(\mathbf{x}) - z_i^{
|
|
1393
|
+
\frac{f_i(\mathbf{x}) - z_i^{nad}}{z_i^{nad} - \bar{z}_i}
|
|
2312
1394
|
\right]
|
|
2313
|
-
+ \rho \sum_{
|
|
1395
|
+
+ \rho \sum_{i=1}^k \frac{f_i(\mathbf{x})}{z_i^{nad} - \bar{z}_i},
|
|
2314
1396
|
\quad & \\
|
|
2315
1397
|
\text{s.t.}\quad
|
|
2316
|
-
& d_j =
|
|
2317
|
-
\begin{cases}
|
|
2318
|
-
z^\text{nad}_j - \bar{z}_j,\quad \forall j \notin I^\diamond,\\
|
|
2319
|
-
z^\text{nad}_j - z^{\star\star}_j,\quad \forall j \in I^\diamond,\\
|
|
2320
|
-
\end{cases}\\
|
|
2321
1398
|
& \mathbf{x} \in S,
|
|
2322
1399
|
\end{align*}
|
|
2323
1400
|
|
|
2324
|
-
where $f_{i
|
|
2325
|
-
|
|
2326
|
-
is a component of the reference point, $\rho$
|
|
2327
|
-
|
|
1401
|
+
where $f_{i}$ are objective functions, $z_{i}^{nad}$ is a component of the
|
|
1402
|
+
nadir point, $\bar{z}_{i}$
|
|
1403
|
+
is a component of the reference point, $\rho$ is a small scalar
|
|
1404
|
+
value, and $S$ is the feasible solution space of the original problem. The
|
|
2328
1405
|
index set $I^\diamond$ represents objective vectors whose values are free to
|
|
2329
1406
|
change. The indices belonging to this set are interpreted as those objective
|
|
2330
1407
|
vectors whose components in the reference point is set to be the the
|
|
2331
|
-
respective nadir point component of the problem.
|
|
1408
|
+
respective nadir point component of the problem. Note that in Buchanan (1997),
|
|
1409
|
+
the GUESS method considers all objective functions, i.e. $I^\diamond$ is
|
|
1410
|
+
an empty set. The functionality to have free-to-change objectives was added
|
|
1411
|
+
in Miettinen & Mäkelä (2006).
|
|
2332
1412
|
|
|
2333
1413
|
References:
|
|
2334
1414
|
Buchanan, J. T. (1997). A naive approach for solving MCDM problems: The
|
|
2335
1415
|
GUESS method. Journal of the Operational Research Society, 48, 202-206.
|
|
2336
1416
|
|
|
1417
|
+
Miettinen, K., & Mäkelä, M. M. (2006). Synchronous approach in interactive
|
|
1418
|
+
multiobjective optimization. European Journal of Operational Research,
|
|
1419
|
+
170(3), 909-922.
|
|
1420
|
+
|
|
2337
1421
|
Args:
|
|
2338
1422
|
problem (Problem): the problem the scalarization is added to.
|
|
2339
1423
|
symbol (str): the symbol given to the added scalarization.
|
|
@@ -2377,7 +1461,7 @@ def add_guess_sf_nondiff(
|
|
|
2377
1461
|
msg = "Nadir point not defined!"
|
|
2378
1462
|
raise ScalarizationError(msg)
|
|
2379
1463
|
|
|
2380
|
-
corrected_rp =
|
|
1464
|
+
corrected_rp = flip_maximized_objective_values(problem, reference_point)
|
|
2381
1465
|
|
|
2382
1466
|
# the indices that are free to change, set if component of reference point
|
|
2383
1467
|
# has the corresponding nadir value, or if it is greater than the nadir value
|
|
@@ -2393,8 +1477,8 @@ def add_guess_sf_nondiff(
|
|
|
2393
1477
|
max_expr = ", ".join(
|
|
2394
1478
|
[
|
|
2395
1479
|
(
|
|
2396
|
-
f"({obj.symbol}_min - {(
|
|
2397
|
-
f"({
|
|
1480
|
+
f"({obj.symbol}_min - {(nadir_point[obj.symbol])}) / "
|
|
1481
|
+
f"({nadir_point[obj.symbol]} - {(corrected_rp[obj.symbol])})"
|
|
2398
1482
|
)
|
|
2399
1483
|
for obj in problem.objectives
|
|
2400
1484
|
if obj.symbol not in free_to_change
|
|
@@ -2404,9 +1488,10 @@ def add_guess_sf_nondiff(
|
|
|
2404
1488
|
# define the augmentation term
|
|
2405
1489
|
aug_expr = " + ".join(
|
|
2406
1490
|
[
|
|
2407
|
-
(
|
|
2408
|
-
f"{obj.symbol}_min / ({nadir_point[obj.symbol]} - "
|
|
2409
|
-
|
|
1491
|
+
( # Technically delta should be included (according to the paper), but I'm a rebel and don't want to add it
|
|
1492
|
+
f"{obj.symbol}_min / ({nadir_point[obj.symbol]} - {ideal_point[obj.symbol]})"
|
|
1493
|
+
if obj.symbol in free_to_change
|
|
1494
|
+
else f"{obj.symbol}_min / ({nadir_point[obj.symbol]} - {corrected_rp[obj.symbol]})"
|
|
2410
1495
|
)
|
|
2411
1496
|
for obj in problem.objectives
|
|
2412
1497
|
]
|
|
@@ -2425,225 +1510,6 @@ def add_guess_sf_nondiff(
|
|
|
2425
1510
|
return problem.add_scalarization(scalarization), symbol
|
|
2426
1511
|
|
|
2427
1512
|
|
|
2428
|
-
def add_group_guess_sf(
|
|
2429
|
-
problem: Problem,
|
|
2430
|
-
symbol: str,
|
|
2431
|
-
reference_points: list[dict[str, float]],
|
|
2432
|
-
nadir: dict[str, float] | None = None,
|
|
2433
|
-
rho: float = 1e-6,
|
|
2434
|
-
delta: float = 1e-6,
|
|
2435
|
-
) -> tuple[Problem, str]:
|
|
2436
|
-
r"""Adds the non-differentiable variant of the multiple decision maker variant of the GUESS scalarizing function.
|
|
2437
|
-
|
|
2438
|
-
The scalarization function is defined as follows:
|
|
2439
|
-
|
|
2440
|
-
\begin{align}
|
|
2441
|
-
&\mbox{minimize} &&\max_{i,d} [w_{id}(f_{id}(\mathbf{x})-z^{nad}_{id})] +
|
|
2442
|
-
\rho \sum^k_{i=1} \sum^{n_d}_{d=1} w_{id}f_{id}(\mathbf{x}) \\
|
|
2443
|
-
&\mbox{subject to} &&\mathbf{x} \in \mathbf{X},
|
|
2444
|
-
\end{align}
|
|
2445
|
-
|
|
2446
|
-
where $w_{id} = \frac{1}{z^{nad}_{id} - \overline{z}_{id}}$.
|
|
2447
|
-
|
|
2448
|
-
Args:
|
|
2449
|
-
problem (Problem): the problem the scalarization is added to.
|
|
2450
|
-
symbol (str): the symbol given to the added scalarization.
|
|
2451
|
-
reference_points (list[dict[str, float]]): a list of dicts with keys corresponding to objective
|
|
2452
|
-
function symbols and values to reference point components, i.e.,
|
|
2453
|
-
aspiration levels.
|
|
2454
|
-
nadir (dict[str, float], optional): nadir point values. If not given, attempt will be made
|
|
2455
|
-
to calculate nadir point from problem.
|
|
2456
|
-
rho (float, optional): a small scalar value to scale the sum in the objective
|
|
2457
|
-
function of the scalarization. Defaults to 1e-6.
|
|
2458
|
-
delta (float, optional): a small scalar to define the utopian point. Defaults to 1e-6.
|
|
2459
|
-
|
|
2460
|
-
Raises:
|
|
2461
|
-
ScalarizationError: there are missing elements in any reference point.
|
|
2462
|
-
|
|
2463
|
-
Returns:
|
|
2464
|
-
tuple[Problem, str]: a tuple with the copy of the problem with the added
|
|
2465
|
-
scalarization and the symbol of the added scalarization.
|
|
2466
|
-
"""
|
|
2467
|
-
# check reference points
|
|
2468
|
-
for reference_point in reference_points:
|
|
2469
|
-
if not objective_dict_has_all_symbols(problem, reference_point):
|
|
2470
|
-
msg = f"The give reference point {reference_point} is missing value for one or more objectives."
|
|
2471
|
-
raise ScalarizationError(msg)
|
|
2472
|
-
|
|
2473
|
-
# check if nadir point is specified
|
|
2474
|
-
# if not specified, try to calculate corrected nadir point
|
|
2475
|
-
if nadir is not None:
|
|
2476
|
-
nadir_point = nadir
|
|
2477
|
-
elif problem.get_nadir_point() is not None:
|
|
2478
|
-
nadir_point = get_corrected_nadir(problem)
|
|
2479
|
-
else:
|
|
2480
|
-
msg = "Nadir point not defined!"
|
|
2481
|
-
raise ScalarizationError(msg)
|
|
2482
|
-
|
|
2483
|
-
# calculate the weights
|
|
2484
|
-
weights = []
|
|
2485
|
-
for reference_point in reference_points:
|
|
2486
|
-
corrected_rp = get_corrected_reference_point(problem, reference_point)
|
|
2487
|
-
weights.append(
|
|
2488
|
-
{
|
|
2489
|
-
obj.symbol: 1 / ((nadir_point[obj.symbol] + delta) - (corrected_rp[obj.symbol]))
|
|
2490
|
-
for obj in problem.objectives
|
|
2491
|
-
}
|
|
2492
|
-
)
|
|
2493
|
-
|
|
2494
|
-
# form the max term
|
|
2495
|
-
max_terms = []
|
|
2496
|
-
for i in range(len(reference_points)):
|
|
2497
|
-
corrected_rp = get_corrected_reference_point(problem, reference_points[i])
|
|
2498
|
-
for obj in problem.objectives:
|
|
2499
|
-
max_terms.append(f"{weights[i][obj.symbol]} * ({obj.symbol}_min - {nadir_point[obj.symbol]})")
|
|
2500
|
-
max_terms = ", ".join(max_terms)
|
|
2501
|
-
|
|
2502
|
-
# form the augmentation term
|
|
2503
|
-
aug_exprs = []
|
|
2504
|
-
for i in range(len(reference_points)):
|
|
2505
|
-
aug_expr = " + ".join([f"({weights[i][obj.symbol]} * {obj.symbol}_min)" for obj in problem.objectives])
|
|
2506
|
-
aug_exprs.append(aug_expr)
|
|
2507
|
-
aug_exprs = " + ".join(aug_exprs)
|
|
2508
|
-
|
|
2509
|
-
func = f"{Op.MAX}({max_terms}) + {rho}*({aug_exprs})"
|
|
2510
|
-
scalarization = ScalarizationFunction(
|
|
2511
|
-
name="GUESS scalarization objective function for multiple decision makers",
|
|
2512
|
-
symbol=symbol,
|
|
2513
|
-
func=func,
|
|
2514
|
-
is_linear=problem.is_linear,
|
|
2515
|
-
is_convex=problem.is_convex,
|
|
2516
|
-
is_twice_differentiable=False,
|
|
2517
|
-
)
|
|
2518
|
-
return problem.add_scalarization(scalarization), symbol
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
def add_group_guess_sf_diff(
|
|
2522
|
-
problem: Problem,
|
|
2523
|
-
symbol: str,
|
|
2524
|
-
reference_points: list[dict[str, float]],
|
|
2525
|
-
nadir: dict[str, float] | None = None,
|
|
2526
|
-
rho: float = 1e-6,
|
|
2527
|
-
delta: float = 1e-6,
|
|
2528
|
-
) -> tuple[Problem, str]:
|
|
2529
|
-
r"""Adds the differentiable variant of the multiple decision maker variant of the GUESS scalarizing function.
|
|
2530
|
-
|
|
2531
|
-
The scalarization function is defined as follows:
|
|
2532
|
-
|
|
2533
|
-
\begin{align}
|
|
2534
|
-
&\mbox{minimize} &&\alpha +
|
|
2535
|
-
\rho \sum^k_{i=1} \sum^{n_d}_{d=1} w_{id}f_{id}(\mathbf{x}) \\
|
|
2536
|
-
&\mbox{subject to} && w_{id}(f_{id}(\mathbf{x})-z^{nad}_{id}) - \alpha \leq 0,\\
|
|
2537
|
-
&&&\mathbf{x} \in \mathbf{X},
|
|
2538
|
-
\end{align}
|
|
2539
|
-
|
|
2540
|
-
where $w_{id} = \frac{1}{z^{nad}_{id} - \overline{z}_{id}}$.
|
|
2541
|
-
|
|
2542
|
-
Args:
|
|
2543
|
-
problem (Problem): the problem the scalarization is added to.
|
|
2544
|
-
symbol (str): the symbol given to the added scalarization.
|
|
2545
|
-
reference_points (list[dict[str, float]]): a list of dicts with keys corresponding to objective
|
|
2546
|
-
function symbols and values to reference point components, i.e.,
|
|
2547
|
-
aspiration levels.
|
|
2548
|
-
nadir (dict[str, float], optional): nadir point values. If not given, attempt will be made
|
|
2549
|
-
to calculate nadir point from problem.
|
|
2550
|
-
rho (float, optional): a small scalar value to scale the sum in the objective
|
|
2551
|
-
function of the scalarization. Defaults to 1e-6.
|
|
2552
|
-
delta (float, optional): a small scalar to define the utopian point. Defaults to 1e-6.
|
|
2553
|
-
|
|
2554
|
-
Raises:
|
|
2555
|
-
ScalarizationError: there are missing elements in any reference point.
|
|
2556
|
-
|
|
2557
|
-
Returns:
|
|
2558
|
-
tuple[Problem, str]: a tuple with the copy of the problem with the added
|
|
2559
|
-
scalarization and the symbol of the added scalarization.
|
|
2560
|
-
"""
|
|
2561
|
-
# check reference points
|
|
2562
|
-
for reference_point in reference_points:
|
|
2563
|
-
if not objective_dict_has_all_symbols(problem, reference_point):
|
|
2564
|
-
msg = f"The give reference point {reference_point} is missing value for one or more objectives."
|
|
2565
|
-
raise ScalarizationError(msg)
|
|
2566
|
-
|
|
2567
|
-
# check if nadir point is specified
|
|
2568
|
-
# if not specified, try to calculate corrected nadir point
|
|
2569
|
-
if nadir is not None:
|
|
2570
|
-
nadir_point = nadir
|
|
2571
|
-
elif problem.get_nadir_point() is not None:
|
|
2572
|
-
nadir_point = get_corrected_nadir(problem)
|
|
2573
|
-
else:
|
|
2574
|
-
msg = "Nadir point not defined!"
|
|
2575
|
-
raise ScalarizationError(msg)
|
|
2576
|
-
|
|
2577
|
-
# define the auxiliary variable
|
|
2578
|
-
alpha = Variable(
|
|
2579
|
-
name="alpha",
|
|
2580
|
-
symbol="_alpha",
|
|
2581
|
-
variable_type=VariableTypeEnum.real,
|
|
2582
|
-
lowerbound=-float("Inf"),
|
|
2583
|
-
upperbound=float("Inf"),
|
|
2584
|
-
initial_value=1.0,
|
|
2585
|
-
)
|
|
2586
|
-
|
|
2587
|
-
# calculate the weights
|
|
2588
|
-
weights = []
|
|
2589
|
-
for reference_point in reference_points:
|
|
2590
|
-
corrected_rp = get_corrected_reference_point(problem, reference_point)
|
|
2591
|
-
weights.append(
|
|
2592
|
-
{
|
|
2593
|
-
obj.symbol: 1 / ((nadir_point[obj.symbol] + delta) - (corrected_rp[obj.symbol]))
|
|
2594
|
-
for obj in problem.objectives
|
|
2595
|
-
}
|
|
2596
|
-
)
|
|
2597
|
-
|
|
2598
|
-
# form the max term
|
|
2599
|
-
con_terms = []
|
|
2600
|
-
for i in range(len(reference_points)):
|
|
2601
|
-
corrected_rp = get_corrected_reference_point(problem, reference_points[i])
|
|
2602
|
-
rp = {}
|
|
2603
|
-
for obj in problem.objectives:
|
|
2604
|
-
rp[obj.symbol] = f"{weights[i][obj.symbol]} * ({obj.symbol}_min - {nadir_point[obj.symbol]}) - _alpha"
|
|
2605
|
-
con_terms.append(rp)
|
|
2606
|
-
|
|
2607
|
-
# form the augmentation term
|
|
2608
|
-
aug_exprs = []
|
|
2609
|
-
for i in range(len(reference_points)):
|
|
2610
|
-
aug_expr = " + ".join([f"({weights[i][obj.symbol]} * {obj.symbol}_min)" for obj in problem.objectives])
|
|
2611
|
-
aug_exprs.append(aug_expr)
|
|
2612
|
-
aug_exprs = " + ".join(aug_exprs)
|
|
2613
|
-
|
|
2614
|
-
constraints = []
|
|
2615
|
-
# loop to create a constraint for every objective of every reference point given
|
|
2616
|
-
for i in range(len(reference_points)):
|
|
2617
|
-
for obj in problem.objectives:
|
|
2618
|
-
# since we are subtracting a constant value, the linearity, convexity,
|
|
2619
|
-
# and differentiability of the objective function, and hence the
|
|
2620
|
-
# constraint, should not change.
|
|
2621
|
-
constraints.append(
|
|
2622
|
-
Constraint(
|
|
2623
|
-
name=f"Constraint for {obj.symbol}",
|
|
2624
|
-
symbol=f"{obj.symbol}_con_{i+1}",
|
|
2625
|
-
func=con_terms[i][obj.symbol],
|
|
2626
|
-
cons_type=ConstraintTypeEnum.LTE,
|
|
2627
|
-
is_linear=obj.is_linear,
|
|
2628
|
-
is_convex=obj.is_convex,
|
|
2629
|
-
is_twice_differentiable=obj.is_twice_differentiable,
|
|
2630
|
-
)
|
|
2631
|
-
)
|
|
2632
|
-
|
|
2633
|
-
func = f"_alpha + {rho}*({aug_exprs})"
|
|
2634
|
-
scalarization = ScalarizationFunction(
|
|
2635
|
-
name="Differentiable GUESS scalarization objective function for multiple decision makers",
|
|
2636
|
-
symbol=symbol,
|
|
2637
|
-
func=func,
|
|
2638
|
-
is_linear=problem.is_linear,
|
|
2639
|
-
is_convex=problem.is_convex,
|
|
2640
|
-
is_twice_differentiable=problem.is_twice_differentiable,
|
|
2641
|
-
)
|
|
2642
|
-
_problem = problem.add_variables([alpha])
|
|
2643
|
-
_problem = _problem.add_scalarization(scalarization)
|
|
2644
|
-
return _problem.add_constraints(constraints), symbol
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
1513
|
def add_asf_diff(
|
|
2648
1514
|
problem: Problem,
|
|
2649
1515
|
symbol: str,
|
|
@@ -2717,7 +1583,7 @@ def add_asf_diff(
|
|
|
2717
1583
|
msg = "Nadir point not defined!"
|
|
2718
1584
|
raise ScalarizationError(msg)
|
|
2719
1585
|
|
|
2720
|
-
corrected_rp =
|
|
1586
|
+
corrected_rp = flip_maximized_objective_values(problem, reference_point)
|
|
2721
1587
|
|
|
2722
1588
|
# define the auxiliary variable
|
|
2723
1589
|
alpha = Variable(
|
|
@@ -2979,160 +1845,221 @@ def create_epsilon_constraints_json(
|
|
|
2979
1845
|
return scalarization_expr, constraint_exprs
|
|
2980
1846
|
|
|
2981
1847
|
|
|
2982
|
-
def
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
) ->
|
|
2989
|
-
r"""
|
|
1848
|
+
def __create_HDF(
|
|
1849
|
+
y: str,
|
|
1850
|
+
a: float,
|
|
1851
|
+
r: float,
|
|
1852
|
+
d1: float = 0.9,
|
|
1853
|
+
d2: float = 0.1,
|
|
1854
|
+
) -> str:
|
|
1855
|
+
r"""Create a Harrington's one-sided desirability function.
|
|
2990
1856
|
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
\min_{\mathbf{x}}\quad
|
|
2994
|
-
&\max_{i,p}\bigl[w_{ip}\bigl(f_{ip}(\mathbf{x}) - \bar z_{ip}\bigr)\bigr]
|
|
2995
|
-
\;+\;\varepsilon \sum_{i,p} w_{ip}\bigl(f_{ip}(\mathbf{x}) - \bar z_{ip}\bigr) \\[6pt]
|
|
2996
|
-
\text{s.t.}\quad
|
|
2997
|
-
&\mathbf{x} \in \mathcal{X}\,,
|
|
2998
|
-
\end{align}
|
|
1857
|
+
Harrington's desirability function is used to compute the desirability of a
|
|
1858
|
+
given value of an objective function based on its aspiration and reservation levels.
|
|
2999
1859
|
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
function symbols and values to reference point components, i.e., aspiration levels.
|
|
3005
|
-
weights (list[dict[str, float]]): the list of weights to be used in the scalarization function.
|
|
3006
|
-
Must be positive.
|
|
3007
|
-
epsilon: small augmentation multiplier ε
|
|
1860
|
+
The desirability function is defined as follows:
|
|
1861
|
+
\begin{equation}
|
|
1862
|
+
D(y) = \exp\left(-\exp\left(-b_0 - b_1 y\right)\right),
|
|
1863
|
+
\end{equation}
|
|
3008
1864
|
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
1865
|
+
where
|
|
1866
|
+
\begin{align*}
|
|
1867
|
+
b_0 &= -\log(-\log(d_1)) - b_1 a, \\
|
|
1868
|
+
b_1 &= \frac{\log(-\log(d_2)) - \log(-\log(d_1))}{r - a}.
|
|
1869
|
+
\end{align*}
|
|
1870
|
+
|
|
1871
|
+
The desirability function returns a value between 0 and 1, where higher values indicate
|
|
1872
|
+
more desirable outcomes. I took the equations from the following source:
|
|
1873
|
+
Wagner, T., and Trautmann, H. Integration of preference in hypervolume-based
|
|
1874
|
+
multiobjective evolutionary algorithms by means of desirability functions.
|
|
1875
|
+
IEEE Transactions on Evolutionary Computation 14, 5 (2010), 688-701.
|
|
1876
|
+
|
|
1877
|
+
Args:
|
|
1878
|
+
y (str): The objective value to compute the desirability for.
|
|
1879
|
+
a (float): Aspiration level for the objective.
|
|
1880
|
+
r (float): Reservation level for the objective.
|
|
1881
|
+
d1 (float): The desirability for the aspiration level.
|
|
1882
|
+
d2 (float): The desirability for the reservation level.
|
|
1883
|
+
|
|
1884
|
+
Returns:
|
|
1885
|
+
callable (Function): A function that computes the desirability for a given value.
|
|
3012
1886
|
"""
|
|
3013
|
-
if
|
|
3014
|
-
raise
|
|
1887
|
+
if not (0 < d1 < 1 and 0 < d2 < 1):
|
|
1888
|
+
raise ValueError("Desirability values must be between 0 and 1 (exclusive).")
|
|
1889
|
+
if not (a < r):
|
|
1890
|
+
raise ValueError("a must be less than r.")
|
|
1891
|
+
if not d2 < d1:
|
|
1892
|
+
raise ValueError("d2 must be less than d1. Higher desirability should correspond to lower values of y.")
|
|
1893
|
+
b1: float = -np.log(-np.log(d2)) + np.log(-np.log(d1)) / (r - a)
|
|
1894
|
+
b0: float = -np.log(-np.log(d1)) - b1 * a
|
|
3015
1895
|
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
f"The give reference point {reference_point} " f"is missing value for one or more objectives."
|
|
3020
|
-
)
|
|
3021
|
-
if not objective_dict_has_all_symbols(problem, weight):
|
|
3022
|
-
raise ScalarizationError(
|
|
3023
|
-
f"The given weight vector {weight} is missing " f"a value for one or more objectives."
|
|
3024
|
-
)
|
|
1896
|
+
def __HDF(y: float):
|
|
1897
|
+
"""Compute the desirability for a given value."""
|
|
1898
|
+
return np.exp(-np.exp(-(b0 + b1 * y)))
|
|
3025
1899
|
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
for reference_point, weight in zip(reference_points, weights, strict=True):
|
|
3029
|
-
corrected_ref_point = get_corrected_reference_point(problem, reference_point)
|
|
1900
|
+
func = f"Exp(-Exp(-({b0} + {b1} * {y})))"
|
|
1901
|
+
return func
|
|
3030
1902
|
|
|
3031
|
-
for obj in problem.objectives:
|
|
3032
|
-
expr = f"{weight[obj.symbol]}*({obj.symbol}_min - {corrected_ref_point[obj.symbol]})"
|
|
3033
|
-
max_list.append(expr)
|
|
3034
|
-
sum_list.append(expr)
|
|
3035
1903
|
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
func = f"{max_part} + {epsilon}*({sum_part})"
|
|
1904
|
+
def __create_MDF(y: str, a: float, r: float, d1: float = 0.9, d2: float = 0.1) -> str:
|
|
1905
|
+
"""Create MaoMao's desirability function.
|
|
3039
1906
|
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
1907
|
+
Distinctions form MaoMao's original function:
|
|
1908
|
+
- The upper and lower bounds of desirability are fixed to 0 and 1, respectively.
|
|
1909
|
+
|
|
1910
|
+
Args:
|
|
1911
|
+
y (str): The objective value to compute the desirability for.
|
|
1912
|
+
a (float): Aspiration level for the objective.
|
|
1913
|
+
r (float): Reservation level for the objective.
|
|
1914
|
+
d1 (float): The desirability for the aspiration level.
|
|
1915
|
+
d2 (float): The desirability for the reservation level.
|
|
1916
|
+
|
|
1917
|
+
Returns:
|
|
1918
|
+
callable (Function): A function that computes the desirability for a given value.
|
|
1919
|
+
"""
|
|
1920
|
+
if not (0 < d1 < 1 and 0 < d2 < 1):
|
|
1921
|
+
raise ValueError("Desirability values must be between 0 and 1 (exclusive).")
|
|
1922
|
+
if not (a < r):
|
|
1923
|
+
raise ValueError("a must be less than r.")
|
|
1924
|
+
if not d2 < d1:
|
|
1925
|
+
raise ValueError("d2 must be less than d1. Higher desirability should correspond to lower values of y.")
|
|
1926
|
+
ea = 1 - d1
|
|
1927
|
+
er = d2
|
|
1928
|
+
m1 = -ea * ea * (a - r) / (d1 - d2)
|
|
1929
|
+
b1 = -a + ea * (a - r) / (d1 - d2)
|
|
1930
|
+
m2 = (d1 - d2) / (a - r)
|
|
1931
|
+
b2 = (d2 * a - d1 * r) / (a - r)
|
|
1932
|
+
m3 = -er * er * (a - r) / (d1 - d2)
|
|
1933
|
+
b3 = -r - er * (a - r) / (d1 - d2)
|
|
1934
|
+
|
|
1935
|
+
def MDF1(y):
|
|
1936
|
+
"""Compute the desirability for a given value."""
|
|
1937
|
+
if isinstance(y, np.ndarray):
|
|
1938
|
+
return np.array([MDF1(yi) for yi in y])
|
|
1939
|
+
if y < a:
|
|
1940
|
+
return 1 + m1 / (y + b1)
|
|
1941
|
+
elif a <= y <= r:
|
|
1942
|
+
return m2 * y + b2
|
|
1943
|
+
else:
|
|
1944
|
+
return m3 / (y + b3)
|
|
1945
|
+
|
|
1946
|
+
def MDF(y):
|
|
1947
|
+
"""Compute the desirability for a given value."""
|
|
1948
|
+
# Same but without the if statements
|
|
1949
|
+
if isinstance(y, np.ndarray):
|
|
1950
|
+
return np.array([MDF(yi) for yi in y])
|
|
1951
|
+
return (
|
|
1952
|
+
max(a - y, 0) * (1 + m1 / (y + b1)) / (a - y)
|
|
1953
|
+
+ max(y - r, 0) * (m3 / (y + b3)) / (y - r)
|
|
1954
|
+
+ max(y - a, 0) * max(r - y, 0) * (m2 * y + b2) / ((y - a) * (r - y))
|
|
1955
|
+
)
|
|
1956
|
+
|
|
1957
|
+
func = (
|
|
1958
|
+
f"Max({a} - {y}, 0) * (1 + {m1} / ({y} + {b1})) / ({a} - {y}) + "
|
|
1959
|
+
f"Max({y} - {r}, 0) * ({m3} / ({y} + {b3})) / ({y} - {r}) + "
|
|
1960
|
+
f"Max({y} - {a}, 0) * Max({r} - {y}, 0) * ({m2} * {y} + {b2}) / "
|
|
1961
|
+
f"(({y} - {a}) * ({r} - {y}))"
|
|
3047
1962
|
)
|
|
3048
|
-
return
|
|
1963
|
+
return func
|
|
3049
1964
|
|
|
3050
1965
|
|
|
3051
|
-
def
|
|
1966
|
+
def add_desirability_funcs(
|
|
3052
1967
|
problem: Problem,
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
) -> tuple[Problem, str]:
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
\min_{x,\alpha}\quad
|
|
3063
|
-
& \alpha \;+\; \varepsilon \sum_{i,p} w_{ip}\bigl(f_{ip}(x) - \bar z_{ip}\bigr) \\
|
|
3064
|
-
\text{s.t.}\quad
|
|
3065
|
-
& w_{ip}\bigl(f_{ip}(x) - \bar z_{ip}\bigr)\;-\;\alpha \;\le\;0
|
|
3066
|
-
\quad\forall\,i,p,\\
|
|
3067
|
-
& x \in \mathcal{X}\,,
|
|
3068
|
-
\end{align}
|
|
1968
|
+
aspiration_levels: dict[str, float],
|
|
1969
|
+
reservation_levels: dict[str, float],
|
|
1970
|
+
desirability_levels: dict[str, tuple[float, float]] | None = None,
|
|
1971
|
+
desirability_func: Literal["Harrington", "MaoMao"] = "Harrington",
|
|
1972
|
+
) -> tuple[Problem, list[str]]:
|
|
1973
|
+
"""Adds desirability functions to the problem based on the given aspiration and reservation levels.
|
|
1974
|
+
|
|
1975
|
+
Note that the desirability functions are added as scalarization functions to the problem. They are also multiplied
|
|
1976
|
+
by -1 to ensure that "desirability" values can be minimized, as is assumed by the optimizers.
|
|
3069
1977
|
|
|
3070
1978
|
Args:
|
|
3071
|
-
problem (Problem):
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
1979
|
+
problem (Problem): The problem to which the desirability functions should be added.
|
|
1980
|
+
aspiration_levels (dict[str, float]): A dictionary with keys corresponding to objective function symbols
|
|
1981
|
+
and values to aspiration levels.
|
|
1982
|
+
reservation_levels (dict[str, float]): A dictionary with keys corresponding to objective function symbols
|
|
1983
|
+
and values to reservation levels.
|
|
1984
|
+
desirability_levels (dict[str, tuple[float, float]] | None, optional): A dictionary with keys corresponding to
|
|
1985
|
+
objective function symbols and values to desirability levels, where each value is a tuple of (d1, d2). If
|
|
1986
|
+
not given, the default values for d1 and d2 are used, which are 0.9 and 0.1 respectively. Defaults to None.
|
|
1987
|
+
desirability_func (str, optional): The type of desirability function to use. Currently, only "Harrington" or
|
|
1988
|
+
"MaoMao" is supported. Defaults to "Harrington".
|
|
3078
1989
|
|
|
3079
1990
|
Returns:
|
|
3080
|
-
|
|
3081
|
-
|
|
1991
|
+
Problem: A copy of the problem with the added desirability functions as scalarization functions.
|
|
1992
|
+
list[str]: A list of symbols of the added desirability functions.
|
|
3082
1993
|
"""
|
|
3083
|
-
if
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
if not objective_dict_has_all_symbols(problem, weight):
|
|
3090
|
-
raise ScalarizationError(f"weights[{idx}] missing some objectives")
|
|
3091
|
-
|
|
3092
|
-
alpha = Variable(
|
|
3093
|
-
name="alpha",
|
|
3094
|
-
symbol="_alpha",
|
|
3095
|
-
variable_type=VariableTypeEnum.real,
|
|
3096
|
-
lowerbound=-float("Inf"),
|
|
3097
|
-
upperbound=float("Inf"),
|
|
3098
|
-
initial_value=0.0,
|
|
3099
|
-
)
|
|
1994
|
+
if desirability_func == "Harrington":
|
|
1995
|
+
create_func = __create_HDF
|
|
1996
|
+
elif desirability_func == "MaoMao":
|
|
1997
|
+
create_func = __create_MDF
|
|
1998
|
+
else:
|
|
1999
|
+
raise ScalarizationError(f"Desirability function {desirability_func} is not supported.")
|
|
3100
2000
|
|
|
3101
|
-
|
|
3102
|
-
|
|
2001
|
+
if desirability_levels is None:
|
|
2002
|
+
desirability_levels = {obj.symbol: (0.9, 0.1) for obj in problem.objectives}
|
|
3103
2003
|
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
2004
|
+
# check that all objectives have aspiration and reservation levels defined
|
|
2005
|
+
for obj in problem.objectives:
|
|
2006
|
+
if obj.symbol not in aspiration_levels or obj.symbol not in reservation_levels:
|
|
2007
|
+
raise ScalarizationError(
|
|
2008
|
+
f"Objective {obj.symbol} does not have both aspiration and reservation levels defined."
|
|
2009
|
+
)
|
|
2010
|
+
maximize: dict[str, int] = {obj.symbol: -1 if obj.maximize else 1 for obj in problem.objectives}
|
|
2011
|
+
symbols = []
|
|
2012
|
+
problem_: Problem = problem.model_copy(deep=True)
|
|
2013
|
+
for obj in problem.objectives:
|
|
2014
|
+
d1, d2 = desirability_levels[obj.symbol]
|
|
2015
|
+
func = (
|
|
2016
|
+
"- ("
|
|
2017
|
+
+ create_func(
|
|
2018
|
+
obj.symbol + "_min",
|
|
2019
|
+
aspiration_levels[obj.symbol] * maximize[obj.symbol],
|
|
2020
|
+
reservation_levels[obj.symbol] * maximize[obj.symbol],
|
|
2021
|
+
d1,
|
|
2022
|
+
d2,
|
|
3120
2023
|
)
|
|
2024
|
+
+ ")"
|
|
2025
|
+
)
|
|
2026
|
+
symbols.append(f"{obj.symbol}_d")
|
|
2027
|
+
scalarization = ScalarizationFunction(
|
|
2028
|
+
name=f"Desirability function for {obj.symbol}",
|
|
2029
|
+
symbol=f"{obj.symbol}_d",
|
|
2030
|
+
func=func,
|
|
2031
|
+
is_linear=False,
|
|
2032
|
+
is_convex=False,
|
|
2033
|
+
is_twice_differentiable=obj.is_twice_differentiable,
|
|
2034
|
+
)
|
|
2035
|
+
problem_ = problem_.add_scalarization(scalarization)
|
|
3121
2036
|
|
|
3122
|
-
|
|
2037
|
+
return problem_, symbols
|
|
3123
2038
|
|
|
3124
|
-
func = f"_alpha + {epsilon}*({sum_part})"
|
|
3125
|
-
scalar = ScalarizationFunction(
|
|
3126
|
-
name="Scenario-based differentiable ASF",
|
|
3127
|
-
symbol=symbol,
|
|
3128
|
-
func=func,
|
|
3129
|
-
is_linear=problem.is_linear,
|
|
3130
|
-
is_convex=problem.is_convex,
|
|
3131
|
-
is_twice_differentiable=problem.is_twice_differentiable,
|
|
3132
|
-
)
|
|
3133
2039
|
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
2040
|
+
def add_iopis_funcs(
|
|
2041
|
+
problem: Problem,
|
|
2042
|
+
reference_point: dict[str, float],
|
|
2043
|
+
ideal: dict[str, float] | None = None,
|
|
2044
|
+
nadir: dict[str, float] | None = None,
|
|
2045
|
+
rho: float = 1e-6,
|
|
2046
|
+
delta: float = 1e-6,
|
|
2047
|
+
) -> tuple[Problem, list[str]]:
|
|
2048
|
+
symbols = ["iopis_guess", "iopis_stom"]
|
|
2049
|
+
_problem, _ = add_guess_sf_nondiff(
|
|
2050
|
+
problem=problem,
|
|
2051
|
+
symbol=symbols[0],
|
|
2052
|
+
reference_point=reference_point,
|
|
2053
|
+
ideal=ideal,
|
|
2054
|
+
nadir=nadir,
|
|
2055
|
+
rho=rho,
|
|
2056
|
+
)
|
|
3137
2057
|
|
|
3138
|
-
|
|
2058
|
+
_problem, _ = add_stom_sf_nondiff(
|
|
2059
|
+
problem=_problem,
|
|
2060
|
+
symbol=symbols[1],
|
|
2061
|
+
reference_point=reference_point,
|
|
2062
|
+
ideal=ideal,
|
|
2063
|
+
delta=delta,
|
|
2064
|
+
)
|
|
2065
|
+
return _problem, symbols
|