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.
- desdeo/__init__.py +8 -8
- desdeo/adm/ADMAfsar.py +551 -0
- desdeo/adm/ADMChen.py +414 -0
- desdeo/adm/BaseADM.py +119 -0
- desdeo/adm/__init__.py +11 -0
- desdeo/api/README.md +73 -0
- desdeo/api/__init__.py +15 -0
- desdeo/api/app.py +50 -0
- desdeo/api/config.py +90 -0
- desdeo/api/config.toml +64 -0
- desdeo/api/db.py +27 -0
- desdeo/api/db_init.py +85 -0
- desdeo/api/db_models.py +164 -0
- desdeo/api/malaga_db_init.py +27 -0
- desdeo/api/models/__init__.py +266 -0
- desdeo/api/models/archive.py +23 -0
- desdeo/api/models/emo.py +128 -0
- desdeo/api/models/enautilus.py +69 -0
- desdeo/api/models/gdm/gdm_aggregate.py +139 -0
- desdeo/api/models/gdm/gdm_base.py +69 -0
- desdeo/api/models/gdm/gdm_score_bands.py +114 -0
- desdeo/api/models/gdm/gnimbus.py +138 -0
- desdeo/api/models/generic.py +104 -0
- desdeo/api/models/generic_states.py +401 -0
- desdeo/api/models/nimbus.py +158 -0
- desdeo/api/models/preference.py +128 -0
- desdeo/api/models/problem.py +717 -0
- desdeo/api/models/reference_point_method.py +18 -0
- desdeo/api/models/session.py +49 -0
- desdeo/api/models/state.py +463 -0
- desdeo/api/models/user.py +52 -0
- desdeo/api/models/utopia.py +25 -0
- desdeo/api/routers/_EMO.backup +309 -0
- desdeo/api/routers/_NAUTILUS.py +245 -0
- desdeo/api/routers/_NAUTILUS_navigator.py +233 -0
- desdeo/api/routers/_NIMBUS.py +765 -0
- desdeo/api/routers/__init__.py +5 -0
- desdeo/api/routers/emo.py +497 -0
- desdeo/api/routers/enautilus.py +237 -0
- desdeo/api/routers/gdm/gdm_aggregate.py +234 -0
- desdeo/api/routers/gdm/gdm_base.py +420 -0
- desdeo/api/routers/gdm/gdm_score_bands/gdm_score_bands_manager.py +398 -0
- desdeo/api/routers/gdm/gdm_score_bands/gdm_score_bands_routers.py +377 -0
- desdeo/api/routers/gdm/gnimbus/gnimbus_manager.py +698 -0
- desdeo/api/routers/gdm/gnimbus/gnimbus_routers.py +591 -0
- desdeo/api/routers/generic.py +233 -0
- desdeo/api/routers/nimbus.py +705 -0
- desdeo/api/routers/problem.py +307 -0
- desdeo/api/routers/reference_point_method.py +93 -0
- desdeo/api/routers/session.py +100 -0
- desdeo/api/routers/test.py +16 -0
- desdeo/api/routers/user_authentication.py +520 -0
- desdeo/api/routers/utils.py +187 -0
- desdeo/api/routers/utopia.py +230 -0
- desdeo/api/schema.py +100 -0
- desdeo/api/tests/__init__.py +0 -0
- desdeo/api/tests/conftest.py +151 -0
- desdeo/api/tests/test_enautilus.py +330 -0
- desdeo/api/tests/test_models.py +1179 -0
- desdeo/api/tests/test_routes.py +1075 -0
- desdeo/api/utils/_database.py +263 -0
- desdeo/api/utils/_logger.py +29 -0
- desdeo/api/utils/database.py +36 -0
- desdeo/api/utils/emo_database.py +40 -0
- desdeo/core.py +34 -0
- desdeo/emo/__init__.py +159 -0
- desdeo/emo/hooks/archivers.py +188 -0
- desdeo/emo/methods/EAs.py +541 -0
- desdeo/emo/methods/__init__.py +0 -0
- desdeo/emo/methods/bases.py +12 -0
- desdeo/emo/methods/templates.py +111 -0
- desdeo/emo/operators/__init__.py +1 -0
- desdeo/emo/operators/crossover.py +1282 -0
- desdeo/emo/operators/evaluator.py +114 -0
- desdeo/emo/operators/generator.py +459 -0
- desdeo/emo/operators/mutation.py +1224 -0
- desdeo/emo/operators/scalar_selection.py +202 -0
- desdeo/emo/operators/selection.py +1778 -0
- desdeo/emo/operators/termination.py +286 -0
- desdeo/emo/options/__init__.py +108 -0
- desdeo/emo/options/algorithms.py +435 -0
- desdeo/emo/options/crossover.py +164 -0
- desdeo/emo/options/generator.py +131 -0
- desdeo/emo/options/mutation.py +260 -0
- desdeo/emo/options/repair.py +61 -0
- desdeo/emo/options/scalar_selection.py +66 -0
- desdeo/emo/options/selection.py +127 -0
- desdeo/emo/options/templates.py +383 -0
- desdeo/emo/options/termination.py +143 -0
- desdeo/explanations/__init__.py +6 -0
- desdeo/explanations/explainer.py +100 -0
- desdeo/explanations/utils.py +90 -0
- desdeo/gdm/__init__.py +22 -0
- desdeo/gdm/gdmtools.py +45 -0
- desdeo/gdm/score_bands.py +114 -0
- desdeo/gdm/voting_rules.py +50 -0
- desdeo/mcdm/__init__.py +41 -0
- desdeo/mcdm/enautilus.py +338 -0
- desdeo/mcdm/gnimbus.py +484 -0
- desdeo/mcdm/nautili.py +345 -0
- desdeo/mcdm/nautilus.py +477 -0
- desdeo/mcdm/nautilus_navigator.py +656 -0
- desdeo/mcdm/nimbus.py +417 -0
- desdeo/mcdm/pareto_navigator.py +269 -0
- desdeo/mcdm/reference_point_method.py +186 -0
- desdeo/problem/__init__.py +83 -0
- desdeo/problem/evaluator.py +561 -0
- desdeo/problem/external/__init__.py +18 -0
- desdeo/problem/external/core.py +356 -0
- desdeo/problem/external/pymoo_provider.py +266 -0
- desdeo/problem/external/runtime.py +44 -0
- desdeo/problem/gurobipy_evaluator.py +562 -0
- desdeo/problem/infix_parser.py +341 -0
- desdeo/problem/json_parser.py +944 -0
- desdeo/problem/pyomo_evaluator.py +487 -0
- desdeo/problem/schema.py +1829 -0
- desdeo/problem/simulator_evaluator.py +348 -0
- desdeo/problem/sympy_evaluator.py +244 -0
- desdeo/problem/testproblems/__init__.py +88 -0
- desdeo/problem/testproblems/benchmarks_server.py +120 -0
- desdeo/problem/testproblems/binh_and_korn_problem.py +88 -0
- desdeo/problem/testproblems/cake_problem.py +185 -0
- desdeo/problem/testproblems/dmitry_forest_problem_discrete.py +71 -0
- desdeo/problem/testproblems/dtlz2_problem.py +102 -0
- desdeo/problem/testproblems/forest_problem.py +283 -0
- desdeo/problem/testproblems/knapsack_problem.py +163 -0
- desdeo/problem/testproblems/mcwb_problem.py +831 -0
- desdeo/problem/testproblems/mixed_variable_dimenrions_problem.py +83 -0
- desdeo/problem/testproblems/momip_problem.py +172 -0
- desdeo/problem/testproblems/multi_valued_constraints.py +119 -0
- desdeo/problem/testproblems/nimbus_problem.py +143 -0
- desdeo/problem/testproblems/pareto_navigator_problem.py +89 -0
- desdeo/problem/testproblems/re_problem.py +492 -0
- desdeo/problem/testproblems/river_pollution_problems.py +440 -0
- desdeo/problem/testproblems/rocket_injector_design_problem.py +140 -0
- desdeo/problem/testproblems/simple_problem.py +351 -0
- desdeo/problem/testproblems/simulator_problem.py +92 -0
- desdeo/problem/testproblems/single_objective.py +289 -0
- desdeo/problem/testproblems/spanish_sustainability_problem.py +945 -0
- desdeo/problem/testproblems/zdt_problem.py +274 -0
- desdeo/problem/utils.py +245 -0
- desdeo/tools/GenerateReferencePoints.py +181 -0
- desdeo/tools/__init__.py +120 -0
- desdeo/tools/desc_gen.py +22 -0
- desdeo/tools/generics.py +165 -0
- desdeo/tools/group_scalarization.py +3090 -0
- desdeo/tools/gurobipy_solver_interfaces.py +258 -0
- desdeo/tools/indicators_binary.py +117 -0
- desdeo/tools/indicators_unary.py +362 -0
- desdeo/tools/interaction_schema.py +38 -0
- desdeo/tools/intersection.py +54 -0
- desdeo/tools/iterative_pareto_representer.py +99 -0
- desdeo/tools/message.py +265 -0
- desdeo/tools/ng_solver_interfaces.py +199 -0
- desdeo/tools/non_dominated_sorting.py +134 -0
- desdeo/tools/patterns.py +283 -0
- desdeo/tools/proximal_solver.py +99 -0
- desdeo/tools/pyomo_solver_interfaces.py +477 -0
- desdeo/tools/reference_vectors.py +229 -0
- desdeo/tools/scalarization.py +2065 -0
- desdeo/tools/scipy_solver_interfaces.py +454 -0
- desdeo/tools/score_bands.py +627 -0
- desdeo/tools/utils.py +388 -0
- desdeo/tools/visualizations.py +67 -0
- desdeo/utopia_stuff/__init__.py +0 -0
- desdeo/utopia_stuff/data/1.json +15 -0
- desdeo/utopia_stuff/data/2.json +13 -0
- desdeo/utopia_stuff/data/3.json +15 -0
- desdeo/utopia_stuff/data/4.json +17 -0
- desdeo/utopia_stuff/data/5.json +15 -0
- desdeo/utopia_stuff/from_json.py +40 -0
- desdeo/utopia_stuff/reinit_user.py +38 -0
- desdeo/utopia_stuff/utopia_db_init.py +212 -0
- desdeo/utopia_stuff/utopia_problem.py +403 -0
- desdeo/utopia_stuff/utopia_problem_old.py +415 -0
- desdeo/utopia_stuff/utopia_reference_solutions.py +79 -0
- desdeo-2.1.0.dist-info/METADATA +186 -0
- desdeo-2.1.0.dist-info/RECORD +180 -0
- {desdeo-1.2.dist-info → desdeo-2.1.0.dist-info}/WHEEL +1 -1
- desdeo-2.1.0.dist-info/licenses/LICENSE +21 -0
- desdeo-1.2.dist-info/METADATA +0 -16
- 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()
|