desdeo 2.0.0__py3-none-any.whl → 2.1.1__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 (130) 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 +16 -11
  87. desdeo/problem/evaluator.py +4 -5
  88. desdeo/problem/external/__init__.py +18 -0
  89. desdeo/problem/external/core.py +356 -0
  90. desdeo/problem/external/pymoo_provider.py +266 -0
  91. desdeo/problem/external/runtime.py +44 -0
  92. desdeo/problem/gurobipy_evaluator.py +37 -12
  93. desdeo/problem/infix_parser.py +1 -16
  94. desdeo/problem/json_parser.py +7 -11
  95. desdeo/problem/pyomo_evaluator.py +25 -6
  96. desdeo/problem/schema.py +73 -55
  97. desdeo/problem/simulator_evaluator.py +65 -15
  98. desdeo/problem/testproblems/__init__.py +26 -11
  99. desdeo/problem/testproblems/benchmarks_server.py +120 -0
  100. desdeo/problem/testproblems/cake_problem.py +185 -0
  101. desdeo/problem/testproblems/dmitry_forest_problem_discrete.py +71 -0
  102. desdeo/problem/testproblems/forest_problem.py +77 -69
  103. desdeo/problem/testproblems/multi_valued_constraints.py +119 -0
  104. desdeo/problem/testproblems/{river_pollution_problem.py → river_pollution_problems.py} +28 -22
  105. desdeo/problem/testproblems/single_objective.py +289 -0
  106. desdeo/problem/testproblems/zdt_problem.py +4 -1
  107. desdeo/problem/utils.py +1 -1
  108. desdeo/tools/__init__.py +39 -21
  109. desdeo/tools/desc_gen.py +22 -0
  110. desdeo/tools/generics.py +22 -2
  111. desdeo/tools/group_scalarization.py +3090 -0
  112. desdeo/tools/indicators_binary.py +107 -1
  113. desdeo/tools/indicators_unary.py +3 -16
  114. desdeo/tools/message.py +33 -2
  115. desdeo/tools/non_dominated_sorting.py +4 -3
  116. desdeo/tools/patterns.py +9 -7
  117. desdeo/tools/pyomo_solver_interfaces.py +49 -36
  118. desdeo/tools/reference_vectors.py +118 -351
  119. desdeo/tools/scalarization.py +340 -1413
  120. desdeo/tools/score_bands.py +491 -328
  121. desdeo/tools/utils.py +117 -49
  122. desdeo/tools/visualizations.py +67 -0
  123. desdeo/utopia_stuff/utopia_problem.py +1 -1
  124. desdeo/utopia_stuff/utopia_problem_old.py +1 -1
  125. {desdeo-2.0.0.dist-info → desdeo-2.1.1.dist-info}/METADATA +47 -30
  126. desdeo-2.1.1.dist-info/RECORD +180 -0
  127. {desdeo-2.0.0.dist-info → desdeo-2.1.1.dist-info}/WHEEL +1 -1
  128. desdeo-2.0.0.dist-info/RECORD +0 -120
  129. /desdeo/api/utils/{logger.py → _logger.py} +0 -0
  130. {desdeo-2.0.0.dist-info → desdeo-2.1.1.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,233 @@
1
+ """Defines end-points to access generic functionalities."""
2
+
3
+ from typing import Annotated
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+ from fastapi import APIRouter, Depends, HTTPException, status
8
+ from sqlmodel import Session, select
9
+
10
+ from desdeo.api.db import get_session
11
+ from desdeo.api.models import (
12
+ InteractiveSessionDB,
13
+ IntermediateSolutionRequest,
14
+ IntermediateSolutionState,
15
+ ProblemDB,
16
+ ScoreBandsRequest,
17
+ ScoreBandsResponse,
18
+ SolutionReference,
19
+ StateDB,
20
+ User,
21
+ )
22
+ from desdeo.api.models.generic import GenericIntermediateSolutionResponse
23
+ from desdeo.api.routers.user_authentication import get_current_user
24
+ from desdeo.mcdm.nimbus import solve_intermediate_solutions
25
+ from desdeo.problem import Problem
26
+ from desdeo.tools import SolverResults
27
+ from desdeo.tools.score_bands import calculate_axes_positions, cluster, order_dimensions
28
+
29
+ router = APIRouter(prefix="/method/generic")
30
+
31
+
32
+ @router.post("/intermediate")
33
+ def solve_intermediate(
34
+ request: IntermediateSolutionRequest,
35
+ user: Annotated[User, Depends(get_current_user)],
36
+ session: Annotated[Session, Depends(get_session)],
37
+ ) -> GenericIntermediateSolutionResponse:
38
+ """Solve intermediate solutions between given two solutions."""
39
+ if request.session_id is not None:
40
+ statement = select(InteractiveSessionDB).where(InteractiveSessionDB.id == request.session_id)
41
+ interactive_session = session.exec(statement)
42
+
43
+ if interactive_session is None:
44
+ raise HTTPException(
45
+ status_code=status.HTTP_404_NOT_FOUND,
46
+ detail=f"Could not find interactive session with id={request.session_id}.",
47
+ )
48
+ else:
49
+ # request.session_id is None:
50
+ # use active session instead
51
+ statement = select(InteractiveSessionDB).where(InteractiveSessionDB.id == user.active_session_id)
52
+
53
+ interactive_session = session.exec(statement).first()
54
+
55
+ # query both reference solutions' variable values
56
+ # stored as lit of tuples, first element of each tuple are variables values, second are objective function values
57
+ var_and_obj_values_of_references: list[tuple[dict, dict]] = []
58
+ reference_states = []
59
+ for solution_info in [request.reference_solution_1, request.reference_solution_2]:
60
+ solution_state = session.exec(select(StateDB).where(StateDB.id == solution_info.state_id)).first()
61
+
62
+ if solution_state is None:
63
+ # no StateDB found with the given id
64
+ raise HTTPException(
65
+ status_code=status.HTTP_404_NOT_FOUND,
66
+ detail=f"Could not find a state with the given id{solution_state.state_id}.",
67
+ )
68
+
69
+ reference_states.append(solution_state)
70
+
71
+ try:
72
+ _var_values = solution_state.state.result_variable_values
73
+ var_values = _var_values[solution_info.solution_index]
74
+
75
+ except IndexError as exc:
76
+ raise HTTPException(
77
+ status_code=status.HTTP_400_BAD_REQUEST,
78
+ detail=(
79
+ f"The index {solution_info.solution_index} is out of bounds for results with len={len(_var_values)}"
80
+ ),
81
+ ) from exc
82
+
83
+ try:
84
+ _obj_values = solution_state.state.result_objective_values
85
+ obj_values = _obj_values[solution_info.solution_index]
86
+
87
+ except IndexError as exc:
88
+ raise HTTPException(
89
+ status_code=status.HTTP_400_BAD_REQUEST,
90
+ detail=(
91
+ f"The index {solution_info.solution_index} is out of bounds for results with len={len(_obj_values)}"
92
+ ),
93
+ ) from exc
94
+
95
+ var_and_obj_values_of_references.append((var_values, obj_values))
96
+
97
+ # fetch the problem from the DB
98
+ statement = select(ProblemDB).where(ProblemDB.user_id == user.id, ProblemDB.id == request.problem_id)
99
+ problem_db = session.exec(statement).first()
100
+
101
+ if problem_db is None:
102
+ raise HTTPException(
103
+ status_code=status.HTTP_404_NOT_FOUND,
104
+ detail=f"Problem with id={request.problem_id} could not be found.",
105
+ )
106
+
107
+ problem = Problem.from_problemdb(problem_db)
108
+
109
+ solver_results: list[SolverResults] = solve_intermediate_solutions(
110
+ problem=problem,
111
+ solution_1=var_and_obj_values_of_references[0][0],
112
+ solution_2=var_and_obj_values_of_references[1][0],
113
+ num_desired=request.num_desired,
114
+ scalarization_options=request.scalarization_options,
115
+ solver=request.solver,
116
+ solver_options=request.solver_options,
117
+ )
118
+
119
+ # fetch parent state
120
+ if request.parent_state_id is None:
121
+ # parent state is assumed to be the last state added to the session.
122
+ parent_state = (
123
+ interactive_session.states[-1]
124
+ if (interactive_session is not None and len(interactive_session.states) > 0)
125
+ else None
126
+ )
127
+
128
+ else:
129
+ # request.parent_state_id is not None
130
+ statement = session.select(StateDB).where(StateDB.id == request.parent_state_id)
131
+ parent_state = session.exec(statement).first()
132
+
133
+ if parent_state is None:
134
+ raise HTTPException(
135
+ status_code=status.HTTP_404_NOT_FOUND,
136
+ detail=f"Could not find state with id={request.parent_state_id}",
137
+ )
138
+
139
+ intermediate_state = IntermediateSolutionState(
140
+ scalarization_options=request.scalarization_options,
141
+ context=request.context,
142
+ solver=request.solver,
143
+ solver_options=request.solver_options,
144
+ solver_results=solver_results,
145
+ num_desired=request.num_desired,
146
+ reference_solution_1=var_and_obj_values_of_references[0][1],
147
+ reference_solution_2=var_and_obj_values_of_references[1][1],
148
+ )
149
+
150
+ # create DB state and add it to the DB
151
+ state = StateDB.create(
152
+ database_session=session,
153
+ problem_id=problem_db.id,
154
+ session_id=interactive_session.id if interactive_session is not None else None,
155
+ parent_id=parent_state.id if parent_state is not None else None,
156
+ state=intermediate_state,
157
+ )
158
+
159
+ session.add(state)
160
+ session.commit()
161
+ session.refresh(state)
162
+
163
+ return GenericIntermediateSolutionResponse(
164
+ state_id=state.id,
165
+ reference_solution_1=SolutionReference(
166
+ state=reference_states[0],
167
+ solution_index=request.reference_solution_1.solution_index,
168
+ name=request.reference_solution_1.name,
169
+ ),
170
+ reference_solution_2=SolutionReference(
171
+ state=reference_states[1],
172
+ solution_index=request.reference_solution_2.solution_index,
173
+ name=request.reference_solution_2.name,
174
+ ),
175
+ intermediate_solutions=[
176
+ SolutionReference(state=state, solution_index=i) for i in range(state.state.num_solutions)
177
+ ],
178
+ )
179
+
180
+
181
+ @router.post("/score-bands-obj-data")
182
+ def calculate_score_bands_from_objective_data(
183
+ request: ScoreBandsRequest,
184
+ ) -> ScoreBandsResponse:
185
+ """Calculate SCORE bands parameters from objective data."""
186
+ try:
187
+ # Convert input data to pandas DataFrame
188
+ data = pd.DataFrame(request.data, columns=request.objs)
189
+
190
+ # Calculate correlation matrix and objective order
191
+ corr, obj_order = order_dimensions(data, use_absolute_corr=request.use_absolute_corr)
192
+
193
+ # Calculate axis positions and signs
194
+ ordered_data, axis_dist, axis_signs = calculate_axes_positions(
195
+ data,
196
+ obj_order,
197
+ corr,
198
+ dist_parameter=request.dist_parameter,
199
+ distance_formula=request.distance_formula,
200
+ )
201
+
202
+ # Handle flip_axes parameter - if flip_axes is False, don't use axis signs
203
+ if not request.flip_axes:
204
+ axis_signs = None
205
+
206
+ # Perform clustering
207
+ groups = cluster(
208
+ ordered_data,
209
+ algorithm=request.clustering_algorithm,
210
+ score=request.clustering_score,
211
+ )
212
+
213
+ # Translate minimum group to 1 (as done in the notebook)
214
+ groups = groups - np.min(groups) + 1
215
+
216
+ # Convert numpy arrays to lists for JSON serialization
217
+ # Handle potential None values and ensure proper type conversion
218
+ return ScoreBandsResponse(
219
+ groups=groups.tolist() if hasattr(groups, "tolist") else list(groups),
220
+ axis_dist=(axis_dist.tolist() if hasattr(axis_dist, "tolist") else list(axis_dist)),
221
+ axis_signs=(
222
+ axis_signs.tolist()
223
+ if axis_signs is not None and hasattr(axis_signs, "tolist")
224
+ else (list(axis_signs) if axis_signs is not None else None)
225
+ ),
226
+ obj_order=(obj_order.tolist() if hasattr(obj_order, "tolist") else list(obj_order)),
227
+ )
228
+
229
+ except Exception as e:
230
+ raise HTTPException(
231
+ status_code=status.HTTP_400_BAD_REQUEST,
232
+ detail=f"Error calculating SCORE bands parameters: {e!r}",
233
+ ) from e