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.
Files changed (182) hide show
  1. desdeo/__init__.py +8 -8
  2. desdeo/adm/ADMAfsar.py +551 -0
  3. desdeo/adm/ADMChen.py +414 -0
  4. desdeo/adm/BaseADM.py +119 -0
  5. desdeo/adm/__init__.py +11 -0
  6. desdeo/api/README.md +73 -0
  7. desdeo/api/__init__.py +15 -0
  8. desdeo/api/app.py +50 -0
  9. desdeo/api/config.py +90 -0
  10. desdeo/api/config.toml +64 -0
  11. desdeo/api/db.py +27 -0
  12. desdeo/api/db_init.py +85 -0
  13. desdeo/api/db_models.py +164 -0
  14. desdeo/api/malaga_db_init.py +27 -0
  15. desdeo/api/models/__init__.py +266 -0
  16. desdeo/api/models/archive.py +23 -0
  17. desdeo/api/models/emo.py +128 -0
  18. desdeo/api/models/enautilus.py +69 -0
  19. desdeo/api/models/gdm/gdm_aggregate.py +139 -0
  20. desdeo/api/models/gdm/gdm_base.py +69 -0
  21. desdeo/api/models/gdm/gdm_score_bands.py +114 -0
  22. desdeo/api/models/gdm/gnimbus.py +138 -0
  23. desdeo/api/models/generic.py +104 -0
  24. desdeo/api/models/generic_states.py +401 -0
  25. desdeo/api/models/nimbus.py +158 -0
  26. desdeo/api/models/preference.py +128 -0
  27. desdeo/api/models/problem.py +717 -0
  28. desdeo/api/models/reference_point_method.py +18 -0
  29. desdeo/api/models/session.py +49 -0
  30. desdeo/api/models/state.py +463 -0
  31. desdeo/api/models/user.py +52 -0
  32. desdeo/api/models/utopia.py +25 -0
  33. desdeo/api/routers/_EMO.backup +309 -0
  34. desdeo/api/routers/_NAUTILUS.py +245 -0
  35. desdeo/api/routers/_NAUTILUS_navigator.py +233 -0
  36. desdeo/api/routers/_NIMBUS.py +765 -0
  37. desdeo/api/routers/__init__.py +5 -0
  38. desdeo/api/routers/emo.py +497 -0
  39. desdeo/api/routers/enautilus.py +237 -0
  40. desdeo/api/routers/gdm/gdm_aggregate.py +234 -0
  41. desdeo/api/routers/gdm/gdm_base.py +420 -0
  42. desdeo/api/routers/gdm/gdm_score_bands/gdm_score_bands_manager.py +398 -0
  43. desdeo/api/routers/gdm/gdm_score_bands/gdm_score_bands_routers.py +377 -0
  44. desdeo/api/routers/gdm/gnimbus/gnimbus_manager.py +698 -0
  45. desdeo/api/routers/gdm/gnimbus/gnimbus_routers.py +591 -0
  46. desdeo/api/routers/generic.py +233 -0
  47. desdeo/api/routers/nimbus.py +705 -0
  48. desdeo/api/routers/problem.py +307 -0
  49. desdeo/api/routers/reference_point_method.py +93 -0
  50. desdeo/api/routers/session.py +100 -0
  51. desdeo/api/routers/test.py +16 -0
  52. desdeo/api/routers/user_authentication.py +520 -0
  53. desdeo/api/routers/utils.py +187 -0
  54. desdeo/api/routers/utopia.py +230 -0
  55. desdeo/api/schema.py +100 -0
  56. desdeo/api/tests/__init__.py +0 -0
  57. desdeo/api/tests/conftest.py +151 -0
  58. desdeo/api/tests/test_enautilus.py +330 -0
  59. desdeo/api/tests/test_models.py +1179 -0
  60. desdeo/api/tests/test_routes.py +1075 -0
  61. desdeo/api/utils/_database.py +263 -0
  62. desdeo/api/utils/_logger.py +29 -0
  63. desdeo/api/utils/database.py +36 -0
  64. desdeo/api/utils/emo_database.py +40 -0
  65. desdeo/core.py +34 -0
  66. desdeo/emo/__init__.py +159 -0
  67. desdeo/emo/hooks/archivers.py +188 -0
  68. desdeo/emo/methods/EAs.py +541 -0
  69. desdeo/emo/methods/__init__.py +0 -0
  70. desdeo/emo/methods/bases.py +12 -0
  71. desdeo/emo/methods/templates.py +111 -0
  72. desdeo/emo/operators/__init__.py +1 -0
  73. desdeo/emo/operators/crossover.py +1282 -0
  74. desdeo/emo/operators/evaluator.py +114 -0
  75. desdeo/emo/operators/generator.py +459 -0
  76. desdeo/emo/operators/mutation.py +1224 -0
  77. desdeo/emo/operators/scalar_selection.py +202 -0
  78. desdeo/emo/operators/selection.py +1778 -0
  79. desdeo/emo/operators/termination.py +286 -0
  80. desdeo/emo/options/__init__.py +108 -0
  81. desdeo/emo/options/algorithms.py +435 -0
  82. desdeo/emo/options/crossover.py +164 -0
  83. desdeo/emo/options/generator.py +131 -0
  84. desdeo/emo/options/mutation.py +260 -0
  85. desdeo/emo/options/repair.py +61 -0
  86. desdeo/emo/options/scalar_selection.py +66 -0
  87. desdeo/emo/options/selection.py +127 -0
  88. desdeo/emo/options/templates.py +383 -0
  89. desdeo/emo/options/termination.py +143 -0
  90. desdeo/explanations/__init__.py +6 -0
  91. desdeo/explanations/explainer.py +100 -0
  92. desdeo/explanations/utils.py +90 -0
  93. desdeo/gdm/__init__.py +22 -0
  94. desdeo/gdm/gdmtools.py +45 -0
  95. desdeo/gdm/score_bands.py +114 -0
  96. desdeo/gdm/voting_rules.py +50 -0
  97. desdeo/mcdm/__init__.py +41 -0
  98. desdeo/mcdm/enautilus.py +338 -0
  99. desdeo/mcdm/gnimbus.py +484 -0
  100. desdeo/mcdm/nautili.py +345 -0
  101. desdeo/mcdm/nautilus.py +477 -0
  102. desdeo/mcdm/nautilus_navigator.py +656 -0
  103. desdeo/mcdm/nimbus.py +417 -0
  104. desdeo/mcdm/pareto_navigator.py +269 -0
  105. desdeo/mcdm/reference_point_method.py +186 -0
  106. desdeo/problem/__init__.py +83 -0
  107. desdeo/problem/evaluator.py +561 -0
  108. desdeo/problem/external/__init__.py +18 -0
  109. desdeo/problem/external/core.py +356 -0
  110. desdeo/problem/external/pymoo_provider.py +266 -0
  111. desdeo/problem/external/runtime.py +44 -0
  112. desdeo/problem/gurobipy_evaluator.py +562 -0
  113. desdeo/problem/infix_parser.py +341 -0
  114. desdeo/problem/json_parser.py +944 -0
  115. desdeo/problem/pyomo_evaluator.py +487 -0
  116. desdeo/problem/schema.py +1829 -0
  117. desdeo/problem/simulator_evaluator.py +348 -0
  118. desdeo/problem/sympy_evaluator.py +244 -0
  119. desdeo/problem/testproblems/__init__.py +88 -0
  120. desdeo/problem/testproblems/benchmarks_server.py +120 -0
  121. desdeo/problem/testproblems/binh_and_korn_problem.py +88 -0
  122. desdeo/problem/testproblems/cake_problem.py +185 -0
  123. desdeo/problem/testproblems/dmitry_forest_problem_discrete.py +71 -0
  124. desdeo/problem/testproblems/dtlz2_problem.py +102 -0
  125. desdeo/problem/testproblems/forest_problem.py +283 -0
  126. desdeo/problem/testproblems/knapsack_problem.py +163 -0
  127. desdeo/problem/testproblems/mcwb_problem.py +831 -0
  128. desdeo/problem/testproblems/mixed_variable_dimenrions_problem.py +83 -0
  129. desdeo/problem/testproblems/momip_problem.py +172 -0
  130. desdeo/problem/testproblems/multi_valued_constraints.py +119 -0
  131. desdeo/problem/testproblems/nimbus_problem.py +143 -0
  132. desdeo/problem/testproblems/pareto_navigator_problem.py +89 -0
  133. desdeo/problem/testproblems/re_problem.py +492 -0
  134. desdeo/problem/testproblems/river_pollution_problems.py +440 -0
  135. desdeo/problem/testproblems/rocket_injector_design_problem.py +140 -0
  136. desdeo/problem/testproblems/simple_problem.py +351 -0
  137. desdeo/problem/testproblems/simulator_problem.py +92 -0
  138. desdeo/problem/testproblems/single_objective.py +289 -0
  139. desdeo/problem/testproblems/spanish_sustainability_problem.py +945 -0
  140. desdeo/problem/testproblems/zdt_problem.py +274 -0
  141. desdeo/problem/utils.py +245 -0
  142. desdeo/tools/GenerateReferencePoints.py +181 -0
  143. desdeo/tools/__init__.py +120 -0
  144. desdeo/tools/desc_gen.py +22 -0
  145. desdeo/tools/generics.py +165 -0
  146. desdeo/tools/group_scalarization.py +3090 -0
  147. desdeo/tools/gurobipy_solver_interfaces.py +258 -0
  148. desdeo/tools/indicators_binary.py +117 -0
  149. desdeo/tools/indicators_unary.py +362 -0
  150. desdeo/tools/interaction_schema.py +38 -0
  151. desdeo/tools/intersection.py +54 -0
  152. desdeo/tools/iterative_pareto_representer.py +99 -0
  153. desdeo/tools/message.py +265 -0
  154. desdeo/tools/ng_solver_interfaces.py +199 -0
  155. desdeo/tools/non_dominated_sorting.py +134 -0
  156. desdeo/tools/patterns.py +283 -0
  157. desdeo/tools/proximal_solver.py +99 -0
  158. desdeo/tools/pyomo_solver_interfaces.py +477 -0
  159. desdeo/tools/reference_vectors.py +229 -0
  160. desdeo/tools/scalarization.py +2065 -0
  161. desdeo/tools/scipy_solver_interfaces.py +454 -0
  162. desdeo/tools/score_bands.py +627 -0
  163. desdeo/tools/utils.py +388 -0
  164. desdeo/tools/visualizations.py +67 -0
  165. desdeo/utopia_stuff/__init__.py +0 -0
  166. desdeo/utopia_stuff/data/1.json +15 -0
  167. desdeo/utopia_stuff/data/2.json +13 -0
  168. desdeo/utopia_stuff/data/3.json +15 -0
  169. desdeo/utopia_stuff/data/4.json +17 -0
  170. desdeo/utopia_stuff/data/5.json +15 -0
  171. desdeo/utopia_stuff/from_json.py +40 -0
  172. desdeo/utopia_stuff/reinit_user.py +38 -0
  173. desdeo/utopia_stuff/utopia_db_init.py +212 -0
  174. desdeo/utopia_stuff/utopia_problem.py +403 -0
  175. desdeo/utopia_stuff/utopia_problem_old.py +415 -0
  176. desdeo/utopia_stuff/utopia_reference_solutions.py +79 -0
  177. desdeo-2.1.0.dist-info/METADATA +186 -0
  178. desdeo-2.1.0.dist-info/RECORD +180 -0
  179. {desdeo-1.2.dist-info → desdeo-2.1.0.dist-info}/WHEEL +1 -1
  180. desdeo-2.1.0.dist-info/licenses/LICENSE +21 -0
  181. desdeo-1.2.dist-info/METADATA +0 -16
  182. desdeo-1.2.dist-info/RECORD +0 -4
@@ -0,0 +1,698 @@
1
+ """GNIMBUS group manager implementation. Handles varying paths of the GNIMBUS method."""
2
+
3
+ import copy
4
+ import json
5
+ import logging
6
+ import sys
7
+ from typing import Any
8
+
9
+ import numpy as np
10
+ from pydantic import ValidationError
11
+ from sqlmodel import Session, select
12
+
13
+ from desdeo.api.models import (
14
+ BaseGroupInfoContainer,
15
+ EndProcessPreference,
16
+ GNIMBUSEndState,
17
+ GNIMBUSOptimizationState,
18
+ GNIMBUSVotingState,
19
+ Group,
20
+ GroupIteration,
21
+ OptimizationPreference,
22
+ ProblemDB,
23
+ ReferencePoint,
24
+ StateDB,
25
+ VotingPreference,
26
+ )
27
+ from desdeo.api.routers.gdm.gdm_base import GroupManager
28
+ from desdeo.mcdm.gnimbus import solve_group_sub_problems, voting_procedure
29
+ from desdeo.problem import Problem
30
+ from desdeo.tools import SolverResults
31
+ from desdeo.tools.scalarization import ScalarizationError
32
+
33
+ logging.basicConfig(
34
+ stream=sys.stdout, format="[%(filename)s:%(lineno)d] %(levelname)s: %(message)s", level=logging.INFO
35
+ )
36
+ logger = logging.getLogger(__name__)
37
+
38
+
39
+ def compare_values(a: int | float | list[float], b: int | float | list[float]) -> bool:
40
+ """Compare two variables.
41
+
42
+ Args:
43
+ a (int | float | list[float]): variable 1
44
+ b (int | float | list[float]): variable 2
45
+
46
+ Returns:
47
+ bool: whether the values are same (within tolerance)
48
+ """
49
+ # Make sure that the variables are of the same type.
50
+ if type(a) is not type(b):
51
+ return False
52
+
53
+ # check numeric types first.
54
+ if type(a) is int:
55
+ return a == b
56
+
57
+ if type(a) is float:
58
+ return np.isclose(a, b)
59
+
60
+ if type(a) is list:
61
+ return np.allclose(a, b)
62
+
63
+ return False
64
+
65
+
66
+ def compare_value_lists(
67
+ a: list[int | float | list[float]], b: list[int | float | list[float]], variable_keys: list[str]
68
+ ) -> bool:
69
+ """Compare two lists of above possible types together.
70
+
71
+ Args:
72
+ a (list[int | float | list[float]]): list 1
73
+ b (list[int | float | list[float]]): list 2
74
+
75
+ Returns:
76
+ whether the value lists are similar.
77
+ """
78
+ if len(a) is not len(b):
79
+ return False
80
+ complete_list = list(zip(a, b, strict=True))
81
+ equal_values = True
82
+ for _, pair in enumerate(complete_list):
83
+ if not compare_values(pair[0], pair[1]):
84
+ # logger.info(f"These aren't supposedly the same {variable_keys[i]} variables:\n{pair[0]}\n{pair[1]}")
85
+ equal_values = False
86
+
87
+ return equal_values
88
+
89
+
90
+ def filter_duplicates_with_variables(results: list[SolverResults]) -> list[SolverResults]:
91
+ """Filters duplicate solutions bu comparing variables.
92
+
93
+ Args:
94
+ results (list[SolverResults]): The solver results that are coming in from the solver
95
+
96
+ Returns:
97
+ list[SolverResults]: Filtered results
98
+ """
99
+ if len(results) < 2: # noqa: PLR2004
100
+ # The length is 1 or 0; there are no duplicates
101
+ return results
102
+
103
+ # Get varuables from results
104
+ variable_values_list = [res.optimal_variables for res in results]
105
+ # Get variable symbols
106
+ variable_keys = list(variable_values_list[0])
107
+ if "_alpha" in variable_keys:
108
+ variable_keys.remove("_alpha")
109
+ # Get the corresponding values for functions into a list of lists of values
110
+ valuelists = [[dictionary[key] for key in variable_keys] for dictionary in variable_values_list]
111
+ duplicate_indices = []
112
+ for i in range(len(results) - 1):
113
+ for j in range(i + 1, len(results)):
114
+ # If comparing the two solutions, two solutions are close to each other,
115
+ # add the index
116
+ if compare_value_lists(valuelists[i], valuelists[j], variable_keys):
117
+ duplicate_indices.append(i)
118
+
119
+ # Quite the memory hell. See If there's a smarter way to do this
120
+ new_solutions = []
121
+ for i in range(len(results)):
122
+ if i not in duplicate_indices:
123
+ new_solutions.append(results[i])
124
+
125
+ return new_solutions
126
+
127
+
128
+ def filter_duplicates_with_objectives(results: list[SolverResults]) -> list[SolverResults]:
129
+ """Filters away duplicate solutions by comparing all objective values.
130
+
131
+ Args:
132
+ results (list[SolverResults]): The list of solutions that the function filters.
133
+ """
134
+ if len(results) < 2: # noqa: PLR2004
135
+ # The length 1 or 0, there is no duplicates.
136
+ return results
137
+
138
+ # Get the variable values
139
+ objective_values_list = [res.optimal_objectives for res in results]
140
+ # Get the variable symbols
141
+ objective_keys = list(objective_values_list[0])
142
+ # Get the corresponding values for functions into a list of lists of values
143
+ valuelists = [[dictionary[key] for key in objective_keys] for dictionary in objective_values_list]
144
+ # Check duplicate indices
145
+ duplicate_indices = []
146
+ for i in range(len(results) - 1):
147
+ for j in range(i + 1, len(results)):
148
+ # If all values of the objective functions are (nearly) identical, that's a duplicate
149
+ if np.allclose(valuelists[i], valuelists[j]):
150
+ duplicate_indices.append(i)
151
+
152
+ # Quite the memory hell. See If there's a smarter way to do this
153
+ new_solutions = []
154
+ for i in range(len(results)):
155
+ if i not in duplicate_indices:
156
+ new_solutions.append(results[i])
157
+
158
+ return new_solutions
159
+
160
+
161
+ class GNIMBUSManager(GroupManager):
162
+ """The Group NIMBUS manager class.
163
+
164
+ Implements Group NIMBUS functionality to the surrounding GDM framework.
165
+ """
166
+
167
+ # Repeated functionality collected into class methods
168
+ async def set_and_update_preferences(
169
+ self,
170
+ user_id: int,
171
+ preference: Any,
172
+ preferences: BaseGroupInfoContainer,
173
+ session: Session,
174
+ current_iteration: GroupIteration,
175
+ ):
176
+ """Set and update preferences; write them into database."""
177
+ preferences.set_preferences[user_id] = preference
178
+ current_iteration.info_container = preferences
179
+ session.add(current_iteration)
180
+ session.commit()
181
+ session.refresh(current_iteration)
182
+ # print(current_iteration.preferences)
183
+ await self.send_message("Received preferences successfully", self.sockets[user_id])
184
+
185
+ async def check_preferences(
186
+ self,
187
+ user_ids: list[int],
188
+ preferences,
189
+ ) -> bool:
190
+ """Function to check if a preference item has all needed preferences."""
191
+ for user_id in user_ids:
192
+ try:
193
+ # This shouldn't happen but just in case.
194
+ if preferences.set_preferences[user_id] is None:
195
+ logger.info("Not all prefs in!")
196
+ return False
197
+ except KeyError:
198
+ logger.info("Not all prefs in!")
199
+ return False
200
+ return True
201
+
202
+ async def get_state(self, session: Session, current_iteration: GroupIteration):
203
+ """Get the current iteration's substate (GNIMBUSOptimizationState, ...VotingState, etc)."""
204
+ prev_state: StateDB = session.exec(
205
+ select(StateDB).where(StateDB.id == current_iteration.parent.state_id)
206
+ ).first()
207
+ if prev_state is None:
208
+ print("No previous state!")
209
+ return None
210
+ return prev_state.state
211
+
212
+ async def set_state( # noqa: PLR0913
213
+ self,
214
+ session: Session,
215
+ problem_db: ProblemDB,
216
+ optim_state: GNIMBUSOptimizationState | GNIMBUSVotingState | GNIMBUSEndState,
217
+ current_iteration: GroupIteration,
218
+ user_ids: list[int],
219
+ owner_id: int,
220
+ ):
221
+ """Add the state into database."""
222
+ """
223
+ if current_iteration.parent:
224
+ parent_state_id = session.exec(
225
+ select(StateDB).where(StateDB.id == current_iteration.parent.state_id)
226
+ ).first()
227
+ """
228
+
229
+ new_state = StateDB.create(
230
+ database_session=session, problem_id=problem_db.id, session_id=None, parent_id=None, state=optim_state
231
+ )
232
+
233
+ session.add(new_state)
234
+ session.commit()
235
+ session.refresh(new_state)
236
+
237
+ # print(new_state.parent)
238
+
239
+ # Update state id to current iteration
240
+ current_iteration.state_id = new_state.id
241
+ session.add(current_iteration)
242
+ session.commit()
243
+
244
+ # notify connected users that the optimization is done
245
+ g = user_ids
246
+ g.append(owner_id)
247
+ notified = await self.notify(
248
+ user_ids=g, message=f"UPDATE: Please fetch {current_iteration.info_container.method} results."
249
+ )
250
+
251
+ # Update iteration's notifcation database item
252
+ current_iteration.notified = notified
253
+ session.add(current_iteration)
254
+ session.commit()
255
+ session.refresh(current_iteration)
256
+
257
+ async def optimization( # noqa: PLR0911, PLR0913
258
+ self,
259
+ user_id: int,
260
+ data: str,
261
+ session: Session,
262
+ group: Group,
263
+ current_iteration: GroupIteration,
264
+ problem_db: ProblemDB,
265
+ ) -> VotingPreference | EndProcessPreference | None:
266
+ """A function to handle the optimization path.
267
+
268
+ This function is responsible for taking users' preferences and attaching them to database. When all preferences
269
+ are in the database (this is compared against groups users), begin optimizing using core logic's gnimbus
270
+ functions. When optimization is done, put the results to database and create a new preference item, so that
271
+ we can return it, attach it to the next iteration and begin voting/ending iteration. If at any point an error
272
+ rises, we return None
273
+
274
+ Args:
275
+ user_id (int): The user's id. This is comes from the websocket from which the call is made.
276
+ data (str): The data to be validated as reference point.
277
+ session (Session): The database session.
278
+ group (Group): The group.
279
+ current_iteration (GroupIteration): The current group iteration, for accessing preferences and the like.
280
+ problem_db (ProblemDB): The problem that we optimize.
281
+
282
+ Returns:
283
+ VotingPreference | EndProcessPreference | None: Return values; If success, return preference items
284
+ """ # noqa: D202
285
+
286
+ # we know the type of data we need so we'll validate the data as ReferencePoint.
287
+ try:
288
+ preference = ReferencePoint.model_validate(json.loads(data))
289
+ except ValidationError:
290
+ await self.send_message("ERROR: Unable to validate sent data as reference point!", self.sockets[user_id])
291
+ return None
292
+ except json.decoder.JSONDecodeError:
293
+ await self.send_message(
294
+ "ERROR: Unable to decode data; make \
295
+ sure it is formatted properly.",
296
+ self.sockets[user_id],
297
+ )
298
+ return None
299
+ except KeyError:
300
+ await self.send_message(
301
+ "ERROR: Unable to validate data; make sure it is formatted properly.", self.sockets[user_id]
302
+ )
303
+ return None
304
+
305
+ # Update the current GroupIteration's database entry with the new preferences
306
+ # We need to do a deep copy here, otherwise the db entry won't be updated
307
+ preferences: OptimizationPreference = copy.deepcopy(current_iteration.info_container)
308
+ await self.set_and_update_preferences(
309
+ user_id=user_id,
310
+ preference=preference,
311
+ preferences=preferences,
312
+ current_iteration=current_iteration,
313
+ session=session,
314
+ )
315
+
316
+ # Check if all preferences are in
317
+ # There has to be a more elegant way of doing this
318
+ preferences: OptimizationPreference = current_iteration.info_container
319
+ if not await self.check_preferences(
320
+ group.user_ids,
321
+ preferences,
322
+ ):
323
+ return None
324
+
325
+ # If all preferences are in, begin optimization.
326
+ problem: Problem = Problem.from_problemdb(problem_db)
327
+ prefs = current_iteration.info_container.set_preferences
328
+
329
+ formatted_prefs = {}
330
+ for key, item in prefs.items():
331
+ formatted_prefs[key] = item.aspiration_levels
332
+ # logger.info(f"Formatted preferences: {formatted_prefs}")
333
+
334
+ # And here we choose the first result of the previous iteration as the current objectives.
335
+ actual_state = await self.get_state(
336
+ session,
337
+ current_iteration,
338
+ )
339
+ if actual_state is None:
340
+ return None
341
+
342
+ prev_sol = actual_state.solver_results[0].optimal_objectives
343
+
344
+ logger.info(f"starting values: {prev_sol}")
345
+
346
+ user_len = len(group.user_ids)
347
+
348
+ # Begin optimization
349
+ try:
350
+ results: list[SolverResults] = solve_group_sub_problems(
351
+ problem,
352
+ current_objectives=prev_sol,
353
+ reference_points=formatted_prefs,
354
+ phase=current_iteration.info_container.phase,
355
+ )
356
+ logger.info(f"Result amount: {len(results)}")
357
+ if current_iteration.info_container.phase in ["learning", "crp"]:
358
+ logger.info(f"Amount on common solutions before filtering: {len(results[user_len:])}")
359
+ common_results = filter_duplicates_with_objectives(results[user_len:])
360
+ results = results[:user_len] + common_results
361
+ logger.info(f"Amount on common solutions after filtering: {len(results[user_len:])}")
362
+
363
+ logger.info(f"Optimization for group {self.group_id} done.")
364
+
365
+ except ScalarizationError as e:
366
+ await self.broadcast(f"ERROR: Error while scalarizing: {e}")
367
+ logger.exception("Found an error while scalarizing.")
368
+ return None
369
+
370
+ except Exception as e:
371
+ await self.broadcast(f"ERROR: An error occured while optimizing: {e}")
372
+ logger.exception("Found an error when scalarizing.")
373
+ return None
374
+
375
+ # All good, attach results to state and attach that to iteration.
376
+ optim_state = GNIMBUSOptimizationState(reference_points=formatted_prefs, solver_results=results)
377
+
378
+ await self.set_state(session, problem_db, optim_state, current_iteration, group.user_ids, group.owner_id)
379
+
380
+ # DIVERGE THE PATH: if we're in the decision/compromise phase, we'll want to see if everyone
381
+ # is happy with the current solution, so we'll return end process preference.
382
+ if current_iteration.info_container.phase in ["decision", "compromise"]:
383
+ new_preferences = EndProcessPreference(set_preferences={}, success=None)
384
+ # If we're in "learning" or "crp" phases, we return ordinary voting preference
385
+ else:
386
+ new_preferences = VotingPreference(set_preferences={})
387
+
388
+ return new_preferences
389
+
390
+ async def voting( # noqa: PLR0913
391
+ self,
392
+ user_id: int,
393
+ data: str,
394
+ session: Session,
395
+ group: Group,
396
+ current_iteration: GroupIteration,
397
+ problem_db: ProblemDB,
398
+ ) -> OptimizationPreference | None:
399
+ """Handles the voting path of GNIMBUS.
400
+
401
+ Very similar to above "optimization" phase, but instead we validate data as voting index.
402
+ Also returns an "OptimizationPreference" item, to which we attach reference points.
403
+
404
+ Args:
405
+ user_id (int): User's id
406
+ data (str): Data as string, to be validated and an index for voting
407
+ session (Session): database session.
408
+ group (Group): group
409
+ current_iteration (GroupIteration): the current iteration, form which we get the results that we vote on.
410
+ problem_db (ProblemDB): the current problem.
411
+
412
+ Returns:
413
+ OptimizationPreference | None: If we succeed in voting, we return an
414
+ item to which we attach optimization preferences (reference points).
415
+ """ # noqa: D202, D210
416
+
417
+ try:
418
+ preference = int(data)
419
+ if preference > 3 or preference < 0: # noqa: PLR2004
420
+ await self.send_message(
421
+ "ERROR: Voting index out of bounds! Can only vote for 0 to 3.", self.sockets[user_id]
422
+ )
423
+ return None
424
+ except Exception as e:
425
+ print(e)
426
+ await self.send_message("ERROR: Unable to validate sent data as an integer!", self.sockets[user_id])
427
+ return None
428
+
429
+ preferences: VotingPreference = copy.deepcopy(current_iteration.info_container)
430
+ await self.set_and_update_preferences(
431
+ user_id=user_id,
432
+ preference=preference,
433
+ preferences=preferences,
434
+ current_iteration=current_iteration,
435
+ session=session,
436
+ )
437
+
438
+ # Check if all preferences are in
439
+ preferences: VotingPreference = current_iteration.info_container
440
+ if not await self.check_preferences(group.user_ids, preferences):
441
+ return None
442
+
443
+ # format the votes
444
+ formatted_votes = {}
445
+ for key, value in preferences.set_preferences.items():
446
+ formatted_votes[str(key)] = value
447
+
448
+ problem: Problem = Problem.from_problemdb(problem_db)
449
+
450
+ actual_state = await self.get_state(
451
+ session,
452
+ current_iteration,
453
+ )
454
+ if actual_state is None:
455
+ return None
456
+
457
+ results = actual_state.solver_results
458
+
459
+ user_len = len(group.user_ids)
460
+
461
+ # Get the winning results
462
+ winner_result: SolverResults = voting_procedure(
463
+ problem=problem,
464
+ solutions=results[user_len:], # we vote from the common solutions
465
+ votes_idxs=formatted_votes,
466
+ )
467
+
468
+ # Add winning result to database
469
+ vote_state = GNIMBUSVotingState(votes=preferences.set_preferences, solver_results=[winner_result])
470
+
471
+ await self.set_state(session, problem_db, vote_state, current_iteration, group.user_ids, group.owner_id)
472
+
473
+ # Return a OptimizationPreferenceResult so
474
+ # that we can fill it with reference points
475
+ return OptimizationPreference(
476
+ # really? I need to get the phase from the previous iteration?
477
+ phase=current_iteration.parent.info_container.phase,
478
+ set_preferences={},
479
+ )
480
+
481
+ async def ending( # noqa: PLR0913
482
+ self,
483
+ user_id: int,
484
+ data: str,
485
+ session: Session,
486
+ group: Group,
487
+ current_iteration: GroupIteration,
488
+ problem_db: ProblemDB,
489
+ ) -> OptimizationPreference | None:
490
+ """Function to handle the "ending" path.
491
+
492
+ This time it is almost identical to above "voting" path, but we validate data as "bool".
493
+
494
+ Args:
495
+ user_id (int): user's id
496
+ data (str): data to be validated as bool
497
+ session (Session): db session
498
+ group (Group): group
499
+ current_iteration (GroupIteration): the current iteration from which we pull the necessary data.
500
+ problem_db (ProblemDB): the problem.
501
+
502
+ Returns:
503
+ OptimizationPreference | None: If success, we return an optimization preference.
504
+ """
505
+ # logger.info(f"incoming data: {data}")
506
+ try:
507
+ preference: bool = bool(int(data))
508
+ except Exception:
509
+ await self.send_message("ERROR: Unable to validate sent data as an boolean value.", self.sockets[user_id])
510
+ return None
511
+
512
+ preferences: EndProcessPreference = copy.deepcopy(current_iteration.info_container)
513
+ await self.set_and_update_preferences(
514
+ user_id=user_id,
515
+ preference=preference,
516
+ preferences=preferences,
517
+ current_iteration=current_iteration,
518
+ session=session,
519
+ )
520
+ session.refresh(current_iteration)
521
+
522
+ # Check if all preferences are in
523
+ preferences: EndProcessPreference = current_iteration.info_container
524
+ if not await self.check_preferences(
525
+ group.user_ids,
526
+ preferences,
527
+ ):
528
+ return None
529
+
530
+ # All preferences in, let's see what they think.
531
+ all_vote_yes: bool = True
532
+ for uid in group.user_ids:
533
+ if not preferences.set_preferences[uid]:
534
+ all_vote_yes = False
535
+ break
536
+ new_copy_preferences: EndProcessPreference = copy.deepcopy(current_iteration.info_container)
537
+ new_copy_preferences.success = all_vote_yes
538
+ current_iteration.info_container = new_copy_preferences
539
+ session.add(current_iteration)
540
+ session.commit()
541
+ session.refresh(current_iteration)
542
+ print(current_iteration.info_container)
543
+
544
+ actual_state = await self.get_state(
545
+ session,
546
+ current_iteration,
547
+ )
548
+ if actual_state is None:
549
+ return None
550
+
551
+ # We take the result that was voted on (there should be only one)
552
+ results = actual_state.solver_results
553
+
554
+ ending_state = GNIMBUSEndState(
555
+ votes=current_iteration.info_container.set_preferences, solver_results=results, success=all_vote_yes
556
+ )
557
+
558
+ await self.set_state(session, problem_db, ending_state, current_iteration, group.user_ids, group.owner_id)
559
+
560
+ # Return a OptimizationPreferenceResult so
561
+ # that we can fill it with reference points
562
+ return OptimizationPreference(
563
+ phase=current_iteration.parent.info_container.phase,
564
+ set_preferences={},
565
+ )
566
+
567
+ async def run_method(self, user_id: int, data: str, db_session: Session):
568
+ """The method function.
569
+
570
+ Here, the preferences are set (and updated to database). If all preferences are set, optimize and
571
+ update database with results. Then, create new iteration and assign the correct relationships
572
+ between the database entries.
573
+
574
+ The paths can hold whatever code one wants, but if done correctly, should result in updating data
575
+ in the current iteration with preferences and results and after the "step" is done, the group's head
576
+ should be updated to a new iteration, where one could the begin attaching new preferences.
577
+
578
+ The flow of this specific method is the following:
579
+
580
+ 1. phase: learning, method: optimize
581
+ 2. phase: learning, method: voting
582
+ 3. if switching phase to crp,
583
+ go to 4.
584
+ otherwise,
585
+ go to 1.
586
+ 4. phase: crp, method: optimize
587
+ 5. phase: crp, method: voting
588
+ 6. if switching phase to decision,
589
+ go to 7.
590
+ otherwise,
591
+ go to 4.
592
+ 7. phase: decision, method: optimize
593
+ 8. phase: decision, method: end
594
+ 9. if all voted "yes" on 8,
595
+ end the process. (flagged item in database)
596
+ otherwise,
597
+ go to 7.
598
+
599
+ NOTE: There's now an additional phase, "compromise", that functions identically to "decision".
600
+ """
601
+ async with self.lock:
602
+ # Fetch the current iteration
603
+ group = db_session.exec(select(Group).where(Group.id == self.group_id)).first()
604
+ if group is None:
605
+ await self.broadcast(f"ERROR: The group with ID {self.group_id} doesn't exist anymore.")
606
+ db_session.close()
607
+ return
608
+
609
+ current_iteration = db_session.exec(
610
+ select(GroupIteration).where(GroupIteration.id == group.head_iteration_id)
611
+ ).first()
612
+ if current_iteration is None:
613
+ await self.broadcast("ERROR: Problem not initialized! Initialize the problem!")
614
+ db_session.close()
615
+ return
616
+
617
+ # logger.info(f"Current iteration ID: {current_iteration.id}")
618
+
619
+ problem_db: ProblemDB = db_session.exec(select(ProblemDB).where(ProblemDB.id == group.problem_id)).first()
620
+ # This shouldn't be a problem at this point anymore, but
621
+ if problem_db is None:
622
+ await self.broadcast(f"ERROR: There's no problem with ID {group.problem_id}!")
623
+ return
624
+
625
+ new_preferences = None
626
+
627
+ # Diverge into different paths using PreferenceResult method type of the current iteration.
628
+ match current_iteration.info_container.method:
629
+ case "optimization":
630
+ new_preferences = await self.optimization(
631
+ user_id=user_id,
632
+ data=data,
633
+ session=db_session,
634
+ group=group,
635
+ current_iteration=current_iteration,
636
+ problem_db=problem_db,
637
+ )
638
+
639
+ case "voting":
640
+ # Here we could do some voting on the NIMBUS results.
641
+ new_preferences = await self.voting(
642
+ user_id=user_id,
643
+ data=data,
644
+ session=db_session,
645
+ group=group,
646
+ current_iteration=current_iteration,
647
+ problem_db=problem_db,
648
+ )
649
+
650
+ case "end":
651
+ # An ending iteration; naming is a bit odd, but means that using this we can end the process.
652
+ new_preferences = await self.ending(
653
+ user_id=user_id,
654
+ data=data,
655
+ session=db_session,
656
+ group=group,
657
+ current_iteration=current_iteration,
658
+ problem_db=problem_db,
659
+ )
660
+
661
+ case _:
662
+ # throw an error
663
+ new_preferences = None
664
+ return
665
+
666
+ if new_preferences is None:
667
+ db_session.close()
668
+ return
669
+
670
+ # If everything has gone according to keikaku (keikaku means plan), create the next iteration.
671
+ next_iteration = GroupIteration(
672
+ group_id=self.group_id,
673
+ problem_id=current_iteration.problem_id,
674
+ info_container=new_preferences,
675
+ notified={},
676
+ parent_id=current_iteration.id, # Probably redundant to have
677
+ parent=current_iteration, # two connections to parents?
678
+ )
679
+
680
+ db_session.add(next_iteration)
681
+ db_session.commit()
682
+ db_session.refresh(next_iteration)
683
+
684
+ # Update new parent iteration
685
+ children = current_iteration.children.copy()
686
+ children.append(next_iteration)
687
+ current_iteration.children = children
688
+ current_iteration.group_id = self.group_id
689
+ db_session.add(current_iteration)
690
+ db_session.commit()
691
+
692
+ # Update head of the group
693
+ group.head_iteration_id = next_iteration.id
694
+ db_session.add(group)
695
+ db_session.commit()
696
+
697
+ # Close the session
698
+ db_session.close()