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
@@ -0,0 +1,398 @@
1
+ """GDM Score Bands manager implementation."""
2
+
3
+ import copy
4
+
5
+ import polars as pl
6
+ from sqlmodel import Session, select
7
+
8
+ from desdeo.api.models import (
9
+ GDMSCOREBandFinalSelection,
10
+ GDMSCOREBandInformation,
11
+ Group,
12
+ GroupIteration,
13
+ ProblemDB,
14
+ User,
15
+ )
16
+ from desdeo.api.routers.gdm.gdm_base import GroupManager, ManagerError
17
+ from desdeo.gdm.score_bands import SCOREBandsGDMConfig, SCOREBandsGDMResult, score_bands_gdm
18
+ from desdeo.gdm.voting_rules import consensus_rule, majority_rule
19
+ from desdeo.tools.score_bands import score_json
20
+
21
+
22
+ class GDMScoreBandsManager(GroupManager):
23
+ """The group manager implementation for GDM Score Bands."""
24
+
25
+ def __init__(self, group_id: int, db_session: Session):
26
+ """Initialize the group manager.
27
+
28
+ Args:
29
+ group_id (int): id of the group of this manager.
30
+ db_session (Session): database session from which the discrete representation of a problem is fetched.
31
+ """
32
+ super().__init__(group_id, db_session)
33
+ # LOAD THE DISCRETE REPRESENTATION
34
+ group: Group = db_session.exec(select(Group).where(Group.id == group_id)).first()
35
+ problem: ProblemDB = db_session.exec(select(ProblemDB).where(ProblemDB.id == group.problem_id)).first()
36
+ if problem.discrete_representation is None:
37
+ raise ManagerError("The group's discrete representation does not exist!")
38
+ self.discrete_representation = problem.discrete_representation
39
+ db_session.close()
40
+
41
+ async def run_method(self, user_id: int, data: str):
42
+ """Method runner implementation for GDM Score Bands.
43
+
44
+ Args:
45
+ user_id (int): The user's ID this data is from.
46
+ data (str): The data itself. To be validated.
47
+ WE MIGHT NOT EVEN NEED THIS!!
48
+ """
49
+ async with self.lock:
50
+ await self.send_message(
51
+ "THIS METHOD IS USED THROUGH THE APPROPRIATE HTTP ENDPOINTS!", self.sockets[user_id]
52
+ )
53
+
54
+ async def vote(self, user: User, group: Group, voted_index: int, session: Session):
55
+ """A method for voting on a specific band.
56
+
57
+ Use this from an actual HTTP endpoint and not in a
58
+ stupid websocket way like in GNIMBUS. Every time this is
59
+ operated, send all connected websockets info on the voting.
60
+
61
+ Args:
62
+ user (User): The user that sent the vote
63
+ group (Group): The group the user belongs to and generally the one we're dealing with
64
+ voted_index (int): the vote
65
+ session: (Session) the database session.
66
+ """
67
+ async with self.lock: # not sure if async lock is all that necessary but here we go anyways.
68
+ group_iteration = session.exec(
69
+ select(GroupIteration).where(GroupIteration.id == group.head_iteration_id)
70
+ ).first()
71
+ if group_iteration is None:
72
+ raise ManagerError("No such Group Iteration! Did you initialize this group?")
73
+ info_container = copy.deepcopy(group_iteration.info_container)
74
+ # if user.id in info_container.user_confirms:
75
+ # raise ManagerError("User has already confirmed they want to move on!")
76
+ info_container.user_votes[str(user.id)] = voted_index
77
+ group_iteration.info_container = info_container
78
+ session.add(group_iteration)
79
+ session.commit()
80
+ session.refresh(group_iteration)
81
+
82
+ # Then update preferences dictionary.
83
+ await self.broadcast("UPDATE: A vote has been cast.")
84
+
85
+ async def confirm(self, user: User, group: Group, session: Session):
86
+ """A method for a user to indicate that we could move forward with clustering anew.
87
+
88
+ After everyone has hit this endpoint, do the following shenanigans:
89
+ 1. Filter the active solution indices using the voting result.
90
+ 2. Create a new GroupIteration.
91
+ 3. Re-cluster the active solutions and put the result into the
92
+ info_container item of the newly created GroupIteration.
93
+ 4. Send all connected websockets info that we've got some hot
94
+ new data to update the UI with. Then use some other endpoint to
95
+ fetch that data.
96
+
97
+ Args:
98
+ user (User): The user
99
+ group (Group): The group
100
+ session (Session): The database session.
101
+ """
102
+ async with self.lock:
103
+ if user.id not in group.user_ids:
104
+ raise ManagerError(
105
+ detail=f"User with ID {user.id} is not part of group with ID {group.id}",
106
+ )
107
+ group_iteration = session.exec(
108
+ select(GroupIteration).where(GroupIteration.id == group.head_iteration_id)
109
+ ).first()
110
+ if group_iteration is None:
111
+ raise ManagerError("No group iterations! Did you initialize this group?")
112
+
113
+ info_container = copy.deepcopy(group_iteration.info_container)
114
+ if str(user.id) not in info_container.user_votes:
115
+ raise ManagerError("User hasn't voted! Cannot confirm!")
116
+ # if user.id in info_container.user_confirms:
117
+ raise ManagerError("User has already confirmed they want to move on!")
118
+ if user.id not in info_container.user_confirms:
119
+ info_container.user_confirms.append(user.id)
120
+ group_iteration.info_container = info_container
121
+ session.add(group_iteration)
122
+ session.commit()
123
+ session.refresh(group_iteration)
124
+
125
+ # After everyone's done, filter etc.
126
+ for uid in group.user_ids:
127
+ # Check if user is not in the list of confirms
128
+ if uid not in info_container.user_confirms:
129
+ return
130
+
131
+ # We're in consensus reaching phase
132
+ if info_container.method == "gdm-score-bands":
133
+ # Seems like every user wants to move on.
134
+ statement = select(GroupIteration).where(GroupIteration.group_id == group.id)
135
+ iterations: list[GroupIteration] = session.exec(statement).all()
136
+ state: list[SCOREBandsGDMResult] = [
137
+ iteration.info_container.score_bands_result
138
+ for iteration in iterations
139
+ if iteration.info_container.method == "gdm-score-bands"
140
+ ]
141
+
142
+ # USE Bhupinder's score bands stuff.
143
+ score_bands_config = SCOREBandsGDMConfig(
144
+ score_bands_config=info_container.score_bands_config.score_bands_config,
145
+ from_iteration=state[-1].iteration, # the ID from the latest iteration.
146
+ )
147
+
148
+ # Get them discrete reprs
149
+ discrete_repr = self.discrete_representation
150
+
151
+ # Make sure that there are enough solutions for re-clustering
152
+ votes = group_iteration.info_container.user_votes
153
+ winners = consensus_rule(votes, score_bands_config.minimum_votes)
154
+ relevant_ids = state[-1].relevant_ids
155
+ clustering = state[-1].score_bands_result.clusters
156
+
157
+ # threshold to decide whether to move to the decision phase
158
+ solution_number_threshold = 10
159
+
160
+ if (
161
+ len([x[0] for x in zip(relevant_ids, clustering, strict=True) if x[1] in winners])
162
+ <= solution_number_threshold
163
+ ):
164
+ # There are less than 10 solutions, so move to the decision phase.
165
+
166
+ obj_keys = list(discrete_repr.objective_values)
167
+ var_keys = list(discrete_repr.variable_values)
168
+
169
+ objs = pl.DataFrame(discrete_repr.objective_values).with_row_index(name="index_")
170
+ varis = pl.DataFrame(discrete_repr.variable_values).with_row_index(name="index_")
171
+ indices = pl.DataFrame({"index_": relevant_ids, "cluster_": clustering}).filter(
172
+ pl.col("cluster_").is_in(winners)
173
+ )
174
+ objs = indices.join(other=objs, how="left", left_on="index_", right_on="index_").select(obj_keys)
175
+ varis = indices.join(other=varis, how="left", left_on="index_", right_on="index_").select(var_keys)
176
+
177
+ info_container = GDMSCOREBandFinalSelection(
178
+ user_votes={},
179
+ user_confirms=[],
180
+ solution_variables=varis.to_dict(),
181
+ solution_objectives=objs.to_dict(),
182
+ winner_solution_variables={},
183
+ winner_solution_objectives={},
184
+ )
185
+
186
+ else:
187
+ # Case in which we have more than 10 solutions
188
+ discrete_repr = discrete_repr.objective_values
189
+ objective_keys = list(discrete_repr)
190
+ objs = pl.DataFrame(discrete_repr).with_row_index()
191
+ objs = objs.select(objective_keys)
192
+
193
+ result: list[SCOREBandsGDMResult] = score_bands_gdm(
194
+ data=objs, config=score_bands_config, state=state, votes=votes
195
+ )
196
+
197
+ # store necessary data to the database. Currently all
198
+ # "voting" related is null bc no voting has happened
199
+ info_container = GDMSCOREBandInformation(
200
+ user_votes={},
201
+ user_confirms=[],
202
+ score_bands_config=score_bands_config,
203
+ score_bands_result=result[-1],
204
+ )
205
+
206
+ # Add group iteration and related stuff, then set new iteration to head.
207
+ new_iteration: GroupIteration = GroupIteration(
208
+ group_id=group.id,
209
+ problem_id=group.problem_id,
210
+ info_container=info_container,
211
+ notified={},
212
+ state_id=None,
213
+ parent_id=group.head_iteration_id,
214
+ parent=group_iteration,
215
+ )
216
+
217
+ session.add(new_iteration)
218
+ session.commit()
219
+ session.refresh(new_iteration)
220
+
221
+ group.head_iteration_id = new_iteration.id
222
+ session.add(group)
223
+ session.commit()
224
+ session.refresh(group)
225
+
226
+ await self.broadcast("UPDATE: A new iteration has begun.")
227
+
228
+ # We're in decision phase.
229
+ elif info_container.method == "gdm-score-bands-final":
230
+ winner = majority_rule(info_container.user_votes) # A different rule perhaps?
231
+ # if len(winners) > 1:
232
+ # pass # TODO: minimize distance or whatever
233
+
234
+ varis = info_container.solution_variables
235
+ vari_keys = list(varis)
236
+ objs = info_container.solution_objectives
237
+ obj_keys = list(objs)
238
+
239
+ vari_d = {}
240
+ for key in vari_keys:
241
+ vari_d[key] = varis[key][winner]
242
+
243
+ obj_d = {}
244
+ for key in obj_keys:
245
+ obj_d[key] = objs[key][winner]
246
+
247
+ info_container.winner_solution_variables = vari_d
248
+ info_container.winner_solution_objectives = obj_d
249
+
250
+ group_iteration.info_container = info_container
251
+
252
+ session.add(group_iteration)
253
+ session.commit()
254
+
255
+ async def revert(self, user: User, group: Group, session: Session, group_iteration_number: int):
256
+ """Revert to a different iteration.
257
+
258
+ Args:
259
+ user (User): Current user
260
+ group (Group): Current group
261
+ session (Session): database session
262
+ group_iteration_number (int): the group iteration to which we want to revert.
263
+
264
+ Raises:
265
+ ManagerError
266
+ """
267
+ async with self.lock:
268
+ group_iteration = session.exec(
269
+ select(GroupIteration).where(GroupIteration.id == group.head_iteration_id)
270
+ ).first()
271
+ if group_iteration is None:
272
+ raise ManagerError("No group iterations! Did you initialize this group?")
273
+
274
+ statement = select(GroupIteration).where(GroupIteration.group_id == group.id)
275
+ iterations: list[GroupIteration] = session.exec(statement).all()
276
+
277
+ target_group_iteration: GroupIteration = next([i for i in iterations if i.id == group_iteration_number])
278
+ if target_group_iteration.info_container.method == "gdm-score-bands-final":
279
+ raise ManagerError("We can only revert to a score bands iteration.")
280
+
281
+ state: list[SCOREBandsGDMResult] = [
282
+ iteration.info_container.score_bands_result
283
+ for iteration in iterations
284
+ if iteration.info_container.method == "gdm-score-bands"
285
+ ]
286
+
287
+ # The parent ID will be the latest state's iteration
288
+ prev_id = state[-1].iteration
289
+ # The results remain the same as in the target
290
+ result = target_group_iteration.info_container.score_bands_result
291
+ result.previous_iteration = prev_id
292
+ result.iteration = prev_id + 1
293
+
294
+ # Essentially create a new iteration.
295
+ info_container = GDMSCOREBandInformation(
296
+ user_votes={},
297
+ user_confirms=[],
298
+ score_bands_config=target_group_iteration.info_container.score_bands_config,
299
+ score_bands_result=result,
300
+ )
301
+
302
+ # Add group iteration and related stuff, then set new iteration to head.
303
+ new_iteration: GroupIteration = GroupIteration(
304
+ group_id=group.id,
305
+ problem_id=group.problem_id,
306
+ info_container=info_container,
307
+ notified={},
308
+ state_id=None,
309
+ parent_id=group.head_iteration_id,
310
+ parent=group_iteration,
311
+ )
312
+
313
+ session.add(new_iteration)
314
+ session.commit()
315
+ session.refresh(new_iteration)
316
+
317
+ group.head_iteration_id = new_iteration.id
318
+ session.add(group)
319
+ session.commit()
320
+ session.refresh(group)
321
+
322
+ await self.broadcast("UPDATE: Iteration reverted.")
323
+
324
+ async def configure(self, group: Group, config: SCOREBandsGDMConfig, session: Session):
325
+ """Configure the SCORE Bands process.
326
+
327
+ Args:
328
+ user (User): Actually might not be necessary...
329
+ group (Group): The group whom the configuration concerns.
330
+ config (SCOREBandsGDMConfig): The configuration.
331
+ session (Session): The database session.
332
+ """
333
+ async with self.lock:
334
+ group_iteration = session.exec(
335
+ select(GroupIteration).where(GroupIteration.id == group.head_iteration_id)
336
+ ).first()
337
+ if group_iteration is None:
338
+ raise ManagerError("No group iterations! Did you initialize this group?")
339
+
340
+ if group_iteration.info_container.method == "gdm-score-bands-final":
341
+ raise ManagerError("Cannot reconfigure in a non SCORE Bands phase!")
342
+
343
+ statement = select(GroupIteration).where(GroupIteration.group_id == group.id)
344
+ iterations: list[GroupIteration] = session.exec(statement).all()
345
+ state: list[SCOREBandsGDMResult] = [
346
+ iteration.info_container.score_bands_result
347
+ for iteration in iterations
348
+ if iteration.info_container.method == "gdm-score-bands"
349
+ ]
350
+
351
+ relevant_indices = state[-1].relevant_ids
352
+ iteration_number = state[-1].iteration
353
+
354
+ index_df = pl.DataFrame({"index": relevant_indices})
355
+
356
+ discrete_repr = self.discrete_representation.objective_values
357
+ objective_keys = list(discrete_repr)
358
+ objs_df = pl.DataFrame(discrete_repr).with_row_index()
359
+
360
+ # Filter
361
+ objs_df = index_df.join(how="left", left_on="index", right_on="index", other=objs_df)
362
+
363
+ objs_df = objs_df.select(objective_keys)
364
+
365
+ score_bands_result = score_json(data=objs_df, options=config.score_bands_config)
366
+ score_bands_gdm_result = SCOREBandsGDMResult(
367
+ score_bands_result=score_bands_result,
368
+ relevant_ids=relevant_indices,
369
+ iteration=iteration_number + 1,
370
+ previous_iteration=iteration_number,
371
+ )
372
+
373
+ # Essentially create a new iteration.
374
+ info_container = GDMSCOREBandInformation(
375
+ user_votes={}, user_confirms=[], score_bands_config=config, score_bands_result=score_bands_gdm_result
376
+ )
377
+
378
+ # Add group iteration and related stuff, then set new iteration to head.
379
+ new_iteration: GroupIteration = GroupIteration(
380
+ group_id=group.id,
381
+ problem_id=group.problem_id,
382
+ info_container=info_container,
383
+ notified={},
384
+ state_id=None,
385
+ parent_id=group.head_iteration_id,
386
+ parent=group_iteration,
387
+ )
388
+
389
+ session.add(new_iteration)
390
+ session.commit()
391
+ session.refresh(new_iteration)
392
+
393
+ group.head_iteration_id = new_iteration.id
394
+ session.add(group)
395
+ session.commit()
396
+ session.refresh(group)
397
+
398
+ await self.broadcast("UPDATE: Reconfigured SCORE Bands.")