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,309 @@
1
+ """Router for evolutionary multiobjective optimization (EMO) methods."""
2
+
3
+ from datetime import datetime
4
+ from typing import Annotated, Dict, List, Optional
5
+
6
+ from fastapi import APIRouter, Depends, HTTPException, status
7
+ from sqlalchemy.orm import Session
8
+ from sqlmodel import select
9
+
10
+ from desdeo.api.db import get_session
11
+ from desdeo.api.models.archive import (
12
+ UserSavedEMOResults,
13
+ )
14
+ from desdeo.api.models.EMO import (
15
+ EMOSaveRequest,
16
+ EMOSolveRequest,
17
+ )
18
+ from desdeo.api.models.preference import (
19
+ NonPreferredSolutions,
20
+ PreferenceBase,
21
+ PreferenceDB,
22
+ PreferredRanges,
23
+ PreferredSolutions,
24
+ ReferencePoint,
25
+ )
26
+ from desdeo.api.models.problem import ProblemDB
27
+ from desdeo.api.models.session import InteractiveSessionDB
28
+ from desdeo.api.models.state import EMOSaveState, EMOState, StateDB
29
+ from desdeo.api.models.user import User
30
+ from desdeo.api.routers.user_authentication import get_current_user
31
+ from desdeo.api.utils.database import user_save_solutions
32
+ from desdeo.api.utils.emo_database import _convert_dataframe_to_dict_list
33
+ from desdeo.emo.hooks.archivers import NonDominatedArchive
34
+ from desdeo.emo.methods.EAs import nsga3, rvea
35
+ from desdeo.problem import Problem
36
+
37
+ router = APIRouter(prefix="/method/emo", tags=["evolutionary"])
38
+
39
+
40
+ @router.post("/solve")
41
+ def start_emo_optimization(
42
+ request: EMOSolveRequest,
43
+ user: Annotated[User, Depends(get_current_user)],
44
+ session: Annotated[Session, Depends(get_session)],
45
+ ) -> EMOState:
46
+ """Start interactive evolutionary multiobjective optimization."""
47
+
48
+ # Handle session logic
49
+ if request.session_id is not None:
50
+ statement = select(InteractiveSessionDB).where(InteractiveSessionDB.id == request.session_id)
51
+ interactive_session = session.exec(statement).first()
52
+
53
+ if interactive_session is None:
54
+ raise HTTPException(
55
+ status_code=status.HTTP_404_NOT_FOUND,
56
+ detail=f"Could not find interactive session with id={request.session_id}.",
57
+ )
58
+ else:
59
+ # Use active session
60
+ statement = select(InteractiveSessionDB).where(InteractiveSessionDB.id == user.active_session_id)
61
+ interactive_session = session.exec(statement).first()
62
+
63
+ # Fetch problem from DB
64
+ statement = select(ProblemDB).where(ProblemDB.user_id == user.id, ProblemDB.id == request.problem_id)
65
+ problem_db = session.exec(statement).first()
66
+
67
+ if problem_db is None:
68
+ raise HTTPException(
69
+ status_code=status.HTTP_404_NOT_FOUND,
70
+ detail=f"Problem with id={request.problem_id} could not be found.",
71
+ )
72
+
73
+ # Convert ProblemDB to Problem object
74
+ problem = Problem.from_problemdb(problem_db)
75
+
76
+ # Build reference vector options based on preference type
77
+ reference_vector_options = _build_reference_vector_options(request.preference, request.number_of_vectors)
78
+
79
+ # Create solver and publisher
80
+ if request.method == "RVEA":
81
+ solver, publisher = rvea(problem=problem, reference_vector_options=reference_vector_options)
82
+ elif request.method == "NSGA3":
83
+ solver, publisher = nsga3(problem=problem, reference_vector_options=reference_vector_options)
84
+ else:
85
+ raise HTTPException(
86
+ status_code=status.HTTP_400_BAD_REQUEST,
87
+ detail=f"Unsupported method: {request.method}. Supported methods are 'NSGA3' and 'RVEA'.",
88
+ )
89
+
90
+ # Add archive if requested
91
+ archive = None
92
+ if request.use_archive:
93
+ archive = NonDominatedArchive(problem=problem, publisher=publisher)
94
+ publisher.auto_subscribe(archive)
95
+
96
+ # Run optimization
97
+ emo_results = solver()
98
+
99
+ # Convert DataFrames to dictionaries for solutions
100
+ solutions_dict = _convert_dataframe_to_dict_list(getattr(emo_results, "solutions", None))
101
+
102
+ # Convert DataFrames to dictionaries for outputs
103
+ outputs_dict = _convert_dataframe_to_dict_list(getattr(emo_results, "outputs", None))
104
+
105
+ # Create DB preference
106
+ preference_db = PreferenceDB(user_id=user.id, problem_id=problem_db.id, preference=request.preference)
107
+
108
+ session.add(preference_db)
109
+ session.commit()
110
+ session.refresh(preference_db)
111
+
112
+ # Handle parent state
113
+ if request.parent_state_id is None:
114
+ parent_state = (
115
+ interactive_session.states[-1]
116
+ if (interactive_session is not None and len(interactive_session.states) > 0)
117
+ else None
118
+ )
119
+ else:
120
+ statement = select(StateDB).where(StateDB.id == request.parent_state_id)
121
+ parent_state = session.exec(statement).first()
122
+
123
+ if parent_state is None:
124
+ raise HTTPException(
125
+ status_code=status.HTTP_404_NOT_FOUND,
126
+ detail=f"Could not find state with id={request.parent_state_id}",
127
+ )
128
+
129
+ # Create EMO state
130
+ emo_state = EMOState(
131
+ method=request.method, # Use the method directly (already uppercase)
132
+ max_evaluations=request.max_evaluations,
133
+ number_of_vectors=request.number_of_vectors,
134
+ use_archive=request.use_archive,
135
+ solutions=solutions_dict,
136
+ outputs=outputs_dict,
137
+ )
138
+
139
+ # Create DB state
140
+ state = StateDB(
141
+ problem_id=problem_db.id,
142
+ preference_id=preference_db.id,
143
+ session_id=interactive_session.id if interactive_session is not None else None,
144
+ parent_id=parent_state.id if parent_state is not None else None,
145
+ state=emo_state, # Convert to dict for JSON serialization
146
+ )
147
+
148
+ session.add(state)
149
+ session.commit()
150
+ session.refresh(state)
151
+
152
+ return emo_state
153
+
154
+
155
+ @router.post("/save")
156
+ def save(
157
+ request: EMOSaveRequest,
158
+ user: Annotated[User, Depends(get_current_user)],
159
+ session: Annotated[Session, Depends(get_session)],
160
+ ) -> EMOSaveState:
161
+ """Save solutions."""
162
+ if request.session_id is not None:
163
+ statement = select(InteractiveSessionDB).where(InteractiveSessionDB.id == request.session_id)
164
+ interactive_session = session.exec(statement)
165
+
166
+ if interactive_session is None:
167
+ raise HTTPException(
168
+ status_code=status.HTTP_404_NOT_FOUND,
169
+ detail=f"Could not find interactive session with id={request.session_id}.",
170
+ )
171
+ else:
172
+ # request.session_id is None:
173
+ # use active session instead
174
+ statement = select(InteractiveSessionDB).where(InteractiveSessionDB.id == user.active_session_id)
175
+
176
+ interactive_session = session.exec(statement).first()
177
+
178
+ # fetch parent state
179
+ if request.parent_state_id is None:
180
+ # parent state is assumed to be the last state added to the session.
181
+ parent_state = (
182
+ interactive_session.states[-1]
183
+ if (interactive_session is not None and len(interactive_session.states) > 0)
184
+ else None
185
+ )
186
+
187
+ else:
188
+ # request.parent_state_id is not None
189
+ statement = session.select(StateDB).where(StateDB.id == request.parent_state_id)
190
+ parent_state = session.exec(statement).first()
191
+
192
+ if parent_state is None:
193
+ raise HTTPException(
194
+ status_code=status.HTTP_404_NOT_FOUND,
195
+ detail=f"Could not find state with id={request.parent_state_id}",
196
+ )
197
+
198
+ # save solver results for state in SolverResults format just for consistency (dont save name field to state)
199
+ # Get values from parent state if available, otherwise use defaults
200
+ max_evaluations = 1000
201
+ number_of_vectors = 20
202
+ use_archive = True
203
+
204
+ if parent_state is not None and isinstance(parent_state.state, EMOState):
205
+ max_evaluations = parent_state.state.max_evaluations
206
+ number_of_vectors = parent_state.state.number_of_vectors
207
+ use_archive = parent_state.state.use_archive
208
+
209
+ save_state = EMOSaveState(
210
+ method=(parent_state.state.method if parent_state else "EMO"), # Get from parent or default
211
+ max_evaluations=max_evaluations,
212
+ number_of_vectors=number_of_vectors,
213
+ use_archive=use_archive,
214
+ problem_id=request.problem_id,
215
+ saved_solutions=[solution.to_emo_results() for solution in request.solutions],
216
+ solutions=[solution.model_dump() for solution in request.solutions], # Original solutions from request
217
+ )
218
+
219
+ # create DB state
220
+ state = StateDB(
221
+ problem_id=request.problem_id,
222
+ session_id=interactive_session.id if interactive_session is not None else None,
223
+ parent_id=parent_state.id if parent_state is not None else None,
224
+ state=save_state,
225
+ )
226
+ # save solutions to the user's archive and add state to the DB
227
+ user_save_solutions(state, request.solutions, user.id, session)
228
+
229
+ return save_state
230
+
231
+
232
+ @router.get("/saved-solutions")
233
+ def get_saved_solutions(
234
+ user: Annotated[User, Depends(get_current_user)],
235
+ session: Annotated[Session, Depends(get_session)],
236
+ ):
237
+ """Get all saved solutions for the current user."""
238
+ from desdeo.api.models.archive import UserSavedSolutionDB
239
+
240
+ # Query saved solutions for the current user
241
+ statement = select(UserSavedSolutionDB).where(UserSavedSolutionDB.user_id == user.id)
242
+ saved_solutions = session.exec(statement).all()
243
+
244
+ # Convert to response format
245
+ results = []
246
+ for solution in saved_solutions:
247
+ results.append(
248
+ {
249
+ "id": solution.id,
250
+ "name": solution.name,
251
+ "variable_values": solution.variable_values,
252
+ "objective_values": solution.objective_values,
253
+ "constraint_values": solution.constraint_values,
254
+ "extra_func_values": solution.extra_func_values,
255
+ "problem_id": solution.problem_id,
256
+ }
257
+ )
258
+
259
+ return results
260
+
261
+
262
+ # Helper functions
263
+ def _build_reference_vector_options(preference: PreferenceBase, number_of_vectors: int) -> Dict:
264
+ """Build reference vector options based on preference type."""
265
+
266
+ base_options = {
267
+ "number_of_vectors": number_of_vectors,
268
+ }
269
+
270
+ # Convert the preference dict to the correct object type
271
+ if isinstance(preference, dict):
272
+ preference_type = preference.get("preference_type")
273
+ if preference_type == "reference_point":
274
+ from desdeo.api.models.preference import ReferencePoint
275
+
276
+ preference = ReferencePoint.model_validate(preference)
277
+ elif preference_type == "preferred_solutions":
278
+ from desdeo.api.models.preference import PreferredSolutions
279
+
280
+ preference = PreferredSolutions.model_validate(preference)
281
+ elif preference_type == "non_preferred_solutions":
282
+ from desdeo.api.models.preference import NonPreferredSolutions
283
+
284
+ preference = NonPreferredSolutions.model_validate(preference)
285
+ elif preference_type == "preferred_ranges":
286
+ from desdeo.api.models.preference import PreferredRanges
287
+
288
+ preference = PreferredRanges.model_validate(preference)
289
+
290
+ # Now handle the properly typed preference object
291
+ if hasattr(preference, "aspiration_levels"):
292
+ base_options["interactive_adaptation"] = "reference_point"
293
+ base_options["reference_point"] = preference.aspiration_levels
294
+ elif hasattr(preference, "preferred_solutions"):
295
+ base_options["interactive_adaptation"] = "preferred_solutions"
296
+ base_options["preferred_solutions"] = preference.preferred_solutions
297
+ elif hasattr(preference, "non_preferred_solutions"):
298
+ base_options["interactive_adaptation"] = "non_preferred_solutions"
299
+ base_options["non_preferred_solutions"] = preference.non_preferred_solutions
300
+ elif hasattr(preference, "preferred_ranges"):
301
+ base_options["interactive_adaptation"] = "preferred_ranges"
302
+ base_options["preferred_ranges"] = preference.preferred_ranges
303
+ else:
304
+ raise HTTPException(
305
+ status_code=400,
306
+ detail=f"Unsupported preference type: {type(preference)} with preference_type: {getattr(preference, 'preference_type', 'unknown')}",
307
+ )
308
+
309
+ return base_options
@@ -150,7 +150,10 @@ def init_nimbus(
150
150
 
151
151
  # If there are no solutions, generate a starting point for NIMBUS
152
152
  if not solutions:
153
- start_result = generate_starting_point(problem=problem, solver=available_solvers[solver] if solver else None)
153
+ start_result = generate_starting_point(
154
+ problem=problem,
155
+ solver=available_solvers[solver]["constructor"] if solver else None
156
+ )
154
157
  save_results_to_db(
155
158
  db=db, user_id=user.index, request=init_request, results=[start_result], previous_solutions=solutions
156
159
  )
@@ -214,7 +217,7 @@ def iterate(
214
217
  ),
215
218
  reference_point=dict(zip([obj.symbol for obj in problem.objectives], request.preference, strict=True)),
216
219
  num_desired=request.num_solutions,
217
- solver=available_solvers[solver] if solver else None,
220
+ solver=available_solvers[solver]["constructor"] if solver else None,
218
221
  scalarization_options={"rho": 0.001, "delta": 0.001},
219
222
  )
220
223
 
@@ -277,7 +280,7 @@ def intermediate(
277
280
  solution_1=dict(zip(problem.objectives, request.reference_solution_1, strict=True)),
278
281
  solution_2=dict(zip(problem.objectives, request.reference_solution_2, strict=True)),
279
282
  num_desired=request.num_solutions,
280
- solver=available_solvers[solver] if solver else None,
283
+ solver=available_solvers[solver]["constructor"] if solver else None,
281
284
  )
282
285
 
283
286
  # Do database stuff again.