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.
Files changed (130) hide show
  1. desdeo/adm/ADMAfsar.py +551 -0
  2. desdeo/adm/ADMChen.py +414 -0
  3. desdeo/adm/BaseADM.py +119 -0
  4. desdeo/adm/__init__.py +11 -0
  5. desdeo/api/__init__.py +6 -6
  6. desdeo/api/app.py +38 -28
  7. desdeo/api/config.py +65 -44
  8. desdeo/api/config.toml +23 -12
  9. desdeo/api/db.py +10 -8
  10. desdeo/api/db_init.py +12 -6
  11. desdeo/api/models/__init__.py +220 -20
  12. desdeo/api/models/archive.py +16 -27
  13. desdeo/api/models/emo.py +128 -0
  14. desdeo/api/models/enautilus.py +69 -0
  15. desdeo/api/models/gdm/gdm_aggregate.py +139 -0
  16. desdeo/api/models/gdm/gdm_base.py +69 -0
  17. desdeo/api/models/gdm/gdm_score_bands.py +114 -0
  18. desdeo/api/models/gdm/gnimbus.py +138 -0
  19. desdeo/api/models/generic.py +104 -0
  20. desdeo/api/models/generic_states.py +401 -0
  21. desdeo/api/models/nimbus.py +158 -0
  22. desdeo/api/models/preference.py +44 -6
  23. desdeo/api/models/problem.py +274 -64
  24. desdeo/api/models/session.py +4 -1
  25. desdeo/api/models/state.py +419 -52
  26. desdeo/api/models/user.py +7 -6
  27. desdeo/api/models/utopia.py +25 -0
  28. desdeo/api/routers/_EMO.backup +309 -0
  29. desdeo/api/routers/_NIMBUS.py +6 -3
  30. desdeo/api/routers/emo.py +497 -0
  31. desdeo/api/routers/enautilus.py +237 -0
  32. desdeo/api/routers/gdm/gdm_aggregate.py +234 -0
  33. desdeo/api/routers/gdm/gdm_base.py +420 -0
  34. desdeo/api/routers/gdm/gdm_score_bands/gdm_score_bands_manager.py +398 -0
  35. desdeo/api/routers/gdm/gdm_score_bands/gdm_score_bands_routers.py +377 -0
  36. desdeo/api/routers/gdm/gnimbus/gnimbus_manager.py +698 -0
  37. desdeo/api/routers/gdm/gnimbus/gnimbus_routers.py +591 -0
  38. desdeo/api/routers/generic.py +233 -0
  39. desdeo/api/routers/nimbus.py +705 -0
  40. desdeo/api/routers/problem.py +201 -4
  41. desdeo/api/routers/reference_point_method.py +20 -44
  42. desdeo/api/routers/session.py +50 -26
  43. desdeo/api/routers/user_authentication.py +180 -26
  44. desdeo/api/routers/utils.py +187 -0
  45. desdeo/api/routers/utopia.py +230 -0
  46. desdeo/api/schema.py +10 -4
  47. desdeo/api/tests/conftest.py +94 -2
  48. desdeo/api/tests/test_enautilus.py +330 -0
  49. desdeo/api/tests/test_models.py +550 -72
  50. desdeo/api/tests/test_routes.py +902 -43
  51. desdeo/api/utils/_database.py +263 -0
  52. desdeo/api/utils/database.py +28 -266
  53. desdeo/api/utils/emo_database.py +40 -0
  54. desdeo/core.py +7 -0
  55. desdeo/emo/__init__.py +154 -24
  56. desdeo/emo/hooks/archivers.py +18 -2
  57. desdeo/emo/methods/EAs.py +128 -5
  58. desdeo/emo/methods/bases.py +9 -56
  59. desdeo/emo/methods/templates.py +111 -0
  60. desdeo/emo/operators/crossover.py +544 -42
  61. desdeo/emo/operators/evaluator.py +10 -14
  62. desdeo/emo/operators/generator.py +127 -24
  63. desdeo/emo/operators/mutation.py +212 -41
  64. desdeo/emo/operators/scalar_selection.py +202 -0
  65. desdeo/emo/operators/selection.py +956 -214
  66. desdeo/emo/operators/termination.py +124 -16
  67. desdeo/emo/options/__init__.py +108 -0
  68. desdeo/emo/options/algorithms.py +435 -0
  69. desdeo/emo/options/crossover.py +164 -0
  70. desdeo/emo/options/generator.py +131 -0
  71. desdeo/emo/options/mutation.py +260 -0
  72. desdeo/emo/options/repair.py +61 -0
  73. desdeo/emo/options/scalar_selection.py +66 -0
  74. desdeo/emo/options/selection.py +127 -0
  75. desdeo/emo/options/templates.py +383 -0
  76. desdeo/emo/options/termination.py +143 -0
  77. desdeo/gdm/__init__.py +22 -0
  78. desdeo/gdm/gdmtools.py +45 -0
  79. desdeo/gdm/score_bands.py +114 -0
  80. desdeo/gdm/voting_rules.py +50 -0
  81. desdeo/mcdm/__init__.py +23 -1
  82. desdeo/mcdm/enautilus.py +338 -0
  83. desdeo/mcdm/gnimbus.py +484 -0
  84. desdeo/mcdm/nautilus_navigator.py +7 -6
  85. desdeo/mcdm/reference_point_method.py +70 -0
  86. desdeo/problem/__init__.py +16 -11
  87. desdeo/problem/evaluator.py +4 -5
  88. desdeo/problem/external/__init__.py +18 -0
  89. desdeo/problem/external/core.py +356 -0
  90. desdeo/problem/external/pymoo_provider.py +266 -0
  91. desdeo/problem/external/runtime.py +44 -0
  92. desdeo/problem/gurobipy_evaluator.py +37 -12
  93. desdeo/problem/infix_parser.py +1 -16
  94. desdeo/problem/json_parser.py +7 -11
  95. desdeo/problem/pyomo_evaluator.py +25 -6
  96. desdeo/problem/schema.py +73 -55
  97. desdeo/problem/simulator_evaluator.py +65 -15
  98. desdeo/problem/testproblems/__init__.py +26 -11
  99. desdeo/problem/testproblems/benchmarks_server.py +120 -0
  100. desdeo/problem/testproblems/cake_problem.py +185 -0
  101. desdeo/problem/testproblems/dmitry_forest_problem_discrete.py +71 -0
  102. desdeo/problem/testproblems/forest_problem.py +77 -69
  103. desdeo/problem/testproblems/multi_valued_constraints.py +119 -0
  104. desdeo/problem/testproblems/{river_pollution_problem.py → river_pollution_problems.py} +28 -22
  105. desdeo/problem/testproblems/single_objective.py +289 -0
  106. desdeo/problem/testproblems/zdt_problem.py +4 -1
  107. desdeo/problem/utils.py +1 -1
  108. desdeo/tools/__init__.py +39 -21
  109. desdeo/tools/desc_gen.py +22 -0
  110. desdeo/tools/generics.py +22 -2
  111. desdeo/tools/group_scalarization.py +3090 -0
  112. desdeo/tools/indicators_binary.py +107 -1
  113. desdeo/tools/indicators_unary.py +3 -16
  114. desdeo/tools/message.py +33 -2
  115. desdeo/tools/non_dominated_sorting.py +4 -3
  116. desdeo/tools/patterns.py +9 -7
  117. desdeo/tools/pyomo_solver_interfaces.py +49 -36
  118. desdeo/tools/reference_vectors.py +118 -351
  119. desdeo/tools/scalarization.py +340 -1413
  120. desdeo/tools/score_bands.py +491 -328
  121. desdeo/tools/utils.py +117 -49
  122. desdeo/tools/visualizations.py +67 -0
  123. desdeo/utopia_stuff/utopia_problem.py +1 -1
  124. desdeo/utopia_stuff/utopia_problem_old.py +1 -1
  125. {desdeo-2.0.0.dist-info → desdeo-2.1.1.dist-info}/METADATA +47 -30
  126. desdeo-2.1.1.dist-info/RECORD +180 -0
  127. {desdeo-2.0.0.dist-info → desdeo-2.1.1.dist-info}/WHEEL +1 -1
  128. desdeo-2.0.0.dist-info/RECORD +0 -120
  129. /desdeo/api/utils/{logger.py → _logger.py} +0 -0
  130. {desdeo-2.0.0.dist-info → desdeo-2.1.1.dist-info/licenses}/LICENSE +0 -0
@@ -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]}{" * -1" if obj.maximize else ''}) "
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]}{" * -1" if obj.maximize else 1}) "
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 = get_corrected_reference_point(problem, reference_point)
314
+ corrected_rp = flip_maximized_objective_values(problem, reference_point)
538
315
  if reference_point_aug is not None:
539
- corrected_rp_aug = get_corrected_reference_point(problem, reference_point_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 = get_corrected_reference_point(problem, reference_point)
477
+ corrected_rp = flip_maximized_objective_values(problem, reference_point)
701
478
  if reference_point_aug is not None:
702
- corrected_rp_aug = get_corrected_reference_point(problem, reference_point_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 = get_corrected_reference_point(problem, current_objective_vector)
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 constriant for {_symbol}",
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 add_group_nimbus_sf( # noqa: PLR0913
1021
+ def add_stom_sf_diff(
1245
1022
  problem: Problem,
1246
1023
  symbol: str,
1247
- classifications_list: list[dict[str, tuple[str, float | None]]],
1248
- current_objective_vector: dict[str, float],
1024
+ reference_point: dict[str, float],
1249
1025
  ideal: dict[str, float] | None = None,
1250
- nadir: dict[str, float] | None = None,
1251
- delta: float = 0.000001,
1252
- rho: float = 0.000001,
1026
+ rho: float = 1e-6,
1027
+ delta: float = 1e-6,
1253
1028
  ) -> tuple[Problem, str]:
1254
- r"""Implements the multiple decision maker variant of the NIMBUS scalarization function.
1255
-
1256
- The scalarization function is defined as follows:
1257
-
1258
- \begin{align}
1259
- &\mbox{minimize} &&\max_{i\in I^<,j\in I^\leq,d} [w_{id}(f_{id}(\mathbf{x})-z^{ideal}_{id}),
1260
- w_{jd}(f_{jd}(\mathbf{x})-\hat{z}_{jd})] +
1261
- \rho \sum^k_{i=1} \sum^{n_d}_{d=1} w_{id}f_{id}(\mathbf{x}) \\
1262
- &\mbox{subject to} &&\mathbf{x} \in \mathbf{X},
1263
- \end{align}
1264
-
1265
- where $w_{id} = \frac{1}{z^{nad}_{id} - z^{uto}_{id}}$, and $w_{jd} = \frac{1}{z^{nad}_{jd} - z^{uto}_{jd}}$.
1266
-
1267
- The $I$-sets are related to the classifications given to each objective function value
1268
- in respect to the current objective vector (e.g., by a decision maker). They
1269
- are as follows:
1270
-
1271
- - $I^{<}$: values that should improve,
1272
- - $I^{\leq}$: values that should improve until a given aspiration level $\hat{z}_i$,
1273
- - $I^{=}$: values that are fine as they are,
1274
- - $I^{\geq}$: values that can be impaired until some reservation level $\varepsilon_i$, and
1275
- - $I^{\diamond}$: values that are allowed to change freely (not present explicitly in this scalarization function).
1029
+ r"""Adds the differentiable variant of the STOM scalarizing function.
1276
1030
 
1277
- The aspiration levels and the reservation levels are supplied for each classification, when relevant, in
1278
- the argument `classifications` as follows:
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
- ```python
1281
- classifications = {
1282
- "f_1": ("<", None),
1283
- "f_2": ("<=", 42.1),
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
- Here, we have assumed four objective functions. The key of the dict is a function's symbol, and the tuple
1290
- consists of a pair where the left element is the classification (self explanatory, '0' is for objective values
1291
- that may change freely), the right element is either `None` or an aspiration or a reservation level
1292
- depending on the classification.
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 to be scalarized.
1296
- symbol (str): the symbol given to the scalarization function, i.e., target of the optimization.
1297
- classifications_list (list[dict[str, tuple[str, float | None]]]): a list of dicts, where the key is a symbol
1298
- of an objective function, and the value is a tuple with a classification and an aspiration
1299
- or a reservation level, or `None`, depending on the classification. See above for an
1300
- explanation.
1301
- current_objective_vector (dict[str, float]): the current objective vector that corresponds to
1302
- a Pareto optimal solution. The classifications are assumed to been given in respect to
1303
- this vector.
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
- nadir (dict[str, float], optional): nadir point values. If not given, attempt will be made
1307
- to calculate nadir point from problem.
1308
- delta (float, optional): a small scalar used to define the utopian point. Defaults to 0.000001.
1309
- rho (float, optional): a small scalar used in the augmentation term. Defaults to 0.000001.
1310
-
1311
- Raises:
1312
- ScalarizationError: any of the given classifications do not define a classification
1313
- for all the objective functions or any of the given classifications do not allow at
1314
- least one objective function value to improve and one to worsen.
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 that classifications have been provided for all objective functions
1321
- for classifications in classifications_list:
1322
- if not objective_dict_has_all_symbols(problem, classifications):
1323
- msg = (
1324
- f"The given classifications {classifications} do not define "
1325
- "a classification for all the objective functions."
1326
- )
1327
- raise ScalarizationError(msg)
1328
-
1329
- # check that at least one objective function is allowed to be improved and one is
1330
- # allowed to worsen
1331
- if not any(classifications[obj.symbol][0] in ["<", "<="] for obj in problem.objectives) or not any(
1332
- classifications[obj.symbol][0] in [">=", "0"] for obj in problem.objectives
1333
- ):
1334
- msg = (
1335
- f"The given classifications {classifications} should allow at least one objective function value "
1336
- "to improve and one to worsen."
1337
- )
1338
- raise ScalarizationError(msg)
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
- # check if nadir point is specified
1351
- # if not specified, try to calculate corrected nadir point
1352
- if nadir is not None:
1353
- nadir_point = nadir
1354
- elif problem.get_nadir_point() is not None:
1355
- nadir_point = get_corrected_nadir(problem)
1356
- else:
1357
- msg = "Nadir point not defined!"
1358
- raise ScalarizationError(msg)
1359
-
1360
- corrected_current_point = get_corrected_reference_point(problem, current_objective_vector)
1361
-
1362
- # calculate the weights
1363
- weights = {
1364
- obj.symbol: 1 / (nadir_point[obj.symbol] - (ideal_point[obj.symbol] - delta)) for obj in problem.objectives
1365
- }
1366
-
1367
- # max term and constraints
1368
- max_args = []
1369
- constraints = []
1080
+ corrected_rp = flip_maximized_objective_values(problem, reference_point)
1370
1081
 
1371
- for i in range(len(classifications_list)):
1372
- classifications = classifications_list[i]
1373
- for obj in problem.objectives:
1374
- _symbol = obj.symbol
1375
- match classifications[_symbol]:
1376
- case ("<", _):
1377
- max_expr = f"{weights[_symbol]} * ({_symbol}_min - {ideal_point[_symbol]})"
1378
- max_args.append(max_expr)
1379
-
1380
- con_expr = f"{_symbol}_min - {corrected_current_point[_symbol]}"
1381
- constraints.append(
1382
- Constraint(
1383
- name=f"improvement constraint for {_symbol}",
1384
- symbol=f"{_symbol}_{i+1}_lt",
1385
- func=con_expr,
1386
- cons_type=ConstraintTypeEnum.LTE,
1387
- is_linear=problem.is_linear,
1388
- is_convex=problem.is_convex,
1389
- is_twice_differentiable=problem.is_twice_differentiable,
1390
- )
1391
- )
1392
- case ("<=", aspiration):
1393
- # if obj is to be maximized, then the current aspiration value needs to be multiplied by -1
1394
- max_expr = (
1395
- f"{weights[_symbol]} * ({_symbol}_min - {aspiration * -1 if obj.maximize else aspiration})"
1396
- )
1397
- max_args.append(max_expr)
1398
-
1399
- con_expr = f"{_symbol}_min - {corrected_current_point[_symbol]}"
1400
- constraints.append(
1401
- Constraint(
1402
- name=f"improvement until constraint for {_symbol}",
1403
- symbol=f"{_symbol}_{i+1}_lte",
1404
- func=con_expr,
1405
- cons_type=ConstraintTypeEnum.LTE,
1406
- is_linear=problem.is_linear,
1407
- is_convex=problem.is_convex,
1408
- is_twice_differentiable=problem.is_twice_differentiable,
1409
- )
1410
- )
1411
- case ("=", _):
1412
- con_expr = f"{_symbol}_min - {corrected_current_point[_symbol]}"
1413
- constraints.append(
1414
- Constraint(
1415
- name=f"Stay at least as good constraint for {_symbol}",
1416
- symbol=f"{_symbol}_{i+1}_eq",
1417
- func=con_expr,
1418
- cons_type=ConstraintTypeEnum.LTE,
1419
- is_linear=problem.is_linear,
1420
- is_convex=problem.is_convex,
1421
- is_twice_differentiable=problem.is_twice_differentiable,
1422
- )
1423
- )
1424
- case (">=", reservation):
1425
- # if obj is to be maximized, then the current reservation value needs to be multiplied by -1
1426
- con_expr = f"{_symbol}_min - {-1 * reservation if obj.maximize else reservation}"
1427
- constraints.append(
1428
- Constraint(
1429
- name=f"Worsen until constraint for {_symbol}",
1430
- symbol=f"{_symbol}_{i+1}_gte",
1431
- func=con_expr,
1432
- cons_type=ConstraintTypeEnum.LTE,
1433
- is_linear=problem.is_linear,
1434
- is_convex=problem.is_convex,
1435
- is_twice_differentiable=problem.is_twice_differentiable,
1436
- )
1437
- )
1438
- case ("0", _):
1439
- # not relevant for this scalarization
1440
- pass
1441
- case (c, _):
1442
- msg = (
1443
- f"Warning! The classification {c} was supplied, but it is not supported."
1444
- "Must be one of ['<', '<=', '0', '=', '>=']"
1445
- )
1446
- max_expr = f"Max({','.join(max_args)})"
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
- # form the augmentation term
1449
- aug_exprs = []
1450
- for _ in range(len(classifications_list)):
1451
- aug_expr = " + ".join([f"({weights[obj.symbol]} * {obj.symbol}_min)" for obj in problem.objectives])
1452
- aug_exprs.append(aug_expr)
1453
- aug_exprs = " + ".join(aug_exprs)
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
- func = f"{max_expr} + {rho} * ({aug_exprs})"
1100
+ target_expr = f"_alpha + {rho}*" + f"({aug_expr})"
1456
1101
  scalarization = ScalarizationFunction(
1457
- name="NIMBUS scalarization objective function for multiple decision makers",
1102
+ name="STOM scalarization objective function",
1458
1103
  symbol=symbol,
1459
- func=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
- _problem = problem.add_scalarization(scalarization)
1466
- return _problem.add_constraints(constraints), symbol
1467
-
1468
-
1469
- def add_group_nimbus_sf_diff( # noqa: PLR0913
1470
- problem: Problem,
1471
- symbol: str,
1472
- classifications_list: list[dict[str, tuple[str, float | None]]],
1473
- current_objective_vector: dict[str, float],
1474
- ideal: dict[str, float] | None = None,
1475
- nadir: dict[str, float] | None = None,
1476
- delta: float = 0.000001,
1477
- rho: float = 0.000001,
1478
- ) -> tuple[Problem, str]:
1479
- r"""Implements the differentiable variant of the multiple decision maker of the group NIMBUS scalarization function.
1480
-
1481
- The scalarization function is defined as follows:
1482
-
1483
- \begin{align}
1484
- \mbox{minimize} \quad
1485
- &\alpha +
1486
- \rho \sum^k_{i=1} \sum^{n_d}_{d=1} w_{id}f_{id}(\mathbf{x})\\
1487
- \mbox{subject to} \quad & w_{id}(f_{id}(\mathbf{x})-z^{ideal}_{id}) - \alpha \leq 0 \quad & \forall i \in I^<,\\
1488
- & w_{jd}(f_{jd}(\mathbf{x})-\hat{z}_{jd}) - \alpha \leq 0 \quad & \forall j \in I^\leq ,\\
1489
- & f_i(\mathbf{x}) - f_i(\mathbf{x_c}) \leq 0 \quad & \forall i \in I^< \cup I^\leq \cup I^= ,\\
1490
- & f_i(\mathbf{x}) - \epsilon_i \leq 0 \quad & \forall i \in I^\geq ,\\
1491
- & \mathbf{x} \in \mathbf{X},
1492
- \end{align}
1493
-
1494
- where $w_{id} = \frac{1}{z^{nad}_{id} - z^{uto}_{id}}$, and $w_{jd} = \frac{1}{z^{nad}_{jd} - z^{uto}_{jd}}$.
1495
-
1496
- The $I$-sets are related to the classifications given to each objective function value
1497
- in respect to the current objective vector (e.g., by a decision maker). They
1498
- are as follows:
1499
-
1500
- - $I^{<}$: values that should improve,
1501
- - $I^{\leq}$: values that should improve until a given aspiration level $\hat{z}_i$,
1502
- - $I^{=}$: values that are fine as they are,
1503
- - $I^{\geq}$: values that can be impaired until some reservation level $\varepsilon_i$, and
1504
- - $I^{\diamond}$: values that are allowed to change freely (not present explicitly in this scalarization function).
1505
-
1506
- The aspiration levels and the reservation levels are supplied for each classification, when relevant, in
1507
- the argument `classifications` as follows:
1508
-
1509
- ```python
1510
- classifications = {
1511
- "f_1": ("<", None),
1512
- "f_2": ("<=", 42.1),
1513
- "f_3": (">=", 22.2),
1514
- "f_4": ("0", None)
1515
- }
1516
- ```
1517
-
1518
- Here, we have assumed four objective functions. The key of the dict is a function's symbol, and the tuple
1519
- consists of a pair where the left element is the classification (self explanatory, '0' is for objective values
1520
- that may change freely), the right element is either `None` or an aspiration or a reservation level
1521
- depending on the classification.
1522
-
1523
- Args:
1524
- problem (Problem): the problem to be scalarized.
1525
- symbol (str): the symbol given to the scalarization function, i.e., target of the optimization.
1526
- classifications_list (list[dict[str, tuple[str, float | None]]]): a list of dicts, where the key is a symbol
1527
- of an objective function, and the value is a tuple with a classification and an aspiration
1528
- or a reservation level, or `None`, depending on the classification. See above for an
1529
- explanation.
1530
- current_objective_vector (dict[str, float]): the current objective vector that corresponds to
1531
- a Pareto optimal solution. The classifications are assumed to been given in respect to
1532
- this vector.
1533
- ideal (dict[str, float], optional): ideal point values. If not given, attempt will be made
1534
- to calculate ideal point from problem.
1535
- nadir (dict[str, float], optional): nadir point values. If not given, attempt will be made
1536
- to calculate nadir point from problem.
1537
- delta (float, optional): a small scalar used to define the utopian point. Defaults to 0.000001.
1538
- rho (float, optional): a small scalar used in the augmentation term. Defaults to 0.000001.
1539
-
1540
- Raises:
1541
- ScalarizationError: any of the given classifications do not define a classification
1542
- for all the objective functions or any of the given classifications do not allow at
1543
- least one objective function value to improve and one to worsen.
1544
-
1545
- Returns:
1546
- tuple[Problem, str]: a tuple with the copy of the problem with the added
1547
- scalarization and the symbol of the added scalarization.
1548
- """
1549
- # check that classifications have been provided for all objective functions
1550
- for classifications in classifications_list:
1551
- if not objective_dict_has_all_symbols(problem, classifications):
1552
- msg = (
1553
- f"The given classifications {classifications} do not define "
1554
- "a classification for all the objective functions."
1555
- )
1556
- raise ScalarizationError(msg)
1557
-
1558
- # check that at least one objective function is allowed to be improved and one is
1559
- # allowed to worsen
1560
- if not any(classifications[obj.symbol][0] in ["<", "<="] for obj in problem.objectives) or not any(
1561
- classifications[obj.symbol][0] in [">=", "0"] for obj in problem.objectives
1562
- ):
1563
- msg = (
1564
- f"The given classifications {classifications} should allow at least one objective function value "
1565
- "to improve and one to worsen."
1566
- )
1567
- raise ScalarizationError(msg)
1568
-
1569
- # check if ideal point is specified
1570
- # if not specified, try to calculate corrected ideal point
1571
- if ideal is not None:
1572
- ideal_point = ideal
1573
- elif problem.get_ideal_point() is not None:
1574
- ideal_point = get_corrected_ideal(problem)
1575
- else:
1576
- msg = "Ideal point not defined!"
1577
- raise ScalarizationError(msg)
1578
-
1579
- # check if nadir point is specified
1580
- # if not specified, try to calculate corrected nadir point
1581
- if nadir is not None:
1582
- nadir_point = nadir
1583
- elif problem.get_nadir_point() is not None:
1584
- nadir_point = get_corrected_nadir(problem)
1585
- else:
1586
- msg = "Nadir point not defined!"
1587
- raise ScalarizationError(msg)
1588
-
1589
- corrected_current_point = get_corrected_reference_point(problem, current_objective_vector)
1590
-
1591
- # define the auxiliary variable
1592
- alpha = Variable(
1593
- name="alpha",
1594
- symbol="_alpha",
1595
- variable_type=VariableTypeEnum.real,
1596
- lowerbound=-float("Inf"),
1597
- upperbound=float("Inf"),
1598
- initial_value=1.0,
1599
- )
1600
-
1601
- # calculate the weights
1602
- weights = {
1603
- obj.symbol: 1 / (nadir_point[obj.symbol] - (ideal_point[obj.symbol] - delta)) for obj in problem.objectives
1604
- }
1605
-
1606
- constraints = []
1607
-
1608
- for i in range(len(classifications_list)):
1609
- classifications = classifications_list[i]
1610
- for obj in problem.objectives:
1611
- _symbol = obj.symbol
1612
- match classifications[_symbol]:
1613
- case ("<", _):
1614
- max_expr = f"{weights[_symbol]} * ({_symbol}_min - {ideal_point[_symbol]}) - _alpha"
1615
- constraints.append(
1616
- Constraint(
1617
- name=f"Max term linearization for {_symbol}",
1618
- symbol=f"max_con_{_symbol}_{i+1}",
1619
- func=max_expr,
1620
- cons_type=ConstraintTypeEnum.LTE,
1621
- is_linear=problem.is_linear,
1622
- is_convex=problem.is_convex,
1623
- is_twice_differentiable=problem.is_twice_differentiable,
1624
- )
1625
- )
1626
- con_expr = f"{_symbol}_min - {corrected_current_point[_symbol]}"
1627
- constraints.append(
1628
- Constraint(
1629
- name=f"improvement constraint for {_symbol}",
1630
- symbol=f"{_symbol}_{i+1}_lt",
1631
- func=con_expr,
1632
- cons_type=ConstraintTypeEnum.LTE,
1633
- is_linear=problem.is_linear,
1634
- is_convex=problem.is_convex,
1635
- is_twice_differentiable=problem.is_twice_differentiable,
1636
- )
1637
- )
1638
- case ("<=", aspiration):
1639
- # if obj is to be maximized, then the current aspiration value needs to be multiplied by -1
1640
- max_expr = (
1641
- f"{weights[_symbol]} * ({_symbol}_min - {aspiration * -1 if obj.maximize else aspiration}) "
1642
- "- _alpha"
1643
- )
1644
- constraints.append(
1645
- Constraint(
1646
- name=f"Max term linearization for {_symbol}",
1647
- symbol=f"max_con_{_symbol}_{i+1}",
1648
- func=max_expr,
1649
- cons_type=ConstraintTypeEnum.LTE,
1650
- is_linear=problem.is_linear,
1651
- is_convex=problem.is_convex,
1652
- is_twice_differentiable=problem.is_twice_differentiable,
1653
- )
1654
- )
1655
- con_expr = f"{_symbol}_min - {corrected_current_point[_symbol]}"
1656
- constraints.append(
1657
- Constraint(
1658
- name=f"improvement until constraint for {_symbol}",
1659
- symbol=f"{_symbol}_{i+1}_lte",
1660
- func=con_expr,
1661
- cons_type=ConstraintTypeEnum.LTE,
1662
- is_linear=problem.is_linear,
1663
- is_convex=problem.is_convex,
1664
- is_twice_differentiable=problem.is_twice_differentiable,
1665
- )
1666
- )
1667
- case ("=", _):
1668
- con_expr = f"{_symbol}_min - {corrected_current_point[_symbol]}"
1669
- constraints.append(
1670
- Constraint(
1671
- name=f"Stay at least as good constraint for {_symbol}",
1672
- symbol=f"{_symbol}_{i+1}_eq",
1673
- func=con_expr,
1674
- cons_type=ConstraintTypeEnum.LTE,
1675
- is_linear=problem.is_linear,
1676
- is_convex=problem.is_convex,
1677
- is_twice_differentiable=problem.is_twice_differentiable,
1678
- )
1679
- )
1680
- case (">=", reservation):
1681
- # if obj is to be maximized, then the current reservation value needs to be multiplied by -1
1682
- con_expr = f"{_symbol}_min - {-1 * reservation if obj.maximize else reservation}"
1683
- constraints.append(
1684
- Constraint(
1685
- name=f"Worsen until constraint for {_symbol}",
1686
- symbol=f"{_symbol}_{i+1}_gte",
1687
- func=con_expr,
1688
- cons_type=ConstraintTypeEnum.LTE,
1689
- is_linear=problem.is_linear,
1690
- is_convex=problem.is_convex,
1691
- is_twice_differentiable=problem.is_twice_differentiable,
1692
- )
1693
- )
1694
- case ("0", _):
1695
- # not relevant for this scalarization
1696
- pass
1697
- case (c, _):
1698
- msg = (
1699
- f"Warning! The classification {c} was supplied, but it is not supported."
1700
- "Must be one of ['<', '<=', '0', '=', '>=']"
1701
- )
1702
-
1703
- # form the augmentation term
1704
- aug_exprs = []
1705
- for _ in range(len(classifications_list)):
1706
- aug_expr = " + ".join([f"({weights[obj.symbol]} * {obj.symbol}_min)" for obj in problem.objectives])
1707
- aug_exprs.append(aug_expr)
1708
- aug_exprs = " + ".join(aug_exprs)
1709
-
1710
- func = f"_alpha + {rho} * ({aug_exprs})"
1711
- scalarization_function = ScalarizationFunction(
1712
- name="Differentiable NIMBUS scalarization objective function for multiple decision makers",
1713
- symbol=symbol,
1714
- func=func,
1715
- is_linear=problem.is_linear,
1716
- is_convex=problem.is_convex,
1717
- is_twice_differentiable=problem.is_twice_differentiable,
1718
- )
1719
- _problem = problem.add_variables([alpha])
1720
- _problem = _problem.add_scalarization(scalarization_function)
1721
- return _problem.add_constraints(constraints), symbol
1722
-
1723
-
1724
- def add_stom_sf_diff(
1725
- problem: Problem,
1726
- symbol: str,
1727
- reference_point: dict[str, float],
1728
- ideal: dict[str, float] | None = None,
1729
- rho: float = 1e-6,
1730
- delta: float = 1e-6,
1731
- ) -> tuple[Problem, str]:
1732
- r"""Adds the differentiable variant of the STOM scalarizing function.
1733
-
1734
- \begin{align*}
1735
- \min \quad & \alpha + \rho \sum_{i=1}^k \frac{f_i(\mathbf{x})}{\bar{z}_i - z_i^{\star\star}} \\
1736
- \text{s.t.} \quad & \frac{f_i(\mathbf{x}) - z_i^{\star\star}}{\bar{z}_i
1737
- - z_i^{\star\star}} - \alpha \leq 0 \quad & \forall i = 1,\dots,k\\
1738
- & \mathbf{x} \in S,
1739
- \end{align*}
1740
-
1741
- where $f_i$ are objective functions, $z_i^{\star\star} = z_i^\star - \delta$ is
1742
- a component of the utopian point, $\bar{z}_i$ is a component of the reference point,
1743
- $\rho$ and $\delta$ are small scalar values, $S$ is the feasible solution
1744
- space of the original problem, and $\alpha$ is an auxiliary variable.
1745
-
1746
- References:
1747
- H. Nakayama, Y. Sawaragi, Satisficing trade-off method for
1748
- multiobjective programming, in: M. Grauer, A.P. Wierzbicki (Eds.),
1749
- Interactive Decision Analysis, Springer Verlag, Berlin, 1984, pp.
1750
- 113-122.
1751
-
1752
- Args:
1753
- problem (Problem): the problem the scalarization is added to.
1754
- symbol (str): the symbol given to the added scalarization.
1755
- reference_point (dict[str, float]): a dict with keys corresponding to objective
1756
- function symbols and values to reference point components, i.e.,
1757
- aspiration levels.
1758
- ideal (dict[str, float], optional): ideal point values. If not given, attempt will be made
1759
- to calculate ideal point from problem.
1760
- rho (float, optional): a small scalar value to scale the sum in the objective
1761
- function of the scalarization. Defaults to 1e-6.
1762
- delta (float, optional): a small scalar value to define the utopian point. Defaults to 1e-6.
1763
-
1764
- Returns:
1765
- tuple[Problem, str]: a tuple with the copy of the problem with the added
1766
- scalarization and the symbol of the added scalarization.
1767
- """
1768
- # check reference point
1769
- if not objective_dict_has_all_symbols(problem, reference_point):
1770
- msg = f"The give reference point {reference_point} is missing value for one or more objectives."
1771
- raise ScalarizationError(msg)
1772
-
1773
- # check if ideal point is specified
1774
- # if not specified, try to calculate corrected ideal point
1775
- if ideal is not None:
1776
- ideal_point = ideal
1777
- elif problem.get_ideal_point() is not None:
1778
- ideal_point = get_corrected_ideal(problem)
1779
- else:
1780
- msg = "Ideal point not defined!"
1781
- raise ScalarizationError(msg)
1782
-
1783
- corrected_rp = get_corrected_reference_point(problem, reference_point)
1784
-
1785
- # define the auxiliary variable
1786
- alpha = Variable(
1787
- name="alpha",
1788
- symbol="_alpha",
1789
- variable_type=VariableTypeEnum.real,
1790
- lowerbound=-float("Inf"),
1791
- upperbound=float("Inf"),
1792
- initial_value=1.0,
1793
- )
1794
-
1795
- # define the objective function of the scalarization
1796
- aug_expr = " + ".join(
1797
- [
1798
- f"{obj.symbol}_min / ({reference_point[obj.symbol]} - {ideal_point[obj.symbol] - delta})"
1799
- for obj in problem.objectives
1800
- ]
1801
- )
1802
-
1803
- target_expr = f"_alpha + {rho}*" + f"({aug_expr})"
1804
- scalarization = ScalarizationFunction(
1805
- name="STOM scalarization objective function",
1806
- symbol=symbol,
1807
- func=target_expr,
1808
- is_twice_differentiable=problem.is_twice_differentiable,
1809
- is_linear=problem.is_linear,
1810
- is_convex=problem.is_convex,
1811
- )
1812
-
1813
- constraints = []
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] - (ideal_point[obj.symbol] - delta)}) - _alpha"
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
- reference_points (list[dict[str, float]]): a list of dicts with keys corresponding to objective
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 points
2062
- for reference_point in reference_points:
2063
- if not objective_dict_has_all_symbols(problem, reference_point):
2064
- msg = f"The give reference point {reference_point} is missing value for one or more objectives."
2065
- raise ScalarizationError(msg)
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
- # define the auxiliary variable
2078
- alpha = Variable(
2079
- name="alpha",
2080
- symbol="_alpha",
2081
- variable_type=VariableTypeEnum.real,
2082
- lowerbound=-float("Inf"),
2083
- upperbound=float("Inf"),
2084
- initial_value=1.0,
2085
- )
2086
-
2087
- # calculate the weights
2088
- weights = []
2089
- for reference_point in reference_points:
2090
- corrected_rp = get_corrected_reference_point(problem, reference_point)
2091
- weights.append(
2092
- {
2093
- obj.symbol: 1 / (corrected_rp[obj.symbol] - (ideal_point[obj.symbol] - delta))
2094
- for obj in problem.objectives
2095
- }
2096
- )
2097
-
2098
- # form the max term
2099
- con_terms = []
2100
- for i in range(len(reference_points)):
2101
- rp = {}
2102
- for obj in problem.objectives:
2103
- rp[obj.symbol] = (
2104
- f"{weights[i][obj.symbol]} * ({obj.symbol}_min - {ideal_point[obj.symbol] - delta}) - _alpha"
2105
- )
2106
- con_terms.append(rp)
2107
-
2108
- # form the augmentation term
2109
- aug_exprs = []
2110
- for i in range(len(reference_points)):
2111
- aug_expr = " + ".join([f"({weights[i][obj.symbol]} * {obj.symbol}_min)" for obj in problem.objectives])
2112
- aug_exprs.append(aug_expr)
2113
- aug_exprs = " + ".join(aug_exprs)
1194
+ corrected_rp = flip_maximized_objective_values(problem, reference_point)
2114
1195
 
2115
- constraints = []
2116
- # loop to create a constraint for every objective of every reference point given
2117
- for i in range(len(reference_points)):
2118
- for obj in problem.objectives:
2119
- # since we are subtracting a constant value, the linearity, convexity,
2120
- # and differentiability of the objective function, and hence the
2121
- # constraint, should not change.
2122
- constraints.append(
2123
- Constraint(
2124
- name=f"Constraint for {obj.symbol}",
2125
- symbol=f"{obj.symbol}_con_{i+1}",
2126
- func=con_terms[i][obj.symbol],
2127
- cons_type=ConstraintTypeEnum.LTE,
2128
- is_linear=obj.is_linear,
2129
- is_convex=obj.is_convex,
2130
- is_twice_differentiable=obj.is_twice_differentiable,
2131
- )
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
- func = f"_alpha + {rho}*({aug_exprs})"
1213
+ target_expr = f"{Op.MAX}({max_expr}) + {rho}*" + f"({aug_expr})"
2135
1214
  scalarization = ScalarizationFunction(
2136
- name="Differentiable STOM scalarization objective function for multiple decision makers",
1215
+ name="STOM scalarization objective function",
2137
1216
  symbol=symbol,
2138
- func=func,
2139
- is_linear=problem.is_linear,
2140
- is_convex=problem.is_convex,
2141
- is_twice_differentiable=problem.is_twice_differentiable,
1217
+ func=target_expr,
1218
+ is_linear=False,
1219
+ is_convex=False,
1220
+ is_twice_differentiable=False,
2142
1221
  )
2143
- _problem = problem.add_variables([alpha])
2144
- _problem = _problem.add_scalarization(scalarization)
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})}{d_i} \\
2161
- \text{s.t.} \quad & \frac{f_i(\mathbf{x}) - z_i^{\star\star}}{\bar{z}_i
2162
- - z_i^{\star\star}} - \alpha \leq 0 \quad & \forall i \notin I^{\diamond},\\
2163
- & d_i =
2164
- \begin{cases}
2165
- z^\text{nad}_i - \bar{z}_i,\quad \forall i \notin I^\diamond,\\
2166
- z^\text{nad}_i - z^{\star\star}_i,\quad \forall i \in I^\diamond,\\
2167
- \end{cases}\\
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 $f_i$ are objective functions, $z_i^{\star\star} = z_i^\star - \delta$ is
2172
- a component of the utopian point, $\bar{z}_i$ is a component of the reference point,
2173
- $\rho$ and $\delta$ are small scalar values, $S$ is the feasible solution
2174
- space of the original problem, and $\alpha$ is an auxiliary variable. The index
2175
- set $I^\diamond$ represents objective vectors whose values are free to change. The indices
2176
- belonging to this set are interpreted as those objective vectors whose components in
2177
- the reference point is set to be the the respective nadir point component of the problem.
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 = get_corrected_reference_point(problem, reference_point)
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
- f"{reference_point[obj.symbol] if obj.symbol not in free_to_change else ideal_point[obj.symbol] - delta})" # noqa: E501
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^{\star\star}}{\bar{z}_i - z_i^{\star\star}}
1393
+ \frac{f_i(\mathbf{x}) - z_i^{nad}}{z_i^{nad} - \bar{z}_i}
2312
1394
  \right]
2313
- + \rho \sum_{j=1}^k \frac{f_j(\mathbf{x})}{d_j},
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/j}$ are objective functions, $z_{i/j}^{\star\star} =
2325
- z_{i/j}^\star - \delta$ is a component of the utopian point, $\bar{z}_{i/j}$
2326
- is a component of the reference point, $\rho$ and $\delta$ are small scalar
2327
- values, and $S$ is the feasible solution space of the original problem. The
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 = get_corrected_reference_point(problem, reference_point)
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 - {(ideal_point[obj.symbol] - delta)}) / "
2397
- f"({reference_point[obj.symbol]} - {(ideal_point[obj.symbol] - delta)})"
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
- f"{reference_point[obj.symbol] if obj.symbol not in free_to_change else ideal_point[obj.symbol] - delta})" # noqa: E501
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 = get_corrected_reference_point(problem, reference_point)
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 add_group_scenario_sf_nondiff(
2983
- problem: Problem,
2984
- symbol: str,
2985
- reference_points: list[dict[str, float]],
2986
- weights: list[dict[str, float]],
2987
- epsilon: float = 1e-6,
2988
- ) -> tuple[Problem, str]:
2989
- r"""Add the non-differentiable scenario based scalarization function.
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
- Add the following scalarization function:
2992
- \begin{align}
2993
- \min_{\mathbf{x}}\quad
2994
- &\max_{i,p}\bigl[w_{ip}\bigl(f_{ip}(\mathbf{x}) - \bar z_{ip}\bigr)\bigr]
2995
- \;+\;\varepsilon \sum_{i,p} w_{ip}\bigl(f_{ip}(\mathbf{x}) - \bar z_{ip}\bigr) \\[6pt]
2996
- \text{s.t.}\quad
2997
- &\mathbf{x} \in \mathcal{X}\,,
2998
- \end{align}
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
- Args:
3001
- problem (Problem): the problem the scalarization is added to.
3002
- symbol (str): the symbol given to the added scalarization.
3003
- reference_points (list[dict[str, float]]): a list of reference points as objective dicts.
3004
- function symbols and values to reference point components, i.e., aspiration levels.
3005
- weights (list[dict[str, float]]): the list of weights to be used in the scalarization function.
3006
- Must be positive.
3007
- epsilon: small augmentation multiplier ε
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
- Returns:
3010
- tuple[Problem, str]: A tuple containing a copy of the problem with the scalarization function added,
3011
- and the symbol of the added scalarization function.
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 len(reference_points) != len(weights):
3014
- raise ScalarizationError("reference_points and weights must have same length")
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
- for reference_point, weight in zip(reference_points, weights, strict=True):
3017
- if not objective_dict_has_all_symbols(problem, reference_point):
3018
- raise ScalarizationError(
3019
- f"The give reference point {reference_point} " f"is missing value for one or more objectives."
3020
- )
3021
- if not objective_dict_has_all_symbols(problem, weight):
3022
- raise ScalarizationError(
3023
- f"The given weight vector {weight} is missing " f"a value for one or more objectives."
3024
- )
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
- max_list: list[str] = []
3027
- sum_list: list[str] = []
3028
- for reference_point, weight in zip(reference_points, weights, strict=True):
3029
- corrected_ref_point = get_corrected_reference_point(problem, reference_point)
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
- max_part = f"{Op.MAX}({', '.join(max_list)})"
3037
- sum_part = " + ".join(sum_list)
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
- scalar = ScalarizationFunction(
3041
- name="Group non differentiable scalarization function for scenario based problems.",
3042
- symbol=symbol,
3043
- func=func,
3044
- is_linear=problem.is_linear,
3045
- is_convex=problem.is_convex,
3046
- is_twice_differentiable=problem.is_twice_differentiable,
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 problem.add_scalarization(scalar), symbol
1963
+ return func
3049
1964
 
3050
1965
 
3051
- def add_group_scenario_sf_diff(
1966
+ def add_desirability_funcs(
3052
1967
  problem: Problem,
3053
- symbol: str,
3054
- reference_points: list[dict[str, float]],
3055
- weights: list[dict[str, float]],
3056
- epsilon: float = 1e-6,
3057
- ) -> tuple[Problem, str]:
3058
- r"""Add the differentiable scenario-based scalarization.
3059
-
3060
- Adds the following scalarization function:
3061
- \begin{align}
3062
- \min_{x,\alpha}\quad
3063
- & \alpha \;+\; \varepsilon \sum_{i,p} w_{ip}\bigl(f_{ip}(x) - \bar z_{ip}\bigr) \\
3064
- \text{s.t.}\quad
3065
- & w_{ip}\bigl(f_{ip}(x) - \bar z_{ip}\bigr)\;-\;\alpha \;\le\;0
3066
- \quad\forall\,i,p,\\
3067
- & x \in \mathcal{X}\,,
3068
- \end{align}
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): the problem the scalarization is added to.
3072
- symbol (str): the symbol given to the added scalarization.
3073
- reference_points (list[dict[str, float]]): a list of reference points as objective dicts.
3074
- function symbols and values to reference point components, i.e., aspiration levels.
3075
- weights (list[dict[str, float]]): the list of weights to be used in the scalarization function.
3076
- Must be positive.
3077
- epsilon: small augmentation multiplier ε
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
- tuple[Problem, str]: A tuple containing a copy of the problem with the scalarization function added,
3081
- and the symbol of the added scalarization function.
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 len(reference_points) != len(weights):
3084
- raise ScalarizationError("reference_points and weights must have same length")
3085
-
3086
- for idx, (ref_point, weight) in enumerate(zip(reference_points, weights, strict=True)):
3087
- if not objective_dict_has_all_symbols(problem, ref_point):
3088
- raise ScalarizationError(f"reference_points[{idx}] missing some objectives")
3089
- if not objective_dict_has_all_symbols(problem, weight):
3090
- raise ScalarizationError(f"weights[{idx}] missing some objectives")
3091
-
3092
- alpha = Variable(
3093
- name="alpha",
3094
- symbol="_alpha",
3095
- variable_type=VariableTypeEnum.real,
3096
- lowerbound=-float("Inf"),
3097
- upperbound=float("Inf"),
3098
- initial_value=0.0,
3099
- )
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
- sum_list = []
3102
- constraints = []
2001
+ if desirability_levels is None:
2002
+ desirability_levels = {obj.symbol: (0.9, 0.1) for obj in problem.objectives}
3103
2003
 
3104
- for idx, (ref_point, weight) in enumerate(zip(reference_points, weights, strict=True)):
3105
- corrected_rp = get_corrected_reference_point(problem, ref_point)
3106
- for obj in problem.objectives:
3107
- expr = f"{weight[obj.symbol]}*({obj.symbol}_min - {corrected_rp[obj.symbol]})"
3108
- sum_list.append(expr)
3109
-
3110
- constraints.append(
3111
- Constraint(
3112
- name=f"ssf_con_{obj.symbol}",
3113
- symbol=f"{obj.symbol}_con_{idx}",
3114
- func=f"{expr} - {alpha.symbol}",
3115
- cons_type=ConstraintTypeEnum.LTE,
3116
- is_linear=obj.is_linear,
3117
- is_convex=obj.is_convex,
3118
- is_twice_differentiable=obj.is_twice_differentiable,
3119
- )
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
- sum_part = " + ".join(sum_list)
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
- problem_ = problem.add_variables([alpha])
3135
- problem_ = problem_.add_constraints(constraints)
3136
- problem_ = problem_.add_scalarization(scalar)
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
- return problem_, symbol
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