desdeo 1.2__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.
- desdeo/__init__.py +8 -8
- desdeo/adm/ADMAfsar.py +551 -0
- desdeo/adm/ADMChen.py +414 -0
- desdeo/adm/BaseADM.py +119 -0
- desdeo/adm/__init__.py +11 -0
- desdeo/api/README.md +73 -0
- desdeo/api/__init__.py +15 -0
- desdeo/api/app.py +50 -0
- desdeo/api/config.py +90 -0
- desdeo/api/config.toml +64 -0
- desdeo/api/db.py +27 -0
- desdeo/api/db_init.py +85 -0
- desdeo/api/db_models.py +164 -0
- desdeo/api/malaga_db_init.py +27 -0
- desdeo/api/models/__init__.py +266 -0
- desdeo/api/models/archive.py +23 -0
- desdeo/api/models/emo.py +128 -0
- desdeo/api/models/enautilus.py +69 -0
- desdeo/api/models/gdm/gdm_aggregate.py +139 -0
- desdeo/api/models/gdm/gdm_base.py +69 -0
- desdeo/api/models/gdm/gdm_score_bands.py +114 -0
- desdeo/api/models/gdm/gnimbus.py +138 -0
- desdeo/api/models/generic.py +104 -0
- desdeo/api/models/generic_states.py +401 -0
- desdeo/api/models/nimbus.py +158 -0
- desdeo/api/models/preference.py +128 -0
- desdeo/api/models/problem.py +717 -0
- desdeo/api/models/reference_point_method.py +18 -0
- desdeo/api/models/session.py +49 -0
- desdeo/api/models/state.py +463 -0
- desdeo/api/models/user.py +52 -0
- desdeo/api/models/utopia.py +25 -0
- desdeo/api/routers/_EMO.backup +309 -0
- desdeo/api/routers/_NAUTILUS.py +245 -0
- desdeo/api/routers/_NAUTILUS_navigator.py +233 -0
- desdeo/api/routers/_NIMBUS.py +765 -0
- desdeo/api/routers/__init__.py +5 -0
- desdeo/api/routers/emo.py +497 -0
- desdeo/api/routers/enautilus.py +237 -0
- desdeo/api/routers/gdm/gdm_aggregate.py +234 -0
- desdeo/api/routers/gdm/gdm_base.py +420 -0
- desdeo/api/routers/gdm/gdm_score_bands/gdm_score_bands_manager.py +398 -0
- desdeo/api/routers/gdm/gdm_score_bands/gdm_score_bands_routers.py +377 -0
- desdeo/api/routers/gdm/gnimbus/gnimbus_manager.py +698 -0
- desdeo/api/routers/gdm/gnimbus/gnimbus_routers.py +591 -0
- desdeo/api/routers/generic.py +233 -0
- desdeo/api/routers/nimbus.py +705 -0
- desdeo/api/routers/problem.py +307 -0
- desdeo/api/routers/reference_point_method.py +93 -0
- desdeo/api/routers/session.py +100 -0
- desdeo/api/routers/test.py +16 -0
- desdeo/api/routers/user_authentication.py +520 -0
- desdeo/api/routers/utils.py +187 -0
- desdeo/api/routers/utopia.py +230 -0
- desdeo/api/schema.py +100 -0
- desdeo/api/tests/__init__.py +0 -0
- desdeo/api/tests/conftest.py +151 -0
- desdeo/api/tests/test_enautilus.py +330 -0
- desdeo/api/tests/test_models.py +1179 -0
- desdeo/api/tests/test_routes.py +1075 -0
- desdeo/api/utils/_database.py +263 -0
- desdeo/api/utils/_logger.py +29 -0
- desdeo/api/utils/database.py +36 -0
- desdeo/api/utils/emo_database.py +40 -0
- desdeo/core.py +34 -0
- desdeo/emo/__init__.py +159 -0
- desdeo/emo/hooks/archivers.py +188 -0
- desdeo/emo/methods/EAs.py +541 -0
- desdeo/emo/methods/__init__.py +0 -0
- desdeo/emo/methods/bases.py +12 -0
- desdeo/emo/methods/templates.py +111 -0
- desdeo/emo/operators/__init__.py +1 -0
- desdeo/emo/operators/crossover.py +1282 -0
- desdeo/emo/operators/evaluator.py +114 -0
- desdeo/emo/operators/generator.py +459 -0
- desdeo/emo/operators/mutation.py +1224 -0
- desdeo/emo/operators/scalar_selection.py +202 -0
- desdeo/emo/operators/selection.py +1778 -0
- desdeo/emo/operators/termination.py +286 -0
- desdeo/emo/options/__init__.py +108 -0
- desdeo/emo/options/algorithms.py +435 -0
- desdeo/emo/options/crossover.py +164 -0
- desdeo/emo/options/generator.py +131 -0
- desdeo/emo/options/mutation.py +260 -0
- desdeo/emo/options/repair.py +61 -0
- desdeo/emo/options/scalar_selection.py +66 -0
- desdeo/emo/options/selection.py +127 -0
- desdeo/emo/options/templates.py +383 -0
- desdeo/emo/options/termination.py +143 -0
- desdeo/explanations/__init__.py +6 -0
- desdeo/explanations/explainer.py +100 -0
- desdeo/explanations/utils.py +90 -0
- desdeo/gdm/__init__.py +22 -0
- desdeo/gdm/gdmtools.py +45 -0
- desdeo/gdm/score_bands.py +114 -0
- desdeo/gdm/voting_rules.py +50 -0
- desdeo/mcdm/__init__.py +41 -0
- desdeo/mcdm/enautilus.py +338 -0
- desdeo/mcdm/gnimbus.py +484 -0
- desdeo/mcdm/nautili.py +345 -0
- desdeo/mcdm/nautilus.py +477 -0
- desdeo/mcdm/nautilus_navigator.py +656 -0
- desdeo/mcdm/nimbus.py +417 -0
- desdeo/mcdm/pareto_navigator.py +269 -0
- desdeo/mcdm/reference_point_method.py +186 -0
- desdeo/problem/__init__.py +83 -0
- desdeo/problem/evaluator.py +561 -0
- desdeo/problem/external/__init__.py +18 -0
- desdeo/problem/external/core.py +356 -0
- desdeo/problem/external/pymoo_provider.py +266 -0
- desdeo/problem/external/runtime.py +44 -0
- desdeo/problem/gurobipy_evaluator.py +562 -0
- desdeo/problem/infix_parser.py +341 -0
- desdeo/problem/json_parser.py +944 -0
- desdeo/problem/pyomo_evaluator.py +487 -0
- desdeo/problem/schema.py +1829 -0
- desdeo/problem/simulator_evaluator.py +348 -0
- desdeo/problem/sympy_evaluator.py +244 -0
- desdeo/problem/testproblems/__init__.py +88 -0
- desdeo/problem/testproblems/benchmarks_server.py +120 -0
- desdeo/problem/testproblems/binh_and_korn_problem.py +88 -0
- desdeo/problem/testproblems/cake_problem.py +185 -0
- desdeo/problem/testproblems/dmitry_forest_problem_discrete.py +71 -0
- desdeo/problem/testproblems/dtlz2_problem.py +102 -0
- desdeo/problem/testproblems/forest_problem.py +283 -0
- desdeo/problem/testproblems/knapsack_problem.py +163 -0
- desdeo/problem/testproblems/mcwb_problem.py +831 -0
- desdeo/problem/testproblems/mixed_variable_dimenrions_problem.py +83 -0
- desdeo/problem/testproblems/momip_problem.py +172 -0
- desdeo/problem/testproblems/multi_valued_constraints.py +119 -0
- desdeo/problem/testproblems/nimbus_problem.py +143 -0
- desdeo/problem/testproblems/pareto_navigator_problem.py +89 -0
- desdeo/problem/testproblems/re_problem.py +492 -0
- desdeo/problem/testproblems/river_pollution_problems.py +440 -0
- desdeo/problem/testproblems/rocket_injector_design_problem.py +140 -0
- desdeo/problem/testproblems/simple_problem.py +351 -0
- desdeo/problem/testproblems/simulator_problem.py +92 -0
- desdeo/problem/testproblems/single_objective.py +289 -0
- desdeo/problem/testproblems/spanish_sustainability_problem.py +945 -0
- desdeo/problem/testproblems/zdt_problem.py +274 -0
- desdeo/problem/utils.py +245 -0
- desdeo/tools/GenerateReferencePoints.py +181 -0
- desdeo/tools/__init__.py +120 -0
- desdeo/tools/desc_gen.py +22 -0
- desdeo/tools/generics.py +165 -0
- desdeo/tools/group_scalarization.py +3090 -0
- desdeo/tools/gurobipy_solver_interfaces.py +258 -0
- desdeo/tools/indicators_binary.py +117 -0
- desdeo/tools/indicators_unary.py +362 -0
- desdeo/tools/interaction_schema.py +38 -0
- desdeo/tools/intersection.py +54 -0
- desdeo/tools/iterative_pareto_representer.py +99 -0
- desdeo/tools/message.py +265 -0
- desdeo/tools/ng_solver_interfaces.py +199 -0
- desdeo/tools/non_dominated_sorting.py +134 -0
- desdeo/tools/patterns.py +283 -0
- desdeo/tools/proximal_solver.py +99 -0
- desdeo/tools/pyomo_solver_interfaces.py +477 -0
- desdeo/tools/reference_vectors.py +229 -0
- desdeo/tools/scalarization.py +2065 -0
- desdeo/tools/scipy_solver_interfaces.py +454 -0
- desdeo/tools/score_bands.py +627 -0
- desdeo/tools/utils.py +388 -0
- desdeo/tools/visualizations.py +67 -0
- desdeo/utopia_stuff/__init__.py +0 -0
- desdeo/utopia_stuff/data/1.json +15 -0
- desdeo/utopia_stuff/data/2.json +13 -0
- desdeo/utopia_stuff/data/3.json +15 -0
- desdeo/utopia_stuff/data/4.json +17 -0
- desdeo/utopia_stuff/data/5.json +15 -0
- desdeo/utopia_stuff/from_json.py +40 -0
- desdeo/utopia_stuff/reinit_user.py +38 -0
- desdeo/utopia_stuff/utopia_db_init.py +212 -0
- desdeo/utopia_stuff/utopia_problem.py +403 -0
- desdeo/utopia_stuff/utopia_problem_old.py +415 -0
- desdeo/utopia_stuff/utopia_reference_solutions.py +79 -0
- desdeo-2.1.0.dist-info/METADATA +186 -0
- desdeo-2.1.0.dist-info/RECORD +180 -0
- {desdeo-1.2.dist-info → desdeo-2.1.0.dist-info}/WHEEL +1 -1
- desdeo-2.1.0.dist-info/licenses/LICENSE +21 -0
- desdeo-1.2.dist-info/METADATA +0 -16
- desdeo-1.2.dist-info/RECORD +0 -4
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
desdeo/api/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# DESDEO web API
|
|
2
|
+
Experimental. The instructions below assume that the current working directory
|
|
3
|
+
is `desdeo/api` and that the DESDEO framework has been installed successfully
|
|
4
|
+
with the extra dependencies in the `api` group.
|
|
5
|
+
|
|
6
|
+
## API configuration
|
|
7
|
+
|
|
8
|
+
The API configuration is handled by the settings found in `config.toml`. This
|
|
9
|
+
file should be used for reading existing configuration parameters and adding
|
|
10
|
+
new ones. Except for deployment (TODO).
|
|
11
|
+
|
|
12
|
+
## Initializing the database
|
|
13
|
+
|
|
14
|
+
To initialize the database, run the script `db_init.py` with the following command:
|
|
15
|
+
|
|
16
|
+
```shell
|
|
17
|
+
python db_init.py
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
This will create an initial databse, which allows testing for, e.g., testing
|
|
21
|
+
the databse. However, the tests themselves (see below), do not depend on this
|
|
22
|
+
database, and handle the database for running the tests on their own.
|
|
23
|
+
|
|
24
|
+
Importantly, this will create an analyst user, which can be used to test out
|
|
25
|
+
the end-points using the FastAPI generated documentation (see below). The
|
|
26
|
+
default name of the analyst user will be 'analyst' with the password 'analyst'.
|
|
27
|
+
|
|
28
|
+
This is mainly for testing purposes, which will create and manage a local
|
|
29
|
+
dblite-databse.
|
|
30
|
+
|
|
31
|
+
## Running the API
|
|
32
|
+
|
|
33
|
+
To run the API, invoke `uvicorn` as followsi:
|
|
34
|
+
|
|
35
|
+
```shell
|
|
36
|
+
uvicorn --app-dir=./ app:app --reload
|
|
37
|
+
```
|
|
38
|
+
See the outputs of the command to figure out where the API is running. This is
|
|
39
|
+
likely something along the lines `http:127.0.0.1:8000`, but may vary.
|
|
40
|
+
|
|
41
|
+
## Exploring the API
|
|
42
|
+
|
|
43
|
+
Once the API is running, its endpoints can be interactively explored by accessing
|
|
44
|
+
`<api_url>/docs`. For example, `http:127.0.0:8000/docs`. The `analyst` user created by
|
|
45
|
+
invoking `init_db.py` can be used to authorize and access the protected endpoints.
|
|
46
|
+
|
|
47
|
+
## Running (unit) tests
|
|
48
|
+
|
|
49
|
+
Pytest can be used to run tests relate to the API with the following command:
|
|
50
|
+
|
|
51
|
+
```shell
|
|
52
|
+
pytest
|
|
53
|
+
```
|
|
54
|
+
Again, it is assumed for the current working directory to be `desdeo/api`.
|
|
55
|
+
Otherwise a lot of tests will be executed.
|
|
56
|
+
|
|
57
|
+
## How the API works
|
|
58
|
+
|
|
59
|
+
The API consists of two important concepts: __models__ and __routers__. At the
|
|
60
|
+
core of the API, is a database.
|
|
61
|
+
|
|
62
|
+
Models utilize SQLite to define databse models. In other words, it describes
|
|
63
|
+
stuff we want to store in the databse. These models are very similar to
|
|
64
|
+
pydantic models, and are almost interchangeable. One major difference is that
|
|
65
|
+
the models are relational. This means that models often relate to each other.
|
|
66
|
+
E.g., an `ArchiveEntryDB` is the child of the model `User`.
|
|
67
|
+
|
|
68
|
+
Routers define HTTP endpoints to access and database and the models within.
|
|
69
|
+
These endpoints often modify the contents of the database, e.g., by solving a
|
|
70
|
+
problem with an interactive method, and then saving the solutions back to the
|
|
71
|
+
database. Endpoints often make use of code found in the core-logic (the
|
|
72
|
+
algorithms containing part of DESDEO), and are, in fact, a way to utilize this
|
|
73
|
+
code through the web (which is the whole point of this API!).
|
desdeo/api/__init__.py
ADDED
desdeo/api/app.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""The main FastAPI application for the DESDEO API."""
|
|
2
|
+
|
|
3
|
+
from fastapi import FastAPI
|
|
4
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
5
|
+
|
|
6
|
+
from desdeo.api.config import AuthConfig
|
|
7
|
+
from desdeo.api.routers import (
|
|
8
|
+
emo,
|
|
9
|
+
enautilus,
|
|
10
|
+
generic,
|
|
11
|
+
nimbus,
|
|
12
|
+
problem,
|
|
13
|
+
reference_point_method,
|
|
14
|
+
session,
|
|
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.",
|
|
26
|
+
)
|
|
27
|
+
|
|
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
|
+
)
|