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/mcdm/gnimbus.py ADDED
@@ -0,0 +1,484 @@
1
+ """Functions related to the GNIMBUS method.
2
+
3
+ References:
4
+ TBA
5
+ """
6
+
7
+ from typing import Literal
8
+
9
+ import numpy as np
10
+
11
+ from desdeo.gdm.gdmtools import agg_aspbounds, dict_of_rps_to_list_of_rps, scale_delta
12
+ from desdeo.gdm.voting_rules import plurality_rule
13
+ from desdeo.mcdm.nimbus import infer_classifications, solve_sub_problems
14
+ from desdeo.problem import (
15
+ Problem,
16
+ )
17
+ from desdeo.tools import (
18
+ BaseSolver,
19
+ SolverOptions,
20
+ SolverResults,
21
+ GurobipySolver,
22
+ add_group_asf,
23
+ add_group_asf_agg,
24
+ add_group_asf_agg_diff,
25
+ add_group_asf_diff,
26
+ add_group_guess,
27
+ add_group_guess_agg,
28
+ add_group_guess_agg_diff,
29
+ add_group_guess_diff,
30
+ add_group_nimbus,
31
+ add_group_nimbus_compromise,
32
+ add_group_nimbus_compromise_diff,
33
+ add_group_nimbus_diff,
34
+ add_group_stom,
35
+ add_group_stom_agg,
36
+ add_group_stom_agg_diff,
37
+ add_group_stom_diff,
38
+ guess_best_solver,
39
+ )
40
+
41
+
42
+ class GNIMBUSError(Exception):
43
+ """Raised when an error with a NIMBUS method is encountered."""
44
+
45
+
46
+ def voting_procedure(problem: Problem, solutions, votes_idxs: dict[str, int]) -> SolverResults:
47
+ """More general procedure for GNIMBUS for any number of DMs.
48
+ TODO(@jpajasmaa): docs and cleaning up.
49
+ """
50
+ # winner_idx = None
51
+ # call majority
52
+ """ general procedure does not apply majority rule
53
+ winner_idx = majority_rule(votes_idxs)
54
+ if winner_idx is not None:
55
+ print("Majority winner", winner_idx)
56
+ return solutions[winner_idx]
57
+ """
58
+ # call plurality
59
+ winners = plurality_rule(votes_idxs)
60
+ print("winners")
61
+ if len(winners) == 1:
62
+ print("Plurality winner", winners[0])
63
+ return solutions[winners[0]] # need to unlist the winners list
64
+
65
+ print("TIE-breaking, select a solution randomly among top voted ones")
66
+ """
67
+ # if two same solutions with same number of votes, call intermediate
68
+ # TODO:(@jpajasmaa) not perfect check as it is possible to have a problem that we can calculate more solutions
69
+ # AND discrete representation also.
70
+ if problem.discrete_representation is None:
71
+ wsol1, wsol2 = solutions[winners[0]].optimal_variables, solutions[winners[1]].optimal_variables
72
+ else:
73
+ wsol1, wsol2 = solutions[winners[0]].optimal_objectives, solutions[winners[1]].optimal_objectives
74
+ print("Finding intermediate solution between", wsol1, wsol2)
75
+ # return solve_intermediate_solutions_only_objs(problem, wsol1, wsol2, num_desired=3)
76
+ return solve_intermediate_solutions(problem, wsol1, wsol2, num_desired=1)[0]
77
+ """
78
+ # n_of_sols = len(solutions)
79
+ rng = np.random.default_rng()
80
+ random_idx = rng.choice(winners)
81
+ return solutions[random_idx]
82
+
83
+
84
+ def infer_group_classifications(
85
+ problem: Problem,
86
+ current_objectives: dict[str, float],
87
+ reference_points: dict[str, dict[str, float]],
88
+ *,
89
+ silent: bool = True,
90
+ ) -> dict[str, tuple[Literal["improve", "worsen", "conflict"], list[float]]]:
91
+ """Infers group classification from the reference points given by the group.
92
+
93
+ Args:
94
+ problem (Problem): the problem being solved
95
+ current_objectives (dict[str, float]): objective values at the current iteration
96
+ reference_points (dict[str, dict[str, float]]): The reference points given by the group.
97
+ The keys of the outer dict are the decision makers and the keys of the inner dict are objective symbols.
98
+ silent (bool): If false, the classifications will be printed.
99
+
100
+ Raises:
101
+ GNIMBUSError: _description_
102
+
103
+ Returns:
104
+ dict[str, tuple[str, list[float]]]: _description_
105
+ """
106
+ for dm, reference_point in reference_points.items():
107
+ # for rp in reference_point:
108
+ if not all(obj.symbol in reference_point for obj in problem.objectives):
109
+ print(reference_point)
110
+ msg = (
111
+ f"The reference point {reference_point} of {dm} is missing entries "
112
+ "for one or more of the objective functions."
113
+ )
114
+ raise GNIMBUSError(msg)
115
+
116
+ group_classifications = {}
117
+ for obj in problem.objectives:
118
+ # maximization
119
+ if obj.maximize and all(
120
+ (
121
+ reference_points[dm][obj.symbol] >= current_objectives[obj.symbol]
122
+ or np.isclose(reference_points[dm][obj.symbol], current_objectives[obj.symbol])
123
+ )
124
+ for dm in reference_points
125
+ ):
126
+ classify = "improve"
127
+ elif obj.maximize and all(
128
+ reference_points[dm][obj.symbol] < current_objectives[obj.symbol] for dm in reference_points
129
+ ):
130
+ classify = "worsen"
131
+ # minimization
132
+ elif (not obj.maximize) and all(
133
+ (
134
+ reference_points[dm][obj.symbol] <= current_objectives[obj.symbol]
135
+ or np.isclose(reference_points[dm][obj.symbol], current_objectives[obj.symbol])
136
+ )
137
+ for dm in reference_points
138
+ ):
139
+ classify = "improve"
140
+ elif (not obj.maximize) and all(
141
+ reference_points[dm][obj.symbol] > current_objectives[obj.symbol] for dm in reference_points
142
+ ):
143
+ classify = "worsen"
144
+ else:
145
+ classify = "conflict"
146
+ group_classifications[obj.symbol] = (classify, [reference_points[dm][obj.symbol] for dm in reference_points])
147
+
148
+ if not silent:
149
+ for symbol, value in group_classifications.items():
150
+ if value[0] == "improve":
151
+ print(f"The group wants to improve objective {symbol}")
152
+ print(value[1])
153
+ if value[0] == "worsen":
154
+ print(f"The group wants to worsen objective {symbol}")
155
+ print(value[1])
156
+ if value[0] == "conflict":
157
+ print(f"The group has conflicting views about objective {symbol}")
158
+ print(value[1])
159
+
160
+ return group_classifications
161
+
162
+
163
+ def solve_group_sub_problems( # noqa: PLR0913, RET503
164
+ problem: Problem,
165
+ current_objectives: dict[str, float],
166
+ reference_points: dict[str, dict[str, float]],
167
+ phase: str,
168
+ scalarization_options: dict | None = None,
169
+ create_solver: BaseSolver | None = None,
170
+ solver_options: SolverOptions | None = None,
171
+ ) -> list[SolverResults]:
172
+ r"""Solves a number of sub-problems as defined in the GNIMBUS methods.
173
+
174
+ TODO: update docs
175
+
176
+ Solves 4 scalarized problems utilizing different scalarization
177
+ functions. The scalarizations are based on the classification of a
178
+ solutions provided by a decision maker. The classifications
179
+ are represented by a reference point. Returns a number of new solutions
180
+ corresponding to the number of scalarization functions solved.
181
+
182
+ Solves the following scalarized problems corresponding
183
+ the the following scalarization functions:
184
+
185
+ 1. the NIMBUS scalarization function,
186
+ 2. the STOM scalarization function,
187
+ 3. the achievement scalarizing function, and
188
+ 4. the GUESS scalarization function.
189
+
190
+ Raises:
191
+ GNIMBUSError: the given problem has an undefined ideal or nadir point, or both.
192
+ GNIMBUSError: either the reference point of current objective functions value are
193
+ missing entries for one or more of the objective functions defined in the problem.
194
+
195
+ Args:
196
+ problem(Problem): the problem being solved.
197
+ current_objectives(dict[str, float]): an objective dictionary with the objective functions values
198
+ the classifications have been given with respect to.
199
+ reference_points(dict[str, dict[str, float]]): A dictionary containing an objective dictionary with a reference point for each DM.
200
+ The classifications utilized in the sub problems are derived from
201
+ the reference points.
202
+ phase(str): The selected phase of the solution process. Must be one of "learning", "crp", "decision" or "compromise".
203
+ scalarization_options(dict | None, optional): optional kwargs passed to the scalarization function.
204
+ Defaults to None.
205
+ create_solver(CreateSolverType | None, optional): a function that given a problem, will return a solver.
206
+ If not given, an appropriate solver will be automatically determined based on the features of `problem`.
207
+ Defaults to None.
208
+ solver_options(SolverOptions | None, optional): optional options passed
209
+ to the `create_solver` routine. Ignored if `create_solver` is `None`.
210
+ Defaults to None.
211
+
212
+ Returns:
213
+ list[SolverResults]: a list of `SolverResults` objects. Contains as many elements
214
+ as defined in `num_desired`.
215
+ """
216
+ if None in problem.get_ideal_point() or None in problem.get_nadir_point():
217
+ msg = "The given problem must have both an ideal and nadir point defined."
218
+ raise GNIMBUSError(msg)
219
+
220
+ DMs = reference_points.keys()
221
+ for dm in DMs:
222
+ reference_point = reference_points[dm]
223
+ # for rp in reference_point:
224
+ if not all(obj.symbol in reference_point for obj in problem.objectives):
225
+ print(reference_point)
226
+ msg = (
227
+ f"The reference point {reference_point} is missing entries for one or more of the objective functions."
228
+ )
229
+ raise GNIMBUSError(msg)
230
+ # check that at least one objective function is allowed to be improved and one is allowed to worsen
231
+ classifications = infer_classifications(problem, current_objectives, reference_point)
232
+ if not any(classifications[obj.symbol][0] in ["<", "<="] for obj in problem.objectives) or not any(
233
+ classifications[obj.symbol][0] in [">=", "0"] for obj in problem.objectives
234
+ ):
235
+ msg = (
236
+ f"The given classifications {classifications} should allow at least one objective function value "
237
+ "to improve and one to worsen."
238
+ )
239
+ raise GNIMBUSError(msg)
240
+
241
+ if not all(obj.symbol in current_objectives for obj in problem.objectives):
242
+ msg = f"The current point {current_objectives} is missing entries for one or more of the objective functions."
243
+ raise GNIMBUSError(msg)
244
+
245
+ init_solver = create_solver if create_solver is not None else guess_best_solver(problem)
246
+ if init_solver is GurobipySolver and not solver_options:
247
+ solver_options = {"OutputFlag": 0} #TODO: how does one want this to behave?
248
+ _solver_options = solver_options if solver_options is not None else None
249
+ # print("solver is ", init_solver)
250
+
251
+ solutions = []
252
+ classification_list = []
253
+ achievable_prefs = []
254
+
255
+ ind_sols = []
256
+ reference_points_list = dict_of_rps_to_list_of_rps(reference_points)
257
+
258
+ # Solve for individual solutions using nimbus scalarization.
259
+ for dm_rp in reference_points:
260
+ ind_sols.append(solve_sub_problems(
261
+ problem=problem,
262
+ current_objectives=current_objectives,
263
+ reference_point=reference_points[dm_rp],
264
+ num_desired=1,
265
+ scalarization_options=None,
266
+ solver=init_solver,
267
+ solver_options=_solver_options)[0],
268
+ )
269
+
270
+ achievable_prefs = []
271
+ for q in range(len(reference_points)):
272
+ achievable_prefs.append(ind_sols[q].optimal_objectives)
273
+
274
+ agg_aspirations, agg_bounds = agg_aspbounds(achievable_prefs, problem)
275
+ delta = scale_delta(problem, d=1e-6)
276
+
277
+ if phase == "decision":
278
+ for dm_rp in reference_points:
279
+ classification_list.append(infer_classifications(problem, current_objectives, reference_points[dm_rp]))
280
+ gnimbus_scala = add_group_nimbus_diff if problem.is_twice_differentiable else add_group_nimbus
281
+ add_nimbus_sf = gnimbus_scala
282
+
283
+ problem_g_nimbus, gnimbus_target = add_nimbus_sf(
284
+ problem, "nimbus_sf", classification_list, current_objectives, agg_bounds, delta, **(scalarization_options or {})
285
+ )
286
+
287
+ if _solver_options:
288
+ gnimbus_solver = init_solver(problem_g_nimbus, _solver_options) # type:ignore
289
+ else:
290
+ gnimbus_solver = init_solver(problem_g_nimbus) # type:ignore
291
+
292
+ solutions.append(gnimbus_solver.solve(gnimbus_target))
293
+
294
+ infer_group_classifications(problem, current_objectives, reference_points, silent=False)
295
+
296
+ return solutions
297
+
298
+ elif phase == "compromise":
299
+ # Run compromise phase with applying group-asf.
300
+ reference_points_list = dict_of_rps_to_list_of_rps(reference_points)
301
+ # solve ASF
302
+ add_asf = add_group_asf_diff if problem.is_twice_differentiable else add_group_asf
303
+ problem_w_asf, asf_target = add_asf(
304
+ problem, "asf", reference_points_list, agg_bounds, delta, **(scalarization_options or {})
305
+ )
306
+ if _solver_options:
307
+ asf_solver = init_solver(problem_w_asf, _solver_options) # type:ignore
308
+ else:
309
+ asf_solver = init_solver(problem_w_asf) # type:ignore
310
+
311
+ solutions.append(asf_solver.solve(asf_target))
312
+
313
+ return solutions
314
+
315
+ """
316
+ classification_list = infer_group_classifications(problem, current_objectives, reference_points)
317
+ # All cool, the preference's are in a bit of a different format that other branches but works.
318
+
319
+ gnimbus_scala = add_group_nimbus_compromise_diff \
320
+ if problem.is_twice_differentiable else add_group_nimbus_compromise
321
+ add_nimbus_sf = gnimbus_scala
322
+
323
+ problem_g_nimbus, gnimbus_target = add_nimbus_sf(
324
+ problem, "nimbus_sf", classification_list, current_objectives, **(scalarization_options or {})
325
+ )
326
+ # ISSUE: makes the problem not twice differentiable, thus the initial solver doesn't work
327
+ # Also needed a little tweaking inside the scalarization functions.
328
+
329
+ if _solver_options:
330
+ gnimbus_solver = init_solver(problem_g_nimbus, _solver_options) # type:ignore
331
+ else:
332
+ gnimbus_solver = init_solver(problem_g_nimbus) # type:ignore
333
+
334
+ solutions.append(gnimbus_solver.solve(gnimbus_target))
335
+
336
+ infer_group_classifications(problem, current_objectives, reference_points, silent=False)
337
+
338
+ return solutions
339
+ """
340
+
341
+ elif phase == "learning":
342
+ reference_points_list = dict_of_rps_to_list_of_rps(reference_points)
343
+
344
+ # Add individual solutions
345
+ for i in range(len(ind_sols)):
346
+ solutions.append(ind_sols[i])
347
+ """ Group nimbus scalarization with delta and added hard_constraints """
348
+ classification_list = []
349
+ for dm_rp in reference_points:
350
+ classification_list.append(infer_classifications(problem, current_objectives, reference_points[dm_rp]))
351
+ print(classification_list)
352
+ gnimbus_scala = add_group_nimbus_diff if problem.is_twice_differentiable else add_group_nimbus
353
+ add_nimbus_sf = gnimbus_scala
354
+
355
+ problem_w_nimbus, nimbus_target = add_nimbus_sf(
356
+ problem,
357
+ "nimbus_sf",
358
+ classification_list,
359
+ current_objectives,
360
+ agg_bounds,
361
+ delta,
362
+ **(scalarization_options or {}),
363
+ )
364
+
365
+ if _solver_options:
366
+ nimbus_solver = init_solver(problem_w_nimbus, _solver_options) # type:ignore
367
+ else:
368
+ nimbus_solver = init_solver(problem_w_nimbus) # type:ignore
369
+
370
+ solutions.append(nimbus_solver.solve(nimbus_target))
371
+
372
+ """ SOLVING Group Scals with scaled delta, original RPs and hard_constraints """
373
+ # solve STOM
374
+ add_stom_sf = add_group_stom_diff if problem.is_twice_differentiable else add_group_stom
375
+ problem_w_stom, stom_target = add_stom_sf(
376
+ problem, "stom_sf", reference_points_list, agg_bounds, delta, **(scalarization_options or {})
377
+ )
378
+ if _solver_options:
379
+ stom_solver = init_solver(problem_w_stom, _solver_options) # type:ignore
380
+ else:
381
+ stom_solver = init_solver(problem_w_stom) # type:ignore
382
+
383
+ solutions.append(stom_solver.solve(stom_target))
384
+
385
+ # solve ASF
386
+ add_asf = add_group_asf_diff if problem.is_twice_differentiable else add_group_asf
387
+ problem_w_asf, asf_target = add_asf(
388
+ problem, "asf", reference_points_list, agg_bounds, delta, **(scalarization_options or {})
389
+ )
390
+ if _solver_options:
391
+ asf_solver = init_solver(problem_w_asf, _solver_options) # type:ignore
392
+ else:
393
+ asf_solver = init_solver(problem_w_asf) # type:ignore
394
+
395
+ solutions.append(asf_solver.solve(asf_target))
396
+
397
+ # Solve GUESS
398
+ add_guess_sf = add_group_guess_diff if problem.is_twice_differentiable else add_group_guess
399
+ problem_w_guess, guess_target = add_guess_sf(
400
+ problem, "guess_sf", reference_points_list, agg_bounds, delta, **(scalarization_options or {})
401
+ )
402
+ if _solver_options:
403
+ guess_solver = init_solver(problem_w_guess, _solver_options) # type:ignore
404
+ else:
405
+ guess_solver = init_solver(problem_w_guess) # type:ignore
406
+
407
+ solutions.append(guess_solver.solve(guess_target))
408
+
409
+ infer_group_classifications(problem, current_objectives, reference_points, silent=False)
410
+
411
+ return solutions
412
+
413
+ else: # phase is concsensus reaching
414
+ # Add individual solutions
415
+ for i in range(len(ind_sols)):
416
+ solutions.append(ind_sols[i])
417
+
418
+ """ Group nimbus scalarization with delta and added hard_constraints """
419
+ classification_list = []
420
+ for dm_rp in reference_points:
421
+ print("RPS", reference_points[dm_rp])
422
+ classification_list.append(infer_classifications(problem, current_objectives, reference_points[dm_rp]))
423
+ print(classification_list)
424
+ gnimbus_scala = add_group_nimbus_diff if problem.is_twice_differentiable else add_group_nimbus
425
+ add_nimbus_sf = gnimbus_scala
426
+
427
+ problem_w_nimbus, nimbus_target = add_nimbus_sf(
428
+ problem,
429
+ "nimbus_sf",
430
+ classification_list,
431
+ current_objectives,
432
+ agg_bounds,
433
+ delta,
434
+ **(scalarization_options or {}),
435
+ )
436
+
437
+ if _solver_options:
438
+ nimbus_solver = init_solver(problem_w_nimbus, _solver_options) # type:ignore
439
+ else:
440
+ nimbus_solver = init_solver(problem_w_nimbus) # type:ignore
441
+
442
+ solutions.append(nimbus_solver.solve(nimbus_target))
443
+
444
+ """ SOLVING Group Scals with scaled delta, agg. aspirations and hard_constraints """
445
+
446
+ add_stom_sf2 = add_group_stom_agg_diff if problem.is_twice_differentiable else add_group_stom_agg
447
+
448
+ problem_g_stom, stomg_target = add_stom_sf2(
449
+ problem, "stom_sf2", agg_aspirations, agg_bounds, delta, **(scalarization_options or {})
450
+ )
451
+ if _solver_options:
452
+ stomg_solver = init_solver(problem_g_stom, _solver_options) # type:ignore
453
+ else:
454
+ stomg_solver = init_solver(problem_g_stom) # type:ignore
455
+
456
+ solutions.append(stomg_solver.solve(stomg_target))
457
+
458
+ add_asf2 = add_group_asf_agg_diff if problem.is_twice_differentiable else add_group_asf_agg
459
+ problem_g_asf, asfg_target = add_asf2(
460
+ problem, "asf2", agg_aspirations, agg_bounds, delta, **(scalarization_options or {})
461
+ )
462
+ if _solver_options:
463
+ asfg_solver = init_solver(problem_g_asf, _solver_options) # type:ignore
464
+ else:
465
+ asfg_solver = init_solver(problem_g_asf) # type:ignore
466
+
467
+ solutions.append(asfg_solver.solve(asfg_target))
468
+
469
+ add_guess_sf2 = add_group_guess_agg_diff if problem.is_twice_differentiable else add_group_guess_agg
470
+
471
+ problem_g_guess, guess2_target = add_guess_sf2(
472
+ problem, "guess_sf2", agg_aspirations, agg_bounds, delta, **(scalarization_options or {})
473
+ )
474
+
475
+ if _solver_options:
476
+ guess2_solver = init_solver(problem_g_guess, _solver_options) # type:ignore
477
+ else:
478
+ guess2_solver = init_solver(problem_g_guess) # type:ignore
479
+
480
+ solutions.append(guess2_solver.solve(guess2_target))
481
+
482
+ infer_group_classifications(problem, current_objectives, reference_points, silent=False)
483
+
484
+ return solutions
@@ -303,8 +303,8 @@ def solve_reachable_solution(
303
303
  # Note: We do not solve the global problem. Instead, we solve this constrained problem:
304
304
  constraints = [
305
305
  Constraint(
306
- name=f"_const_{i+1}",
307
- symbol=f"_const_{i+1}",
306
+ name=f"_const_{i + 1}",
307
+ symbol=f"_const_{i + 1}",
308
308
  func=f"{obj.symbol}_min - {previous_nav_point[obj.symbol] * (-1 if obj.maximize else 1)}",
309
309
  cons_type=ConstraintTypeEnum.LTE,
310
310
  is_linear=obj.is_linear,
@@ -317,8 +317,8 @@ def solve_reachable_solution(
317
317
  if bounds is not None:
318
318
  constraints += [
319
319
  Constraint(
320
- name=f"_const_bound_{i+1}",
321
- symbol=f"_const_bound_{i+1}",
320
+ name=f"_const_bound_{i + 1}",
321
+ symbol=f"_const_bound_{i + 1}",
322
322
  cons_type=ConstraintTypeEnum.LTE,
323
323
  func=f"{obj.symbol}_min - {bounds[obj.symbol] * (-1 if obj.maximize else 1)}",
324
324
  is_linear=obj.is_linear,
@@ -416,8 +416,9 @@ def navigator_step( # NOQA: PLR0913
416
416
  nav_point (dict): The current navigation point.
417
417
  solver (BaseSolver | None, optional): The solver to use. Defaults to None.
418
418
  reference_point (dict | None, optional): The reference point provided by the DM. Defaults to None, in which
419
- case it is assumed that the DM has not changed their preference. The algorithm uses the last reachable solution,
420
- which must be provided in this case.
419
+ case it is assumed that the DM has not changed their preference. The
420
+ algorithm uses the last reachable solution,
421
+ which must be provided in this case.
421
422
  bounds (dict | None, optional): The bounds of the problem provided by the DM. Defaults to None.
422
423
  reachable_solution (dict | None, optional): The previous reachable solution. Must only be provided if the DM
423
424
  has not changed their preference. Defaults to None.
@@ -114,3 +114,73 @@ def rpm_solve_solutions(
114
114
 
115
115
  # return the original solution and the solutions found with the perturbed reference points
116
116
  return [initial_solution, *perturbed_solutions]
117
+
118
+
119
+ def rpm_intermediate_solutions( # noqa: PLR0913
120
+ problem: Problem,
121
+ solution_1: dict[str, float],
122
+ solution_2: dict[str, float],
123
+ num_desired: int,
124
+ scalarization_options: dict | None = None,
125
+ solver: BaseSolver | None = None,
126
+ solver_options: SolverOptions | None = None,
127
+ ) -> list[SolverResults]:
128
+ """Generates a desired number of intermediate solutions between two given solutions.
129
+
130
+ Generates a desires number of intermediate solutions given reference vectors.
131
+ The solutions are generated by taking n number of steps between the two reference points in the
132
+ objective space. The vectors corresponding to these values are then
133
+ utilized as reference points in the achievement scalarizing function. Solving the functions
134
+ for each reference point will a solution close the the projection of the reference point on the
135
+ Pareto optimal front of the problem. These solutions are then returned. Note that the
136
+ intermediate solutions are generated _between_ the two given reference points, this means the
137
+ returned solutions will not include solutions corresponding to the original reference points.
138
+
139
+ Args:
140
+ problem (Problem): the problem being solved.
141
+ solution_1 (dict[str, VariableType]): the first of the reference points between which the intermediate
142
+ solutions are to be generated.
143
+ solution_2 (dict[str, VariableType]): the second of the reference points between which the intermediate
144
+ solutions are to be generated.
145
+ num_desired (int): the number of desired intermediate solutions to be generated. Must be at least `1`.
146
+ scalarization_options (dict | None, optional): optional kwargs passed to the scalarization function.
147
+ Defaults to None.
148
+ solver (BaseSolver | None, optional): solver used to solve the problem.
149
+ If not given, an appropriate solver will be automatically determined based on the features of `problem`.
150
+ Defaults to None.
151
+ solver_options (SolverOptions | None, optional): optional options passed
152
+ to the `solver`. Ignored if `solver` is `None`.
153
+ Defaults to None.
154
+
155
+ Returns:
156
+ list[SolverResults]: a list with the projected intermediate solutions as
157
+ `SolverResults` objects.
158
+ """
159
+ if int(num_desired) < 1:
160
+ msg = f"The given number of desired intermediate ({num_desired=}) solutions must be at least 1."
161
+ raise ReferencePointError(msg)
162
+
163
+ init_solver = guess_best_solver(problem) if solver is None else solver
164
+ _solver_options = None if solver_options is None or solver is None else solver_options
165
+
166
+ # Find intermediate solutions by dividing the distance between the two Pareto points into num_desired+1 steps,
167
+ # calculate the solutions found in between the points, because we don't want to find the original solutions
168
+ intermediate_solutions = []
169
+
170
+ for i in range(num_desired):
171
+ rp = {
172
+ key: ((i + 1) * solution_1[key] + (num_desired - i) * solution_2[key]) / (num_desired + 1)
173
+ for key in solution_1.keys() # noqa: SIM118
174
+ }
175
+ # add scalarization
176
+ add_asf = add_asf_diff if problem.is_twice_differentiable else add_asf_nondiff
177
+ asf_problem, target = add_asf(problem, "target", rp, **(scalarization_options or {}))
178
+
179
+ solver = init_solver(asf_problem, _solver_options)
180
+
181
+ # solve and store results
182
+ result: SolverResults = solver.solve(target)
183
+
184
+ intermediate_solutions.append(result)
185
+
186
+ return intermediate_solutions
@@ -28,11 +28,13 @@ __all__ = [
28
28
  "Simulator",
29
29
  "TensorConstant",
30
30
  "TensorVariable",
31
+ "Url",
31
32
  "unflatten_variable_array",
32
33
  "Variable",
33
34
  "VariableDimensionEnum",
34
35
  "VariableDomainTypeEnum",
35
36
  "VariableType",
37
+ "Tensor",
36
38
  "VariableTypeEnum",
37
39
  "variable_dimension_enumerate",
38
40
  ]
@@ -59,14 +61,16 @@ from .schema import (
59
61
  Problem,
60
62
  ScalarizationFunction,
61
63
  Simulator,
64
+ Tensor,
62
65
  TensorConstant,
63
66
  TensorVariable,
67
+ Url,
64
68
  Variable,
65
69
  VariableDomainTypeEnum,
66
70
  VariableType,
67
71
  VariableTypeEnum,
68
72
  )
69
- from .simulator_evaluator import Evaluator
73
+ from .simulator_evaluator import SimulatorEvaluator
70
74
  from .sympy_evaluator import SympyEvaluator
71
75
  from .utils import (
72
76
  flatten_variable_dict,
@@ -0,0 +1,18 @@
1
+ """Export of the external module."""
2
+
3
+ from .core import ProviderParams
4
+ from .pymoo_provider import PymooProblemParams, PymooProvider, create_pymoo_problem
5
+ from .runtime import get_registry, get_resolver, register_provider, supported_schemes
6
+
7
+ # register default providers here
8
+ register_provider("pymoo", PymooProvider())
9
+
10
+ __all__ = [
11
+ "ProviderParams",
12
+ "PymooProblemParams",
13
+ "create_pymoo_problem",
14
+ "get_registry",
15
+ "get_resolver",
16
+ "register_provider",
17
+ "supported_schemes",
18
+ ]