desdeo 2.0.0__py3-none-any.whl → 2.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. desdeo/adm/ADMAfsar.py +551 -0
  2. desdeo/adm/ADMChen.py +414 -0
  3. desdeo/adm/BaseADM.py +119 -0
  4. desdeo/adm/__init__.py +11 -0
  5. desdeo/api/__init__.py +6 -6
  6. desdeo/api/app.py +38 -28
  7. desdeo/api/config.py +65 -44
  8. desdeo/api/config.toml +23 -12
  9. desdeo/api/db.py +10 -8
  10. desdeo/api/db_init.py +12 -6
  11. desdeo/api/models/__init__.py +220 -20
  12. desdeo/api/models/archive.py +16 -27
  13. desdeo/api/models/emo.py +128 -0
  14. desdeo/api/models/enautilus.py +69 -0
  15. desdeo/api/models/gdm/gdm_aggregate.py +139 -0
  16. desdeo/api/models/gdm/gdm_base.py +69 -0
  17. desdeo/api/models/gdm/gdm_score_bands.py +114 -0
  18. desdeo/api/models/gdm/gnimbus.py +138 -0
  19. desdeo/api/models/generic.py +104 -0
  20. desdeo/api/models/generic_states.py +401 -0
  21. desdeo/api/models/nimbus.py +158 -0
  22. desdeo/api/models/preference.py +44 -6
  23. desdeo/api/models/problem.py +274 -64
  24. desdeo/api/models/session.py +4 -1
  25. desdeo/api/models/state.py +419 -52
  26. desdeo/api/models/user.py +7 -6
  27. desdeo/api/models/utopia.py +25 -0
  28. desdeo/api/routers/_EMO.backup +309 -0
  29. desdeo/api/routers/_NIMBUS.py +6 -3
  30. desdeo/api/routers/emo.py +497 -0
  31. desdeo/api/routers/enautilus.py +237 -0
  32. desdeo/api/routers/gdm/gdm_aggregate.py +234 -0
  33. desdeo/api/routers/gdm/gdm_base.py +420 -0
  34. desdeo/api/routers/gdm/gdm_score_bands/gdm_score_bands_manager.py +398 -0
  35. desdeo/api/routers/gdm/gdm_score_bands/gdm_score_bands_routers.py +377 -0
  36. desdeo/api/routers/gdm/gnimbus/gnimbus_manager.py +698 -0
  37. desdeo/api/routers/gdm/gnimbus/gnimbus_routers.py +591 -0
  38. desdeo/api/routers/generic.py +233 -0
  39. desdeo/api/routers/nimbus.py +705 -0
  40. desdeo/api/routers/problem.py +201 -4
  41. desdeo/api/routers/reference_point_method.py +20 -44
  42. desdeo/api/routers/session.py +50 -26
  43. desdeo/api/routers/user_authentication.py +180 -26
  44. desdeo/api/routers/utils.py +187 -0
  45. desdeo/api/routers/utopia.py +230 -0
  46. desdeo/api/schema.py +10 -4
  47. desdeo/api/tests/conftest.py +94 -2
  48. desdeo/api/tests/test_enautilus.py +330 -0
  49. desdeo/api/tests/test_models.py +550 -72
  50. desdeo/api/tests/test_routes.py +902 -43
  51. desdeo/api/utils/_database.py +263 -0
  52. desdeo/api/utils/database.py +28 -266
  53. desdeo/api/utils/emo_database.py +40 -0
  54. desdeo/core.py +7 -0
  55. desdeo/emo/__init__.py +154 -24
  56. desdeo/emo/hooks/archivers.py +18 -2
  57. desdeo/emo/methods/EAs.py +128 -5
  58. desdeo/emo/methods/bases.py +9 -56
  59. desdeo/emo/methods/templates.py +111 -0
  60. desdeo/emo/operators/crossover.py +544 -42
  61. desdeo/emo/operators/evaluator.py +10 -14
  62. desdeo/emo/operators/generator.py +127 -24
  63. desdeo/emo/operators/mutation.py +212 -41
  64. desdeo/emo/operators/scalar_selection.py +202 -0
  65. desdeo/emo/operators/selection.py +956 -214
  66. desdeo/emo/operators/termination.py +124 -16
  67. desdeo/emo/options/__init__.py +108 -0
  68. desdeo/emo/options/algorithms.py +435 -0
  69. desdeo/emo/options/crossover.py +164 -0
  70. desdeo/emo/options/generator.py +131 -0
  71. desdeo/emo/options/mutation.py +260 -0
  72. desdeo/emo/options/repair.py +61 -0
  73. desdeo/emo/options/scalar_selection.py +66 -0
  74. desdeo/emo/options/selection.py +127 -0
  75. desdeo/emo/options/templates.py +383 -0
  76. desdeo/emo/options/termination.py +143 -0
  77. desdeo/gdm/__init__.py +22 -0
  78. desdeo/gdm/gdmtools.py +45 -0
  79. desdeo/gdm/score_bands.py +114 -0
  80. desdeo/gdm/voting_rules.py +50 -0
  81. desdeo/mcdm/__init__.py +23 -1
  82. desdeo/mcdm/enautilus.py +338 -0
  83. desdeo/mcdm/gnimbus.py +484 -0
  84. desdeo/mcdm/nautilus_navigator.py +7 -6
  85. desdeo/mcdm/reference_point_method.py +70 -0
  86. desdeo/problem/__init__.py +5 -1
  87. desdeo/problem/external/__init__.py +18 -0
  88. desdeo/problem/external/core.py +356 -0
  89. desdeo/problem/external/pymoo_provider.py +266 -0
  90. desdeo/problem/external/runtime.py +44 -0
  91. desdeo/problem/infix_parser.py +2 -2
  92. desdeo/problem/pyomo_evaluator.py +25 -6
  93. desdeo/problem/schema.py +69 -48
  94. desdeo/problem/simulator_evaluator.py +65 -15
  95. desdeo/problem/testproblems/__init__.py +26 -11
  96. desdeo/problem/testproblems/benchmarks_server.py +120 -0
  97. desdeo/problem/testproblems/cake_problem.py +185 -0
  98. desdeo/problem/testproblems/dmitry_forest_problem_discrete.py +71 -0
  99. desdeo/problem/testproblems/forest_problem.py +77 -69
  100. desdeo/problem/testproblems/multi_valued_constraints.py +119 -0
  101. desdeo/problem/testproblems/{river_pollution_problem.py → river_pollution_problems.py} +28 -22
  102. desdeo/problem/testproblems/single_objective.py +289 -0
  103. desdeo/problem/testproblems/zdt_problem.py +4 -1
  104. desdeo/tools/__init__.py +39 -21
  105. desdeo/tools/desc_gen.py +22 -0
  106. desdeo/tools/generics.py +22 -2
  107. desdeo/tools/group_scalarization.py +3090 -0
  108. desdeo/tools/indicators_binary.py +107 -1
  109. desdeo/tools/indicators_unary.py +3 -16
  110. desdeo/tools/message.py +33 -2
  111. desdeo/tools/non_dominated_sorting.py +4 -3
  112. desdeo/tools/patterns.py +9 -7
  113. desdeo/tools/pyomo_solver_interfaces.py +48 -35
  114. desdeo/tools/reference_vectors.py +118 -351
  115. desdeo/tools/scalarization.py +340 -1413
  116. desdeo/tools/score_bands.py +491 -328
  117. desdeo/tools/utils.py +117 -49
  118. desdeo/tools/visualizations.py +67 -0
  119. desdeo/utopia_stuff/utopia_problem.py +1 -1
  120. desdeo/utopia_stuff/utopia_problem_old.py +1 -1
  121. {desdeo-2.0.0.dist-info → desdeo-2.1.0.dist-info}/METADATA +46 -28
  122. desdeo-2.1.0.dist-info/RECORD +180 -0
  123. {desdeo-2.0.0.dist-info → desdeo-2.1.0.dist-info}/WHEEL +1 -1
  124. desdeo-2.0.0.dist-info/RECORD +0 -120
  125. /desdeo/api/utils/{logger.py → _logger.py} +0 -0
  126. {desdeo-2.0.0.dist-info → desdeo-2.1.0.dist-info/licenses}/LICENSE +0 -0
desdeo/adm/ADMChen.py ADDED
@@ -0,0 +1,414 @@
1
+ from desdeo.adm import BaseADM
2
+ from desdeo.problem.schema import Problem
3
+ from desdeo.tools import payoff_table_method, non_dominated_sorting as nds
4
+ import numpy as np
5
+ from typing import Optional
6
+
7
+ """ADMChen.py
8
+
9
+ This module implements the Artificial Decision Maker (ADM) proposed by Chen et al.
10
+
11
+ References:
12
+ Chen, L., Miettinen, K., Xin, B., & Ojalehto, V. (2023).
13
+ Comparing reference point based interactive multiobjective
14
+ optimization methods without a human decision maker.
15
+ European Journal of Operational Research, 307(1), 327-345.
16
+
17
+ IMPORTANT: This module is WIP. There are multiple things not clear in the article that need further clarification.
18
+ """
19
+
20
+
21
+ class ADMChen(BaseADM):
22
+ """
23
+ Artificial Decision Maker implementation based on Chen et al. (2023).
24
+
25
+ This ADM simulates human decision-making behavior in interactive multiobjective
26
+ optimization by operating in two phases: learning and decision-making. During the
27
+ learning phase, it explores the Pareto front by identifying neighboring solutions
28
+ with maximum normalized Euclidean distance. In the decision phase, it selects
29
+ solutions based on a utility function that minimizes disutility.
30
+
31
+ Attributes:
32
+ true_ideal (np.ndarray): True ideal point computed from the problem.
33
+ true_nadir (np.ndarray): True nadir point computed from the problem.
34
+ num_objectives (int): Number of objectives in the problem.
35
+ num_variables (int): Number of variables in the problem.
36
+ preference (np.ndarray): Current reference point preference.
37
+ weights (np.ndarray): Objective weights (equal weights by default).
38
+ UF_max (float): Maximum utility function value on the Pareto front.
39
+ UF_opt (float): Optimal (minimum) utility function value on the Pareto front.
40
+ extreme_solutions (np.ndarray): Extreme solutions from the Pareto front.
41
+
42
+ Args:
43
+ problem (Problem): The multiobjective optimization problem.
44
+ it_learning_phase (int): Number of iterations for the learning phase.
45
+ it_decision_phase (int): Number of iterations for the decision phase.
46
+ pareto_front (np.ndarray): Known Pareto front solutions for initialization.
47
+ initial_reference_point (Optional[np.ndarray]): Initial reference point.
48
+ If None, a random point between ideal and nadir is generated.
49
+
50
+ Raises:
51
+ ValueError: If the initial reference point is not between ideal and nadir points.
52
+
53
+ Example:
54
+ >>> problem = Problem(...) # Define your problem
55
+ >>> pareto_front = np.array([[1, 2], [2, 1], [1.5, 1.5]])
56
+ >>> adm = ADMChen(problem, it_learning_phase=5, it_decision_phase=3,
57
+ ... pareto_front=pareto_front)
58
+ >>> preference = adm.get_next_preference(current_front)
59
+ """
60
+
61
+ def __init__(
62
+ self,
63
+ problem: Problem,
64
+ it_learning_phase: int,
65
+ it_decision_phase: int,
66
+ pareto_front: np.ndarray,
67
+ initial_reference_point: Optional[np.ndarray] = None,
68
+ ):
69
+ # Initialize problem with true ideal and nadir points
70
+ self.true_ideal, self.true_nadir = payoff_table_method(problem)
71
+ problem = problem.update_ideal_and_nadir(
72
+ new_ideal=self.true_ideal, new_nadir=self.true_nadir
73
+ )
74
+ super().__init__(problem, it_learning_phase, it_decision_phase)
75
+
76
+ # Store problem dimensions
77
+ self.num_objectives = len(problem.objectives)
78
+ self.num_variables = len(problem.variables)
79
+
80
+ # Generate initial preference
81
+ self.preference = self.generate_initial_preference(initial_reference_point)
82
+ self.iteration_counter += 1
83
+
84
+ # Initialize equal weights for all objectives
85
+ # NOTE: In the original article, weights were set manually
86
+ self.weights = np.ones(self.num_objectives) / self.num_objectives
87
+
88
+ # Compute utility function bounds on the Pareto front
89
+ self.UF_max = np.max([
90
+ self.utility_function(sol, self.true_ideal, self.true_nadir, self.weights)
91
+ for sol in pareto_front
92
+ ])
93
+ self.UF_opt = np.min([
94
+ self.utility_function(sol, self.true_ideal, self.true_nadir, self.weights)
95
+ for sol in pareto_front
96
+ ])
97
+
98
+ # Store extreme solutions from the
99
+ self.extreme_solutions = self.get_extreme_solutions(pareto_front)
100
+
101
+ def generate_initial_preference(self, initial_reference_point: Optional[np.ndarray] = None) -> np.ndarray:
102
+ """
103
+ Generate the initial reference point for the ADM.
104
+
105
+ If an initial reference point is provided, it validates that the point lies
106
+ between the ideal and nadir points. Otherwise, generates a random point
107
+ within the feasible objective space.
108
+
109
+ Args:
110
+ initial_reference_point (Optional[np.ndarray]): User-specified initial
111
+ reference point. Must be between ideal and nadir points.
112
+
113
+ Returns:
114
+ np.ndarray: Valid initial reference point.
115
+
116
+ Raises:
117
+ ValueError: If the provided reference point is outside the valid range.
118
+ """
119
+ if initial_reference_point is not None:
120
+ if not (self.true_ideal <= initial_reference_point <= self.true_nadir).all():
121
+ raise ValueError(
122
+ "Initial reference point must be between the ideal and nadir points."
123
+ )
124
+ return initial_reference_point
125
+ else:
126
+ # Generate random reference point between ideal and nadir
127
+ return np.array([
128
+ np.random.uniform(min_val, max_val)
129
+ for min_val, max_val in zip(
130
+ self.problem.get_ideal_point().values(),
131
+ self.problem.get_nadir_point().values(),
132
+ )
133
+ ])
134
+
135
+ def get_next_preference(self, front: np.ndarray) -> np.ndarray:
136
+ """
137
+ Get the next preference (reference point) based on the current iteration phase.
138
+
139
+ This method determines whether the ADM is in the learning or decision phase
140
+ and calls the appropriate preference generation method.
141
+
142
+ Args:
143
+ front (np.ndarray): Current Pareto front approximation with shape
144
+ (n_solutions, n_objectives).
145
+
146
+ Returns:
147
+ np.ndarray: Next reference point for the interactive method.
148
+ """
149
+ if self.iteration_counter < self.it_learning_phase:
150
+ self.preference = self.generate_preference_learning(front)
151
+ else:
152
+ self.preference = self.generate_preference_decision(front)
153
+ self.iteration_counter += 1
154
+ return self.preference
155
+
156
+ def get_extreme_solutions(self, front: np.ndarray) -> np.ndarray:
157
+ """
158
+ Extract extreme solutions from the Pareto front.
159
+
160
+ An extreme solution is defined as the objective vector that minimizes
161
+ one of the objective functions on the Pareto front. These solutions
162
+ represent the boundaries of the achievable objective space.
163
+
164
+ Args:
165
+ front (np.ndarray): Pareto front with shape (n_solutions, n_objectives).
166
+
167
+ Returns:
168
+ np.ndarray: Array of extreme solutions with shape (n_objectives, n_objectives).
169
+ Each row represents an extreme solution for one objective.
170
+
171
+ Example:
172
+ For a 2-objective problem with front = [[1, 3], [2, 2], [3, 1]]:
173
+ - Extreme for obj 1: [1, 3] (min value 1 in first objective)
174
+ - Extreme for obj 2: [3, 1] (min value 1 in second objective)
175
+ """
176
+ extreme_solutions = []
177
+ for i in range(front.shape[1]): # For each objective
178
+ idx_min_i = np.argmin(front[:, i]) # Find solution with minimum value
179
+ extreme_solutions.append(front[idx_min_i])
180
+ return np.array(extreme_solutions)
181
+
182
+ @staticmethod
183
+ def normalized_euclidean_distance(
184
+ za: np.ndarray,
185
+ zb: np.ndarray,
186
+ znad: np.ndarray,
187
+ zstar: np.ndarray,
188
+ eps: Optional[float] = None
189
+ ) -> float:
190
+ """
191
+ Compute normalized Euclidean distance between two solutions.
192
+
193
+ The normalization is performed using the range between the utopian point
194
+ (ideal - eps) and the nadir point. This ensures that the distance metric
195
+ is scale-independent across different objectives.
196
+
197
+ Args:
198
+ za (np.ndarray): First solution vector of shape (n_objectives,).
199
+ zb (np.ndarray): Second solution vector of shape (n_objectives,).
200
+ znad (np.ndarray): Nadir point (worst values) of shape (n_objectives,).
201
+ zstar (np.ndarray): Ideal point (best values) of shape (n_objectives,).
202
+ eps (Optional[float]): Small positive value for utopian shift.
203
+ Defaults to 1e-6 if None.
204
+
205
+ Returns:
206
+ float: Normalized Euclidean distance between za and zb.
207
+
208
+ Note:
209
+ The utopian point is computed as zstar - eps to ensure strict
210
+ improvement over the ideal point. Division by zero is avoided
211
+ by replacing zero denominators with 1e-12.
212
+ """
213
+ za, zb, znad, zstar = map(np.asarray, (za, zb, znad, zstar))
214
+ if eps is None:
215
+ eps = 1e-6
216
+ if np.isscalar(eps):
217
+ eps = np.full_like(zstar, eps, dtype=float)
218
+
219
+ # Compute utopian point (strictly better than ideal)
220
+ z_utopian = zstar - eps
221
+
222
+ # Compute normalization denominator
223
+ denom = znad - z_utopian
224
+
225
+ # Avoid division by zero when nadir ≈ utopian
226
+ denom = np.where(denom <= 0, 1e-12, denom)
227
+
228
+ # Compute normalized difference and Euclidean distance
229
+ diff = (za - zb) / denom
230
+ return np.sqrt(np.sum(diff**2))
231
+
232
+ @staticmethod
233
+ def are_neighbors(za: np.ndarray, zb: np.ndarray, solutions: np.ndarray) -> bool:
234
+ """
235
+ Check if two solutions are neighbors in the context of a solution set.
236
+
237
+ Two solutions za and zb are considered neighbors if their componentwise
238
+ minimum is not dominated by any other solution in the set.
239
+
240
+ Args:
241
+ za (np.ndarray): First solution vector of shape (n_objectives,).
242
+ zb (np.ndarray): Second solution vector of shape (n_objectives,).
243
+ solutions (np.ndarray): Complete solution set with shape
244
+ (n_solutions, n_objectives).
245
+
246
+ Returns:
247
+ bool: True if za and zb are neighbors, False otherwise.
248
+
249
+ Note:
250
+ The componentwise minimum z_ab = min(za, zb) represents a point
251
+ that is at least as good as both za and zb in all objectives.
252
+ If any other solution dominates z_ab, then za and zb are not
253
+ considered neighbors.
254
+ """
255
+ za = np.asarray(za)
256
+ zb = np.asarray(zb)
257
+ z_ab = np.minimum(za, zb) # Componentwise minimum
258
+
259
+ for i in range(len(solutions)):
260
+ zc = solutions[i]
261
+ # Skip comparing to za and zb themselves
262
+ if np.array_equal(zc, za) or np.array_equal(zc, zb):
263
+ continue
264
+ if nds.dominates(z_ab, zc):
265
+ return False
266
+ return True
267
+
268
+ def generate_preference_learning(self, front: np.ndarray) -> np.ndarray:
269
+ """
270
+ Generate preference during the learning phase through systematic exploration.
271
+
272
+ The learning phase explores the Pareto front by identifying neighboring
273
+ solution pairs with the maximum normalized Euclidean distance.
274
+
275
+ Args:
276
+ front (np.ndarray): Current Pareto front with shape (n_solutions, n_objectives).
277
+
278
+ Returns:
279
+ np.ndarray: New reference point derived from the most distant neighbors.
280
+
281
+ Note:
282
+ The reference point is set as the componentwise minimum of the
283
+ most distant neighboring pair, which represents an aspirational
284
+ point that is better than both neighbors in all objectives.
285
+
286
+ TODO:
287
+ Validate that the same region has not been selected before to
288
+ avoid redundant exploration.
289
+ """
290
+ # Extend front with extreme solutions for comprehensive exploration
291
+ extended_set = np.append(front, self.extreme_solutions, axis=0)
292
+
293
+ neighbors_1 = []
294
+ neighbors_2 = []
295
+ euclidean_distances = []
296
+
297
+ # Find all neighboring pairs and compute their distances
298
+ for i in range(extended_set.shape[0] - 1):
299
+ z1 = extended_set[i, :]
300
+ for j in range(i + 1, extended_set.shape[0]):
301
+ z2 = extended_set[j, :]
302
+ if self.are_neighbors(z1, z2, extended_set):
303
+ neighbors_1.append(z1)
304
+ neighbors_2.append(z2)
305
+ euclidean_distances.append(
306
+ self.normalized_euclidean_distance(
307
+ z1, z2, self.true_nadir, self.true_ideal
308
+ )
309
+ )
310
+
311
+ # Select the pair with maximum distance for exploration
312
+ max_distance_idx = np.argmax(euclidean_distances)
313
+
314
+ # Generate reference point as componentwise minimum of the distant pair
315
+ new_ref_point = np.minimum(
316
+ neighbors_1[max_distance_idx],
317
+ neighbors_2[max_distance_idx]
318
+ )
319
+
320
+ return new_ref_point
321
+
322
+ def utility_function(
323
+ self,
324
+ z: np.ndarray,
325
+ zstar: np.ndarray,
326
+ znad: np.ndarray,
327
+ weight: np.ndarray,
328
+ type: str = 'deterministic',
329
+ eps: Optional[float] = None
330
+ ) -> float:
331
+ """
332
+ Compute the utility function value for a given solution.
333
+
334
+ The utility function measures the maximum weighted normalized distance
335
+ from the utopian point. Lower values indicate better solutions.
336
+
337
+ Args:
338
+ z (np.ndarray): Solution vector of shape (n_objectives,).
339
+ zstar (np.ndarray): Ideal point of shape (n_objectives,).
340
+ znad (np.ndarray): Nadir point of shape (n_objectives,).
341
+ weight (np.ndarray): Objective weights of shape (n_objectives,).
342
+ type (str): Type of utility function. Options: 'deterministic', 'random'.
343
+ Defaults to 'deterministic'.
344
+ eps (Optional[float]): Small positive value for utopian shift.
345
+ Defaults to 1e-6.
346
+
347
+ Returns:
348
+ float: Utility function value (lower is better).
349
+
350
+ Note:
351
+ When type='random', Gaussian noise is added with standard deviation
352
+ that decreases over iterations to simulate learning behavior.
353
+ The noise magnitude is based on the utility function range.
354
+ """
355
+ z, znad, zstar, weight = map(np.asarray, (z, znad, zstar, weight))
356
+ if eps is None:
357
+ eps = 1e-6
358
+ if np.isscalar(eps):
359
+ eps = np.full_like(zstar, eps, dtype=float)
360
+
361
+ # Compute utopian point (strictly better than ideal)
362
+ z_utopian = zstar - eps
363
+
364
+ # Compute normalization denominator
365
+ denom = znad - z_utopian
366
+
367
+ # Avoid division by zero
368
+ denom = np.where(denom <= 0, 1e-12, denom)
369
+
370
+ # Compute weighted normalized distances
371
+ diff = weight * ((z - z_utopian) / denom)
372
+ U_minus = np.max(diff) # Chebyshev scalarization
373
+
374
+ # Add random component if requested
375
+ if type == 'random':
376
+ # Noise decreases over iterations to simulate learning
377
+ sigma = (self.UF_max - self.UF_opt) * 0.2 / (2 ** (self.it_decision_phase - 1))
378
+ noise = np.random.uniform(low=0, high=sigma)
379
+ U_minus = noise + U_minus
380
+
381
+ return U_minus
382
+
383
+ def generate_preference_decision(self, front: np.ndarray) -> np.ndarray:
384
+ """
385
+ Generate preference during the decision phase by selecting the best solution.
386
+
387
+ In the decision phase, the ADM acts more decisively by evaluating all
388
+ solutions in the current front using the utility function and selecting
389
+ the one with minimum disutility (best utility value). This represents
390
+ the final decision-making behavior after the learning phase.
391
+
392
+ Args:
393
+ front (np.ndarray): Current Pareto front with shape (n_solutions, n_objectives).
394
+
395
+ Returns:
396
+ np.ndarray: The solution with minimum disutility as the preferred reference point.
397
+
398
+ Note:
399
+ The returned solution represents the ADM's final preference and
400
+ should be close to the decision maker's most preferred solution.
401
+ """
402
+ min_disutility = np.inf
403
+ preferred_solution = None
404
+
405
+ # Evaluate all solutions and find the one with minimum disutility
406
+ for solution in front:
407
+ disutility = self.utility_function(
408
+ solution, self.true_ideal, self.true_nadir, self.weights
409
+ )
410
+ if disutility < min_disutility:
411
+ min_disutility = disutility
412
+ preferred_solution = solution
413
+
414
+ return preferred_solution
desdeo/adm/BaseADM.py ADDED
@@ -0,0 +1,119 @@
1
+ from desdeo.problem.schema import Problem
2
+ from abc import ABC, abstractmethod
3
+
4
+ """Base class for Artificial Decision Makers (ADMs).
5
+ This class provides the basic structure and methods for implementing
6
+ an ADM.
7
+
8
+ Attributes:
9
+ problem (Problem): The optimization problem to solve.
10
+ it_learning_phase (int): Number of iterations for the learning phase.
11
+ it_decision_phase (int): Number of iterations for the decision phase.
12
+ iteration_counter (int): Counter for the current iteration.
13
+ max_iterations (int): Total number of iterations (learning + decision).
14
+
15
+ Methods:
16
+ has_next():
17
+ Check if there are more iterations left to run.
18
+
19
+ generate_initial_reference_point():
20
+ Abstract method to generate the initial preference information for the ADM.
21
+
22
+ get_next_preference():
23
+ Abstract method to get the next preference value according to the current phase.
24
+
25
+ generate_preference_learning():
26
+ Abstract method to generate preference information during the learning phase.
27
+
28
+ generate_preference_decision():
29
+ Abstract method to generate preference information during the decision phase.
30
+ """
31
+
32
+
33
+ class BaseADM(ABC):
34
+ """
35
+ Abstract base class for Artificial Decision Makers (ADMs).
36
+
37
+ This class provides the basic structure and required methods for implementing
38
+ an ADM. Subclasses must implement the abstract methods to define specific ADM behavior.
39
+
40
+ Attributes:
41
+ problem (Problem): The optimization problem to solve.
42
+ it_learning_phase (int): Number of iterations for the learning phase.
43
+ it_decision_phase (int): Number of iterations for the decision phase.
44
+ iteration_counter (int): Counter for the current iteration.
45
+
46
+ Properties:
47
+ max_iterations (int): Total number of iterations (learning + decision).
48
+ """
49
+
50
+ def __init__(
51
+ self,
52
+ problem: Problem,
53
+ it_learning_phase: int,
54
+ it_decision_phase: int,
55
+ ):
56
+ """
57
+ Initialize the ADM with the given problem and phase lengths.
58
+
59
+ Args:
60
+ problem (Problem): The optimization problem to solve.
61
+ it_learning_phase (int): Number of iterations for the learning phase.
62
+ it_decision_phase (int): Number of iterations for the decision phase.
63
+ """
64
+ self.problem = problem
65
+ self.it_learning_phase = it_learning_phase
66
+ self.it_decision_phase = it_decision_phase
67
+ self.iteration_counter = 0
68
+
69
+ @property
70
+ def max_iterations(self):
71
+ """
72
+ int: Total number of iterations (learning + decision).
73
+ """
74
+ return self.it_learning_phase + self.it_decision_phase
75
+
76
+ def has_next(self):
77
+ """
78
+ Check if there are more iterations left to run.
79
+
80
+ Returns:
81
+ bool: True if more iterations remain, False otherwise.
82
+ """
83
+ return self.iteration_counter < self.max_iterations
84
+
85
+ @abstractmethod
86
+ def generate_initial_preference(self):
87
+ """
88
+ Generate the initial preference information for the ADM.
89
+
90
+ This method must be implemented by subclasses.
91
+ """
92
+ pass
93
+
94
+ @abstractmethod
95
+ def get_next_preference(self):
96
+ """
97
+ Get the next preference value according to the current phase.
98
+
99
+ This method must be implemented by subclasses.
100
+ """
101
+ pass
102
+
103
+ @abstractmethod
104
+ def generate_preference_learning(self):
105
+ """
106
+ Generate preference information during the learning phase.
107
+
108
+ This method must be implemented by subclasses.
109
+ """
110
+ pass
111
+
112
+ @abstractmethod
113
+ def generate_preference_decision(self):
114
+ """
115
+ Generate preference information during the decision phase.
116
+
117
+ This method must be implemented by subclasses.
118
+ """
119
+ pass
desdeo/adm/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """Imports available from desdeo adm."""
2
+
3
+ __all__ = [
4
+ "BaseADM",
5
+ "ADMAfsar",
6
+ "ADMChen",
7
+ ]
8
+
9
+ from .BaseADM import BaseADM
10
+ from .ADMChen import ADMChen
11
+ from .ADMAfsar import ADMAfsar
desdeo/api/__init__.py CHANGED
@@ -1,15 +1,15 @@
1
1
  """Exports for desdeo.api."""
2
2
 
3
3
  __all__ = [
4
- "AuthDebugConfig",
5
- "DatabaseDebugConfig",
6
- "ServerDebugConfig",
4
+ "AuthConfig",
5
+ "DatabaseConfig",
6
+ "ServerConfig",
7
7
  "SettingsConfig",
8
8
  ]
9
9
 
10
10
  from .config import (
11
- AuthDebugConfig,
12
- DatabaseDebugConfig,
13
- ServerDebugConfig,
11
+ AuthConfig,
12
+ DatabaseConfig,
13
+ ServerConfig,
14
14
  SettingsConfig,
15
15
  )
desdeo/api/app.py CHANGED
@@ -3,38 +3,48 @@
3
3
  from fastapi import FastAPI
4
4
  from fastapi.middleware.cors import CORSMiddleware
5
5
 
6
- from desdeo.api.config import AuthDebugConfig, SettingsConfig
6
+ from desdeo.api.config import AuthConfig
7
7
  from desdeo.api.routers import (
8
+ emo,
9
+ enautilus,
10
+ generic,
11
+ nimbus,
8
12
  problem,
9
13
  reference_point_method,
10
14
  session,
11
15
  user_authentication,
16
+ utopia,
17
+ )
18
+ from desdeo.api.routers.gdm import gdm_aggregate, gdm_base
19
+ from desdeo.api.routers.gdm.gdm_score_bands import gdm_score_bands_routers
20
+ from desdeo.api.routers.gdm.gnimbus import gnimbus_routers
21
+
22
+ app = FastAPI(
23
+ title="DESDEO (fast)API",
24
+ version="0.1.0",
25
+ description="A rest API for the DESDEO framework.",
12
26
  )
13
27
 
14
- if SettingsConfig.debug:
15
- # debug and development stuff
16
-
17
- app = FastAPI(
18
- title="DESDEO (fast)API",
19
- version="0.1.0",
20
- description="A rest API for the DESDEO framework.",
21
- )
22
-
23
- app.include_router(user_authentication.router)
24
- app.include_router(problem.router)
25
- app.include_router(session.router)
26
- app.include_router(reference_point_method.router)
27
-
28
- origins = AuthDebugConfig.cors_origins
29
-
30
- app.add_middleware(
31
- CORSMiddleware,
32
- allow_origins=origins,
33
- allow_credentials=True,
34
- allow_methods=["*"],
35
- allow_headers=["*"],
36
- )
37
-
38
- else:
39
- # deployment stuff
40
- pass
28
+ app.include_router(user_authentication.router)
29
+ app.include_router(problem.router)
30
+ app.include_router(session.router)
31
+ app.include_router(reference_point_method.router)
32
+ app.include_router(nimbus.router)
33
+ # app.include_router(emo.router) # TODO: what is going on? cannot serialize pl.dataframe
34
+ app.include_router(generic.router)
35
+ app.include_router(utopia.router)
36
+ app.include_router(gdm_base.router)
37
+ app.include_router(gdm_aggregate.router)
38
+ app.include_router(gnimbus_routers.router)
39
+ app.include_router(enautilus.router)
40
+ app.include_router(gdm_score_bands_routers.router)
41
+
42
+ origins = AuthConfig.cors_origins
43
+
44
+ app.add_middleware(
45
+ CORSMiddleware,
46
+ allow_origins=origins,
47
+ allow_credentials=True,
48
+ allow_methods=["*"],
49
+ allow_headers=["*"],
50
+ )