desdeo 1.1.3__py3-none-any.whl → 2.0.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 (122) hide show
  1. desdeo/__init__.py +8 -8
  2. desdeo/api/README.md +73 -0
  3. desdeo/api/__init__.py +15 -0
  4. desdeo/api/app.py +40 -0
  5. desdeo/api/config.py +69 -0
  6. desdeo/api/config.toml +53 -0
  7. desdeo/api/db.py +25 -0
  8. desdeo/api/db_init.py +79 -0
  9. desdeo/api/db_models.py +164 -0
  10. desdeo/api/malaga_db_init.py +27 -0
  11. desdeo/api/models/__init__.py +66 -0
  12. desdeo/api/models/archive.py +34 -0
  13. desdeo/api/models/preference.py +90 -0
  14. desdeo/api/models/problem.py +507 -0
  15. desdeo/api/models/reference_point_method.py +18 -0
  16. desdeo/api/models/session.py +46 -0
  17. desdeo/api/models/state.py +96 -0
  18. desdeo/api/models/user.py +51 -0
  19. desdeo/api/routers/_NAUTILUS.py +245 -0
  20. desdeo/api/routers/_NAUTILUS_navigator.py +233 -0
  21. desdeo/api/routers/_NIMBUS.py +762 -0
  22. desdeo/api/routers/__init__.py +5 -0
  23. desdeo/api/routers/problem.py +110 -0
  24. desdeo/api/routers/reference_point_method.py +117 -0
  25. desdeo/api/routers/session.py +76 -0
  26. desdeo/api/routers/test.py +16 -0
  27. desdeo/api/routers/user_authentication.py +366 -0
  28. desdeo/api/schema.py +94 -0
  29. desdeo/api/tests/__init__.py +0 -0
  30. desdeo/api/tests/conftest.py +59 -0
  31. desdeo/api/tests/test_models.py +701 -0
  32. desdeo/api/tests/test_routes.py +216 -0
  33. desdeo/api/utils/database.py +274 -0
  34. desdeo/api/utils/logger.py +29 -0
  35. desdeo/core.py +27 -0
  36. desdeo/emo/__init__.py +29 -0
  37. desdeo/emo/hooks/archivers.py +172 -0
  38. desdeo/emo/methods/EAs.py +418 -0
  39. desdeo/emo/methods/__init__.py +0 -0
  40. desdeo/emo/methods/bases.py +59 -0
  41. desdeo/emo/operators/__init__.py +1 -0
  42. desdeo/emo/operators/crossover.py +780 -0
  43. desdeo/emo/operators/evaluator.py +118 -0
  44. desdeo/emo/operators/generator.py +356 -0
  45. desdeo/emo/operators/mutation.py +1053 -0
  46. desdeo/emo/operators/selection.py +1036 -0
  47. desdeo/emo/operators/termination.py +178 -0
  48. desdeo/explanations/__init__.py +6 -0
  49. desdeo/explanations/explainer.py +100 -0
  50. desdeo/explanations/utils.py +90 -0
  51. desdeo/mcdm/__init__.py +19 -0
  52. desdeo/mcdm/nautili.py +345 -0
  53. desdeo/mcdm/nautilus.py +477 -0
  54. desdeo/mcdm/nautilus_navigator.py +655 -0
  55. desdeo/mcdm/nimbus.py +417 -0
  56. desdeo/mcdm/pareto_navigator.py +269 -0
  57. desdeo/mcdm/reference_point_method.py +116 -0
  58. desdeo/problem/__init__.py +79 -0
  59. desdeo/problem/evaluator.py +561 -0
  60. desdeo/problem/gurobipy_evaluator.py +562 -0
  61. desdeo/problem/infix_parser.py +341 -0
  62. desdeo/problem/json_parser.py +944 -0
  63. desdeo/problem/pyomo_evaluator.py +468 -0
  64. desdeo/problem/schema.py +1808 -0
  65. desdeo/problem/simulator_evaluator.py +298 -0
  66. desdeo/problem/sympy_evaluator.py +244 -0
  67. desdeo/problem/testproblems/__init__.py +73 -0
  68. desdeo/problem/testproblems/binh_and_korn_problem.py +88 -0
  69. desdeo/problem/testproblems/dtlz2_problem.py +102 -0
  70. desdeo/problem/testproblems/forest_problem.py +275 -0
  71. desdeo/problem/testproblems/knapsack_problem.py +163 -0
  72. desdeo/problem/testproblems/mcwb_problem.py +831 -0
  73. desdeo/problem/testproblems/mixed_variable_dimenrions_problem.py +83 -0
  74. desdeo/problem/testproblems/momip_problem.py +172 -0
  75. desdeo/problem/testproblems/nimbus_problem.py +143 -0
  76. desdeo/problem/testproblems/pareto_navigator_problem.py +89 -0
  77. desdeo/problem/testproblems/re_problem.py +492 -0
  78. desdeo/problem/testproblems/river_pollution_problem.py +434 -0
  79. desdeo/problem/testproblems/rocket_injector_design_problem.py +140 -0
  80. desdeo/problem/testproblems/simple_problem.py +351 -0
  81. desdeo/problem/testproblems/simulator_problem.py +92 -0
  82. desdeo/problem/testproblems/spanish_sustainability_problem.py +945 -0
  83. desdeo/problem/testproblems/zdt_problem.py +271 -0
  84. desdeo/problem/utils.py +245 -0
  85. desdeo/tools/GenerateReferencePoints.py +181 -0
  86. desdeo/tools/__init__.py +102 -0
  87. desdeo/tools/generics.py +145 -0
  88. desdeo/tools/gurobipy_solver_interfaces.py +258 -0
  89. desdeo/tools/indicators_binary.py +11 -0
  90. desdeo/tools/indicators_unary.py +375 -0
  91. desdeo/tools/interaction_schema.py +38 -0
  92. desdeo/tools/intersection.py +54 -0
  93. desdeo/tools/iterative_pareto_representer.py +99 -0
  94. desdeo/tools/message.py +234 -0
  95. desdeo/tools/ng_solver_interfaces.py +199 -0
  96. desdeo/tools/non_dominated_sorting.py +133 -0
  97. desdeo/tools/patterns.py +281 -0
  98. desdeo/tools/proximal_solver.py +99 -0
  99. desdeo/tools/pyomo_solver_interfaces.py +464 -0
  100. desdeo/tools/reference_vectors.py +462 -0
  101. desdeo/tools/scalarization.py +3138 -0
  102. desdeo/tools/scipy_solver_interfaces.py +454 -0
  103. desdeo/tools/score_bands.py +464 -0
  104. desdeo/tools/utils.py +320 -0
  105. desdeo/utopia_stuff/__init__.py +0 -0
  106. desdeo/utopia_stuff/data/1.json +15 -0
  107. desdeo/utopia_stuff/data/2.json +13 -0
  108. desdeo/utopia_stuff/data/3.json +15 -0
  109. desdeo/utopia_stuff/data/4.json +17 -0
  110. desdeo/utopia_stuff/data/5.json +15 -0
  111. desdeo/utopia_stuff/from_json.py +40 -0
  112. desdeo/utopia_stuff/reinit_user.py +38 -0
  113. desdeo/utopia_stuff/utopia_db_init.py +212 -0
  114. desdeo/utopia_stuff/utopia_problem.py +403 -0
  115. desdeo/utopia_stuff/utopia_problem_old.py +415 -0
  116. desdeo/utopia_stuff/utopia_reference_solutions.py +79 -0
  117. desdeo-2.0.0.dist-info/LICENSE +21 -0
  118. desdeo-2.0.0.dist-info/METADATA +168 -0
  119. desdeo-2.0.0.dist-info/RECORD +120 -0
  120. {desdeo-1.1.3.dist-info → desdeo-2.0.0.dist-info}/WHEEL +1 -1
  121. desdeo-1.1.3.dist-info/METADATA +0 -18
  122. desdeo-1.1.3.dist-info/RECORD +0 -4
desdeo/mcdm/nautili.py ADDED
@@ -0,0 +1,345 @@
1
+ """Methods for the NAUTILI (a group decision making variant for NAUTILUS) method."""
2
+
3
+ import numpy as np
4
+ from pydantic import BaseModel, Field
5
+
6
+ from desdeo.mcdm.nautilus_navigator import (
7
+ calculate_distance_to_front,
8
+ calculate_navigation_point,
9
+ )
10
+ from desdeo.problem import (
11
+ Constraint,
12
+ ConstraintTypeEnum,
13
+ Problem,
14
+ get_nadir_dict,
15
+ )
16
+ from desdeo.tools.generics import BaseSolver, SolverResults, SolverOptions
17
+ from desdeo.tools.scalarization import (
18
+ add_asf_generic_nondiff,
19
+ add_asf_generic_diff,
20
+ add_epsilon_constraints,
21
+ )
22
+ from desdeo.tools.utils import guess_best_solver
23
+
24
+
25
+ class NAUTILI_Response(BaseModel):
26
+ """The response of the NAUTILI method."""
27
+
28
+ step_number: int = Field(description="The step number associted with this response.")
29
+ distance_to_front: float = Field(
30
+ description=(
31
+ "The distance travelled to the Pareto front. "
32
+ "The distance is a ratio of the distances between the nadir and navigation point, and "
33
+ "the nadir and the reachable objective vector. The distance is given in percentage."
34
+ )
35
+ )
36
+ reference_points: dict | None = Field(description="The reference points used in the step.")
37
+ improvement_directions: dict | None = Field(description="The improvement directions for each DM.")
38
+ group_improvement_direction: dict | None = Field(description="The group improvement direction.")
39
+ navigation_point: dict = Field(description="The navigation point used in the step.")
40
+ reachable_solution: dict | None = Field(description="The reachable solution found in the step.")
41
+ reachable_bounds: dict = Field(description="The reachable bounds found in the step.")
42
+
43
+
44
+ class NautiliError(Exception):
45
+ """Exception raised for errors in the NAUTILI method."""
46
+
47
+
48
+ def solve_reachable_bounds(
49
+ problem: Problem,
50
+ navigation_point: dict[str, float],
51
+ solver: BaseSolver | None = None
52
+ ) -> tuple[dict[str, float], dict[str, float]]:
53
+ """Computes the current reachable (upper and lower) bounds of the solutions in the objective space.
54
+
55
+ The reachable bound are computed based on the current navigation point. The bounds are computed by
56
+ solving an epsilon constraint problem.
57
+
58
+ Args:
59
+ problem (Problem): the problem being solved.
60
+ navigation_point (dict[str, float]): the navigation point limiting the
61
+ reachable area. The key is the objective function's symbol and the value
62
+ the navigation point.
63
+ solver (BaseSolver | None, optional): solver based on BaseSolver used to solve the problem.
64
+ If None, then a solver is utilized bases on the problem's properties. Defaults to None.
65
+
66
+ Raises:
67
+ NautiliError: when optimization of an epsilon constraint problem is not successful.
68
+
69
+ Returns:
70
+ tuple[dict[str, float], dict[str, float]]: a tuple of dicts, where the first dict are the lower bounds and the
71
+ second element the upper bounds, the key is the symbol of each objective.
72
+ """
73
+ # If an objective is to be maximized, then the navigation point component of that objective should be
74
+ # multiplied by -1.
75
+ const_bounds = {
76
+ objective.symbol: -1 * navigation_point[objective.symbol]
77
+ if objective.maximize
78
+ else navigation_point[objective.symbol]
79
+ for objective in problem.objectives
80
+ }
81
+
82
+ # if a solver creator was provided, use that, else, guess the best one
83
+ solver_init = guess_best_solver(problem) if solver is None else solver
84
+
85
+ lower_bounds = {}
86
+ upper_bounds = {}
87
+ for objective in problem.objectives:
88
+ # Lower bounds
89
+ eps_problem, target, _ = add_epsilon_constraints(
90
+ problem,
91
+ "target",
92
+ {f"{obj.symbol}": f"{obj.symbol}_eps" for obj in problem.objectives},
93
+ objective.symbol,
94
+ const_bounds,
95
+ )
96
+
97
+ # solve
98
+ solver = solver_init(eps_problem)
99
+ res = solver.solve(target)
100
+
101
+ if not res.success:
102
+ # could not optimize eps problem
103
+ msg = (
104
+ f"Optimizing the epsilon constrait problem for the objective "
105
+ f"{objective.symbol} was not successful. Reason: {res.message}"
106
+ )
107
+ raise NautiliError(msg)
108
+
109
+ lower_bound = res.optimal_objectives[objective.symbol]
110
+
111
+ if isinstance(lower_bound, list):
112
+ lower_bound = lower_bound[0]
113
+
114
+ # solver upper bounds
115
+ # the lower bounds is set as in the NAUTILUS method, e.g., taken from
116
+ # the current iteration/navigation point
117
+ if isinstance(navigation_point[objective.symbol], list):
118
+ # It should never be a list accordint to the type hints
119
+ upper_bound = navigation_point[objective.symbol][0]
120
+ else:
121
+ upper_bound = navigation_point[objective.symbol]
122
+
123
+ # add the lower and upper bounds logically depending whether an objective is to be maximized or minimized
124
+ lower_bounds[objective.symbol] = lower_bound if not objective.maximize else upper_bound
125
+ upper_bounds[objective.symbol] = upper_bound if not objective.maximize else lower_bound
126
+
127
+ return lower_bounds, upper_bounds
128
+
129
+
130
+ def solve_reachable_solution(
131
+ problem: Problem,
132
+ group_improvement_direction: dict[str, float],
133
+ previous_nav_point: dict[str, float],
134
+ solver: BaseSolver | None = None,
135
+ solver_options: SolverOptions | None = None,
136
+ ) -> SolverResults:
137
+ """Calculates the reachable solution on the Pareto optimal front.
138
+
139
+ For the calculation to make sense in the context of NAUTILI, the reference point
140
+ should be bounded by the reachable bounds present at the navigation step the
141
+ reference point has been given.
142
+
143
+ In practice, the reachable solution is calculated by solving an achievement
144
+ scalarizing function.
145
+
146
+ Args:
147
+ problem (Problem): the problem being solved.
148
+ group_improvement_direction (dict[str, float]): the improvement direction for the group.
149
+ previous_nav_point (dict[str, float]): the previous navigation point. The reachable solution found
150
+ is always better than the previous navigation point.
151
+ solver (BaseSolver | None, optional): solver based on BaseSolver used to solve the problem.
152
+ If None, then a solver is utilized bases on the problem's properties. Defaults to None.
153
+ solver_options (SolverOptions | None, optional): optional options passed
154
+ to the `solver`. Ignored if `solver` is `None`.
155
+ Defaults to None.
156
+ Returns:
157
+ SolverResults: the results of the projection.
158
+ """
159
+ # check solver
160
+ init_solver = guess_best_solver(problem) if solver is None else solver
161
+
162
+ # create and add scalarization function
163
+ # previous_nav_point = objective_dict_to_numpy_array(problem, previous_nav_point).tolist()
164
+ # weights = objective_dict_to_numpy_array(problem, group_improvement_direction).tolist()
165
+ if problem.is_twice_differentiable:
166
+ problem_w_asf, target = add_asf_generic_diff(
167
+ problem,
168
+ symbol="asf",
169
+ reference_point=previous_nav_point,
170
+ weights=group_improvement_direction,
171
+ reference_point_aug=previous_nav_point,
172
+ )
173
+ else:
174
+ problem_w_asf, target = add_asf_generic_nondiff(
175
+ problem,
176
+ symbol="asf",
177
+ reference_point=previous_nav_point,
178
+ weights=group_improvement_direction,
179
+ reference_point_aug=previous_nav_point,
180
+ )
181
+
182
+ # Note: We do not solve the global problem. Instead, we solve this constrained problem:
183
+ problem_w_asf = problem_w_asf.add_constraints(
184
+ [
185
+ Constraint(
186
+ name=f"_const_{i+1}",
187
+ symbol=f"_const_{i+1}",
188
+ func=f"{obj.symbol}_min - {previous_nav_point[obj.symbol] * (-1 if obj.maximize else 1)}",
189
+ cons_type=ConstraintTypeEnum.LTE,
190
+ is_linear=obj.is_linear,
191
+ is_convex=obj.is_convex,
192
+ is_twice_differentiable=obj.is_twice_differentiable,
193
+ )
194
+ for i, obj in enumerate(problem_w_asf.objectives)
195
+ ]
196
+ )
197
+
198
+ # solve the problem
199
+ solver = init_solver(problem_w_asf)
200
+ return solver.solve(target)
201
+
202
+
203
+ def nautili_init(problem: Problem, solver: BaseSolver | None = None) -> NAUTILI_Response:
204
+ """Initializes the NAUTILI method.
205
+
206
+ Creates the initial response of the method, which sets the navigation point to the nadir point
207
+ and the reachable bounds to the ideal and nadir points.
208
+
209
+ Args:
210
+ problem (Problem): The problem to be solved.
211
+ solver (BaseSolver | None, optional): The solver to use. Defaults to None.
212
+
213
+ Returns:
214
+ NAUTILUS_Response: The initial response of the method.
215
+ """
216
+ nav_point = get_nadir_dict(problem)
217
+ lower_bounds, upper_bounds = solve_reachable_bounds(problem, nav_point, solver=solver)
218
+ return NAUTILI_Response(
219
+ distance_to_front=0,
220
+ navigation_point=nav_point,
221
+ reachable_bounds={"lower_bounds": lower_bounds, "upper_bounds": upper_bounds},
222
+ reachable_solution=None,
223
+ reference_points=None,
224
+ improvement_directions=None,
225
+ group_improvement_direction=None,
226
+ step_number=0,
227
+ )
228
+
229
+
230
+ def nautili_step( # NOQA: PLR0913
231
+ problem: Problem,
232
+ steps_remaining: int,
233
+ step_number: int,
234
+ nav_point: dict,
235
+ solver: BaseSolver | None = None,
236
+ group_improvement_direction: dict | None = None,
237
+ reachable_solution: dict | None = None,
238
+ ) -> NAUTILI_Response:
239
+ if group_improvement_direction is None and reachable_solution is None:
240
+ raise NautiliError("Either group_improvement_direction or reachable_solution must be provided.")
241
+
242
+ if group_improvement_direction is not None and reachable_solution is not None:
243
+ raise NautiliError("Only one of group_improvement_direction or reachable_solution should be provided.")
244
+
245
+ if group_improvement_direction is not None:
246
+ opt_result = solve_reachable_solution(problem, group_improvement_direction, nav_point, solver)
247
+ reachable_solution = opt_result.optimal_objectives
248
+
249
+ # update nav point
250
+ new_nav_point = calculate_navigation_point(problem, nav_point, reachable_solution, steps_remaining)
251
+
252
+ # update_bounds
253
+ lower_bounds, upper_bounds = solve_reachable_bounds(problem, new_nav_point, solver=solver)
254
+
255
+ distance = calculate_distance_to_front(problem, new_nav_point, reachable_solution)
256
+
257
+ return NAUTILI_Response(
258
+ step_number=step_number,
259
+ distance_to_front=distance,
260
+ reference_points=None,
261
+ improvement_directions=None,
262
+ group_improvement_direction=group_improvement_direction,
263
+ navigation_point=new_nav_point,
264
+ reachable_solution=reachable_solution,
265
+ reachable_bounds={"lower_bounds": lower_bounds, "upper_bounds": upper_bounds},
266
+ )
267
+
268
+
269
+ def nautili_all_steps(
270
+ problem: Problem,
271
+ steps_remaining: int,
272
+ reference_points: dict[str, dict[str, float]],
273
+ previous_responses: list[NAUTILI_Response],
274
+ solver: BaseSolver | None = None,
275
+ ) -> [NAUTILI_Response]:
276
+ responses = []
277
+ nav_point = previous_responses[-1].navigation_point
278
+ step_number = previous_responses[-1].step_number + 1
279
+ first_iteration = True
280
+ reachable_solution = dict
281
+
282
+ # Calculate the improvement directions for each DM
283
+
284
+ improvement_directions = {}
285
+ for dm in reference_points:
286
+ if reference_points[dm] is None:
287
+ # If no reference point is provided, use the previous improvement direction
288
+ if previous_responses[-1].reference_points is None:
289
+ raise NautiliError("A reference point must be provided for the first iteration.")
290
+ if previous_responses[-1].improvement_directions is None:
291
+ raise NautiliError("An improvement direction must be provided for the first iteration.")
292
+ reference_points[dm] = previous_responses[-1].reference_points[dm]
293
+ improvement_directions[dm] = previous_responses[-1].improvement_directions[dm]
294
+ else:
295
+ # If a reference point is provided, calculate the improvement direction
296
+ # First, check if the reference point is better than the navigation point
297
+ max_multiplier = [-1 if obj.maximize else 1 for obj in problem.objectives]
298
+ reference_point = (
299
+ np.array([reference_points[dm][obj.symbol] for obj in problem.objectives]) * max_multiplier
300
+ )
301
+ nav_point_arr = np.array([nav_point[obj.symbol] for obj in problem.objectives]) * max_multiplier
302
+ improvement = nav_point_arr - reference_point
303
+ if np.any(improvement < 0):
304
+ msg = (
305
+ f"If a reference point is provided, it must be better than the navigation point.\n"
306
+ f" The reference point for {dm} is not better than the navigation point.\n"
307
+ f" Reference point: {reference_point}, Navigation point: {nav_point}\n"
308
+ f"Check objectives {np.where(improvement < 0)}"
309
+ )
310
+ raise NautiliError(msg)
311
+ # The improvement direction is in the true objective space
312
+ improvement_directions[dm] = improvement * max_multiplier
313
+ mean_improvement_direction = np.mean(list(improvement_directions.values()), axis=0)
314
+ group_improvement_direction = {
315
+ obj.symbol: mean_improvement_direction[i] for i, obj in enumerate(problem.objectives)
316
+ }
317
+
318
+ while steps_remaining > 0:
319
+ if first_iteration:
320
+ response = nautili_step(
321
+ problem,
322
+ steps_remaining=steps_remaining,
323
+ step_number=step_number,
324
+ nav_point=nav_point,
325
+ group_improvement_direction=group_improvement_direction,
326
+ solver=solver,
327
+ )
328
+ first_iteration = False
329
+ else:
330
+ response = nautili_step(
331
+ problem,
332
+ steps_remaining=steps_remaining,
333
+ step_number=step_number,
334
+ nav_point=nav_point,
335
+ reachable_solution=reachable_solution,
336
+ solver=solver,
337
+ )
338
+ response.reference_points = reference_points
339
+ response.improvement_directions = improvement_directions
340
+ responses.append(response)
341
+ reachable_solution = response.reachable_solution
342
+ nav_point = response.navigation_point
343
+ steps_remaining -= 1
344
+ step_number += 1
345
+ return responses