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,591 @@
1
+ """GNIMBUS routers.
2
+
3
+ Separated from GNIMBUSManager file for
4
+ A.) Clarity and
5
+ B.) To avoid circular imports, since we need to access the ManagerManager singleton.
6
+ """
7
+
8
+ import copy
9
+ import logging
10
+ import sys
11
+ from typing import Annotated
12
+
13
+ from fastapi import APIRouter, Depends, HTTPException, status
14
+ from fastapi.responses import JSONResponse
15
+ from sqlmodel import Session, select
16
+
17
+ from desdeo.api.db import get_session
18
+ from desdeo.api.models import (
19
+ EndProcessPreference,
20
+ FullIteration,
21
+ GNIMBUSAllIterationsResponse,
22
+ GNIMBUSResultResponse,
23
+ GNIMBUSSwitchPhaseRequest,
24
+ GNIMBUSSwitchPhaseResponse,
25
+ GNIMBUSVotingState,
26
+ Group,
27
+ GroupInfoRequest,
28
+ GroupIteration,
29
+ GroupRevertRequest,
30
+ OptimizationPreference,
31
+ ProblemDB,
32
+ SolutionReference,
33
+ SolutionReferenceLite,
34
+ StateDB,
35
+ User,
36
+ VotingPreference,
37
+ )
38
+ from desdeo.api.routers.gdm.gdm_aggregate import manager
39
+ from desdeo.api.routers.problem import check_solver
40
+ from desdeo.api.routers.user_authentication import get_current_user
41
+ from desdeo.mcdm.nimbus import generate_starting_point
42
+ from desdeo.problem import Problem
43
+
44
+ logging.basicConfig(
45
+ stream=sys.stdout, format="[%(filename)s:%(lineno)d] %(levelname)s: %(message)s", level=logging.INFO
46
+ )
47
+
48
+ not_init_error = HTTPException(detail="Problem has not been initialized!", status_code=status.HTTP_400_BAD_REQUEST)
49
+
50
+ router = APIRouter(prefix="/gnimbus", tags=["GNIMBUS"])
51
+
52
+
53
+ @router.post("/initialize")
54
+ def gnimbus_initialize(
55
+ request: GroupInfoRequest,
56
+ user: Annotated[User, Depends(get_current_user)],
57
+ session: Annotated[Session, Depends(get_session)],
58
+ ):
59
+ """Initialize the problem for GNIMBUS."""
60
+ #Check that all pre-conditions are all right
61
+ group = session.exec(select(Group).where(Group.id == request.group_id)).first()
62
+ if group is None:
63
+ raise HTTPException(detail=f"No group with ID {request.group_id} found!", status_code=status.HTTP_404_NOT_FOUND)
64
+ if not (user.id in group.user_ids or user.id is group.owner_id):
65
+ raise HTTPException(detail="Unauthorized user", status_code=status.HTTP_401_UNAUTHORIZED)
66
+
67
+ head_iteration = session.exec(select(GroupIteration)
68
+ .where(GroupIteration.id == group.head_iteration_id)).first()
69
+ if head_iteration is not None:
70
+ raise HTTPException(detail="Group problem already initialized!", status_code=status.HTTP_400_BAD_REQUEST)
71
+
72
+ problem_db = session.exec(select(ProblemDB).where(ProblemDB.id == group.problem_id)).first()
73
+ if problem_db is None:
74
+ raise HTTPException(
75
+ detail=f"No problem with id {group.problem_id} found!", status_code=status.HTTP_404_NOT_FOUND
76
+ )
77
+
78
+ solver = check_solver(problem_db=problem_db)
79
+
80
+ problem = Problem.from_problemdb(problem_db)
81
+
82
+ # Create the first iteration for the group
83
+ # As if we just voted for a result, but really is just the starting point.
84
+ starting_point = generate_starting_point(problem=problem, solver=solver)
85
+
86
+ gnimbus_state = GNIMBUSVotingState(votes={}, solver_results=[starting_point])
87
+
88
+ # Create init state
89
+ state = StateDB.create(
90
+ database_session=session, problem_id=problem_db.id, session_id=None, parent_id=None, state=gnimbus_state
91
+ )
92
+
93
+ session.add(state)
94
+ session.commit()
95
+ session.refresh(state)
96
+
97
+ # The starting iteration
98
+ start_iteration = GroupIteration(
99
+ problem_id=group.problem_id,
100
+ group_id=group.id,
101
+ info_container=VotingPreference(
102
+ set_preferences={},
103
+ ),
104
+ notified={},
105
+ state_id=state.id,
106
+ parent_id=None,
107
+ parent=None,
108
+ )
109
+
110
+ session.add(start_iteration)
111
+ session.commit()
112
+ session.refresh(start_iteration)
113
+
114
+ # New iteration for continuing; to this we add new preferences and begin building a linked list
115
+ new_iteration = GroupIteration(
116
+ problem_id=start_iteration.problem_id,
117
+ group_id=start_iteration.group_id,
118
+ info_container=OptimizationPreference(
119
+ set_preferences={},
120
+ ),
121
+ notified={},
122
+ parent_id=start_iteration.id,
123
+ parent=start_iteration,
124
+ )
125
+
126
+ session.add(new_iteration)
127
+ session.commit()
128
+ session.refresh(new_iteration)
129
+
130
+ children = start_iteration.children.copy()
131
+ children.append(new_iteration)
132
+ start_iteration.children = children
133
+ session.add(start_iteration)
134
+ group.head_iteration_id = new_iteration.id
135
+ session.add(group)
136
+ session.commit()
137
+
138
+ return JSONResponse(content={"message": f"Group {group.id} initialized."}, status_code=status.HTTP_200_OK)
139
+
140
+
141
+ @router.post("/get_latest_results")
142
+ def get_latest_results(
143
+ request: GroupInfoRequest,
144
+ user: Annotated[User, Depends(get_current_user)],
145
+ session: Annotated[Session, Depends(get_session)],
146
+ ) -> GNIMBUSResultResponse:
147
+ """Get the latest results from group iteration.
148
+
149
+ (OBSOLETE AND OUT OF DATE!)
150
+
151
+ Args:
152
+ request (GroupInfoRequest): essentially just the ID of the group
153
+ user (Annotated[User, Depends(get_current_user)]): Current user
154
+ session (Annotated[Session, Depends(get_session)]): Database session.
155
+
156
+ Returns:
157
+ GNIMBUSResultResponse: A GNIMBUSResultResponse response containing the latest gnimbus results
158
+
159
+ Raises:
160
+ HTTPException: Validation errors or no results
161
+ """
162
+ group: Group = session.exec(select(Group).where(Group.id == request.group_id)).first()
163
+ if group is None:
164
+ raise HTTPException(detail=f"No group with ID {request.group_id} found", status_code=status.HTTP_404_NOT_FOUND)
165
+
166
+ if not (user.id in group.user_ids or user.id is group.owner_id):
167
+ raise HTTPException(detail="Unauthorized user.", status_code=status.HTTP_401_UNAUTHORIZED)
168
+
169
+ nores_exp = HTTPException(detail="No results found!", status_code=status.HTTP_404_NOT_FOUND)
170
+
171
+ head_iteration = session.exec(select(GroupIteration)
172
+ .where(GroupIteration.id == group.head_iteration_id)).first()
173
+
174
+ try:
175
+ iteration = head_iteration.parent
176
+ except Exception as e:
177
+ raise nores_exp from e
178
+
179
+ if iteration is None:
180
+ raise nores_exp
181
+
182
+ state = session.exec(select(StateDB).where(StateDB.id == iteration.state_id)).first()
183
+ if state is None:
184
+ raise nores_exp
185
+
186
+ actual_state = state.state
187
+ preferences = iteration.info_container
188
+
189
+ if type(actual_state) is GNIMBUSVotingState:
190
+ return GNIMBUSResultResponse(
191
+ method="voting",
192
+ phase=iteration.parent.info_container.phase if iteration.parent is not None else "learning",
193
+ preferences=preferences,
194
+ common_results=[SolutionReference(state=state, solution_index=0)],
195
+ user_results=[],
196
+ personal_result_index=None,
197
+ )
198
+
199
+ personal_result_index = None
200
+ for i, item in enumerate(preferences.set_preferences.items()):
201
+ if item[0] == user.id:
202
+ personal_result_index = i
203
+ break
204
+
205
+ if personal_result_index is None:
206
+ raise HTTPException(detail=f"No personal results for user ID {user.id}", status_code=status.HTTP_404_NOT_FOUND)
207
+
208
+ solution_references = []
209
+ for i, _ in enumerate(actual_state.solver_results):
210
+ solution_references.append(SolutionReference(state=state, solution_index=i))
211
+
212
+ return GNIMBUSResultResponse(
213
+ method="optimization",
214
+ phase=iteration.info_container.phase,
215
+ preferences=preferences,
216
+ common_results=solution_references[-4:],
217
+ user_results=solution_references[:-4],
218
+ personal_result_index=personal_result_index,
219
+ )
220
+
221
+
222
+ @router.post("/all_iterations")
223
+ def full_iteration( # noqa: C901, PLR0912
224
+ request: GroupInfoRequest,
225
+ user: Annotated[User, Depends(get_current_user)],
226
+ session: Annotated[Session, Depends(get_session)],
227
+ ) -> GNIMBUSAllIterationsResponse:
228
+ """Get all results from all iterations of the group.
229
+
230
+ Args:
231
+ request (GroupInfoRequest): essentially just the ID of the group
232
+ user (Annotated[User, Depends(get_current_user)]): current user
233
+ session (Annotated[Session, Depends(get_session)]): current session
234
+
235
+ Returns:
236
+ GNIMBUSAllIterationsResponse: A GNIMBUSAllIterationsResponse response
237
+ containing all the results of the iterations. If last iteration was optimization,
238
+ the first iteration is incomplete (i.e. the voting preferences and voting results are missing)
239
+
240
+ Raises:
241
+ HTTPException: Validation errors or no results or no states and such.
242
+ """
243
+ group = session.exec(select(Group).where(Group.id == request.group_id)).first()
244
+ if group is None:
245
+ raise HTTPException(detail=f"No group with ID {request.group_id}!", status_code=status.HTTP_404_NOT_FOUND)
246
+
247
+ if user.id not in group.user_ids and user.id is not group.owner_id:
248
+ raise HTTPException(detail="Unauthorized user", status_code=status.HTTP_401_UNAUTHORIZED)
249
+
250
+ head_iteration = session.exec(select(GroupIteration)
251
+ .where(GroupIteration.id == group.head_iteration_id)).first()
252
+
253
+ try:
254
+ groupiter = head_iteration.parent
255
+ except Exception as e:
256
+ raise not_init_error from e
257
+
258
+ if groupiter is None:
259
+ raise not_init_error
260
+
261
+ full_iterations: list[FullIteration] = []
262
+
263
+ user_len = len(group.user_ids)
264
+
265
+ if groupiter.info_container.method == "optimization":
266
+ # There are no full results because the latest iteration is optimization,
267
+ # so add an incomplete entry to the list to be returned.
268
+ prev_state = session.exec(select(StateDB).where(StateDB.id == groupiter.parent.state_id)).first()
269
+ if prev_state is None:
270
+ raise HTTPException(detail="No state for starting results!", status_code=status.HTTP_404_NOT_FOUND)
271
+
272
+ this_state = session.exec(select(StateDB).where(StateDB.id == groupiter.state_id)).first()
273
+ if this_state is None:
274
+ raise HTTPException(detail="No state in most recent iteration!", status_code=status.HTTP_404_NOT_FOUND)
275
+
276
+ personal_result_index = None
277
+ for i, item in enumerate(groupiter.info_container.set_preferences.items()):
278
+ if item[0] == user.id:
279
+ personal_result_index = i
280
+ break
281
+
282
+ all_results = []
283
+ for i, _ in enumerate(this_state.state.solver_results):
284
+ all_results.append(SolutionReferenceLite(state=this_state, solution_index=i))
285
+
286
+ phase = groupiter.info_container.phase
287
+
288
+ full_iterations.append(
289
+ FullIteration(
290
+ phase=phase,
291
+ optimization_preferences=groupiter.info_container,
292
+ voting_preferences=None,
293
+ starting_result=SolutionReferenceLite(state=prev_state, solution_index=0),
294
+ common_results=all_results if phase in ["decision", "compromise"] else all_results[user_len:],
295
+ user_results=all_results[:user_len],
296
+ personal_result_index=personal_result_index,
297
+ final_result=None,
298
+ )
299
+ )
300
+
301
+ groupiter = groupiter.parent
302
+
303
+ # We're at voting/end method now. Construct an FullIteration item from Voting/Ending an Optimization iterations
304
+ # A bit of a complicated mess, I could have done this in a better manner.
305
+ while groupiter is not None and groupiter.parent is not None and groupiter.parent.parent is not None:
306
+ this_state = session.exec(select(StateDB).where(StateDB.id == groupiter.state_id)).first()
307
+ prev_state = session.exec(select(StateDB).where(StateDB.id == groupiter.parent.state_id)).first()
308
+ first_state = session.exec(select(StateDB).where(StateDB.id == groupiter.parent.parent.state_id)).first()
309
+
310
+ if this_state is None or prev_state is None or first_state is None:
311
+ raise HTTPException(detail="All needed states do not exist!", status_code=status.HTTP_404_NOT_FOUND)
312
+
313
+ all_results = []
314
+ for i, _ in enumerate(prev_state.state.solver_results):
315
+ all_results.append(SolutionReferenceLite(state=prev_state, solution_index=i))
316
+
317
+ personal_result_index = None
318
+ for i, item in enumerate(groupiter.parent.info_container.set_preferences.items()):
319
+ if item[0] == user.id:
320
+ personal_result_index = i
321
+ break
322
+
323
+ phase = groupiter.parent.info_container.phase
324
+
325
+ full_iterations.append(
326
+ FullIteration(
327
+ phase=phase,
328
+ optimization_preferences=groupiter.parent.info_container,
329
+ voting_preferences=groupiter.info_container,
330
+ starting_result=SolutionReferenceLite(state=first_state, solution_index=0),
331
+ common_results=all_results if phase in ["decision", "compromise"] else all_results[user_len:],
332
+ user_results=all_results[:user_len],
333
+ personal_result_index=personal_result_index,
334
+ final_result=SolutionReferenceLite(state=this_state, solution_index=0),
335
+ )
336
+ )
337
+
338
+ groupiter = groupiter.parent.parent
339
+
340
+ # We're at the root, so add the initialization iteration (essentially empty with just a final result)
341
+ if groupiter is not None and groupiter.parent is None:
342
+ this_state = session.exec(select(StateDB).where(StateDB.id == groupiter.state_id)).first()
343
+
344
+ if this_state is None:
345
+ raise HTTPException(detail="Initialization state does not exist!", status_code=status.HTTP_404_NOT_FOUND)
346
+
347
+ full_iterations.append(
348
+ FullIteration(
349
+ phase="init",
350
+ optimization_preferences=None,
351
+ voting_preferences=None,
352
+ starting_result=None,
353
+ common_results=[],
354
+ user_results=[],
355
+ personal_result_index=None,
356
+ final_result=SolutionReferenceLite(state=this_state, solution_index=0),
357
+ )
358
+ )
359
+
360
+ return GNIMBUSAllIterationsResponse(all_full_iterations=full_iterations)
361
+
362
+
363
+ @router.post("/toggle_phase")
364
+ async def switch_phase(
365
+ request: GNIMBUSSwitchPhaseRequest,
366
+ user: Annotated[User, Depends(get_current_user)],
367
+ session: Annotated[Session, Depends(get_session)],
368
+ ) -> GNIMBUSSwitchPhaseResponse:
369
+ """Switch the phase from one to another. "learning", "crp", "decision" and "compromise" phases are allowed."""
370
+ #Preliminary checks etc.
371
+ if request.new_phase not in ["learning", "crp", "decision", "compromise"]:
372
+ raise HTTPException(
373
+ detail=f"Undefined phase: {request.new_phase}! Can only be {['learning', 'crp', 'decision', 'compromise']}",
374
+ status_code=status.HTTP_400_BAD_REQUEST,
375
+ )
376
+ group: Group = session.exec(select(Group).where(Group.id == request.group_id)).first()
377
+ if group is None:
378
+ raise HTTPException(detail=f"No group with ID {request.group_id} found", status_code=status.HTTP_404_NOT_FOUND)
379
+ if user.id is not group.owner_id:
380
+ raise HTTPException(detail="Unauthorized user.", status_code=status.HTTP_401_UNAUTHORIZED)
381
+ iteration = session.exec(select(GroupIteration)
382
+ .where(GroupIteration.id == group.head_iteration_id)).first()
383
+ if iteration is None:
384
+ raise HTTPException(
385
+ detail="There's no iterations! Did you forget to initialize the problem?",
386
+ status_code=status.HTTP_404_NOT_FOUND,
387
+ )
388
+ if iteration.info_container is None:
389
+ raise HTTPException(details="Now this is a funky problem!", status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
390
+ if iteration.info_container.method == "voting":
391
+ raise HTTPException(
392
+ detail="You cannot switch the phase in the middle of the iteration.",
393
+ status_code=status.HTTP_400_BAD_REQUEST,
394
+ )
395
+
396
+ old_phase = iteration.info_container.phase
397
+ new_phase = request.new_phase
398
+
399
+ # Set the phase in the preferences
400
+ preferences = copy.deepcopy(iteration.info_container)
401
+ preferences.phase = new_phase
402
+
403
+ iteration.info_container = preferences
404
+ session.add(iteration)
405
+ session.commit()
406
+ session.refresh(iteration)
407
+
408
+ # get the group manager and notify the connected users that the phase has changed
409
+ gman = await manager.get_group_manager(group_id=group.id, method="gnimbus")
410
+
411
+ await gman.broadcast(f"The phase was changed from {old_phase} to {new_phase}.")
412
+
413
+ return GNIMBUSSwitchPhaseResponse(old_phase=old_phase, new_phase=new_phase)
414
+
415
+
416
+ @router.post("/get_phase")
417
+ def get_phase(
418
+ request: GroupInfoRequest,
419
+ user: Annotated[User, Depends(get_current_user)],
420
+ session: Annotated[Session, Depends(get_session)],
421
+ ) -> JSONResponse:
422
+ """Get the current phase of the group."""
423
+ group: Group = session.exec(select(Group).where(Group.id == request.group_id)).first()
424
+ if group is None:
425
+ raise HTTPException(detail=f"No group with ID {request.group_id} found", status_code=status.HTTP_404_NOT_FOUND)
426
+
427
+ g = group.user_ids
428
+ g.append(group.owner_id)
429
+
430
+ if user.id not in g:
431
+ raise HTTPException(detail="Unauthorized user.", status_code=status.HTTP_401_UNAUTHORIZED)
432
+
433
+ current_iteration= session.exec(select(GroupIteration)
434
+ .where(GroupIteration.id == group.head_iteration_id)).first()
435
+ if current_iteration is None:
436
+ raise not_init_error
437
+
438
+ if current_iteration.info_container.method != "optimization":
439
+ current_iteration = current_iteration.parent
440
+
441
+ return JSONResponse(content={"phase": current_iteration.info_container.phase}, status_code=status.HTTP_200_OK)
442
+
443
+ def get_preference_item(iteration):
444
+ """Returns an empty preference item for adding preferences; The preferences are the next preferences.
445
+
446
+ Args:
447
+ iteration: we consider this iteration and it's preferences.
448
+
449
+ Returns:
450
+ Preference item.
451
+ """
452
+ item = iteration.preferences
453
+ if type(item) is OptimizationPreference:
454
+ if item.phase in ["decision", "compromise"]:
455
+ return EndProcessPreference(
456
+ success=None,
457
+ set_preferences={}
458
+ )
459
+ return VotingPreference(
460
+ set_preferences={}
461
+ )
462
+ return OptimizationPreference(
463
+ phase=iteration.parent.preferences.phase if iteration.parent.preferences.phase is not None else "learning",
464
+ set_preferences={},
465
+ )
466
+
467
+
468
+ @router.post("/revert_iteration")
469
+ async def revert_iteration(
470
+ request: GroupRevertRequest,
471
+ user: Annotated[User, Depends(get_current_user)],
472
+ session: Annotated[Session, Depends(get_session)],
473
+ ) -> JSONResponse:
474
+ """Changes the starting solution of an iteration so in case of emergency the group owner can just change it.
475
+
476
+ Args:
477
+ request (GNIMBUSChangeStartingSolutionRequest): The request containing necessary details to fulfill the change.
478
+ user (Annotated[User, Depends): The current user.
479
+ session (Annotated[Session, Depends): The database session.
480
+
481
+ Raises:
482
+ HTTPException
483
+
484
+ Returns:
485
+ JSONResponse: Response that acknowledges the changes.
486
+ """
487
+ group: Group = session.exec(select(Group).where(Group.id == request.group_id)).first()
488
+ if not group:
489
+ raise HTTPException(
490
+ detail=f"No group with ID {request.group_id}!",
491
+ status_code=status.HTTP_404_NOT_FOUND
492
+ )
493
+ if group.owner_id is not user.id:
494
+ raise HTTPException(
495
+ detail="Unauthorized user!",
496
+ status_code=status.HTTP_401_UNAUTHORIZED
497
+ )
498
+
499
+ current_iteration = session.exec(select(GroupIteration)
500
+ .where(GroupIteration.id == group.head_iteration_id)).first()
501
+
502
+ if not current_iteration:
503
+ raise HTTPException(
504
+ detail="There's no head iteration",
505
+ status_code=status.HTTP_404_NOT_FOUND
506
+ )
507
+ head_iteration_type = type(current_iteration.info_container)
508
+ if head_iteration_type in [VotingPreference, EndProcessPreference]:
509
+ raise HTTPException(
510
+ detail="Complete the iteration before reverting. Sorry for the inconvenience.",
511
+ status_code=status.HTTP_400_BAD_REQUEST
512
+ )
513
+
514
+ # Get the GroupIteration corresponding to the requests state id
515
+ target_iteration = session.exec(select(GroupIteration).where(GroupIteration.state_id == request.state_id)).first()
516
+ if not target_iteration:
517
+ raise HTTPException(
518
+ detail=f"There's no iteration with state ID {request.state_id}!",
519
+ status_code=status.HTTP_404_NOT_FOUND
520
+ )
521
+ target_iteration_type = type(target_iteration.info_container)
522
+ if target_iteration_type == OptimizationPreference:
523
+ raise HTTPException(
524
+ detail="You can only revert to a result of a complete iteration. Sorry for the inconvenience.",
525
+ status_code=status.HTTP_400_BAD_REQUEST
526
+ )
527
+
528
+ # We must "artificially" create some history, so that we can later on fetch stuff seamlessly,
529
+ # without any hiccups or changes to the "all_iterations" endpoint. Essentially we copy two latest
530
+ # iterations to the head of the history.
531
+ new_parent_1 = GroupIteration(
532
+ problem_id=group.problem_id,
533
+ group_id=group.id,
534
+ info_container=target_iteration.parent.info_container.model_copy(),
535
+ notified=target_iteration.parent.notified.copy(),
536
+ state_id=target_iteration.parent.state_id,
537
+ parent_id=current_iteration.parent.id,
538
+ parent=current_iteration.parent
539
+ )
540
+
541
+ session.add(new_parent_1)
542
+ session.commit()
543
+ session.refresh(new_parent_1)
544
+
545
+ new_parent_2 = GroupIteration(
546
+ problem_id=group.problem_id,
547
+ group_id=group.id,
548
+ info_container=target_iteration.info_container.model_copy(),
549
+ notified=target_iteration.notified.copy(),
550
+ state_id=target_iteration.state_id,
551
+ parent_id=new_parent_1.id,
552
+ parent=new_parent_1
553
+ )
554
+
555
+ session.add(new_parent_2)
556
+ session.commit()
557
+ session.refresh(new_parent_2)
558
+
559
+ # New head iteration
560
+ new_head = GroupIteration(
561
+ problem_id=group.problem_id,
562
+ group_id=group.id,
563
+ info_container=OptimizationPreference(
564
+ phase=target_iteration.parent.info_container.phase \
565
+ if target_iteration.parent.info_container.phase is not None else "learning",
566
+ set_preferences={}
567
+ ),
568
+ notified={},
569
+ parent_id=new_parent_2.id,
570
+ parent=new_parent_2,
571
+ )
572
+
573
+ session.add(new_head)
574
+ session.commit()
575
+
576
+ group.head_iteration_id = new_head.id
577
+
578
+ session.add(group)
579
+ session.delete(current_iteration)
580
+ session.commit()
581
+ session.refresh(group)
582
+
583
+ gman = await manager.get_group_manager(group_id=group.id, method="gnimbus")
584
+ await gman.broadcast("UPDATE: Latest iteration reversed by the group owner!")
585
+
586
+ return JSONResponse(
587
+ content={
588
+ "message": "Iteration reversed!",
589
+ },
590
+ status_code=status.HTTP_200_OK
591
+ )