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
@@ -1,96 +1,463 @@
1
1
  """Defines models for representing the state of various interactive methods."""
2
2
 
3
- from typing import Literal
3
+ import warnings
4
+ from typing import TYPE_CHECKING
4
5
 
5
6
  from sqlalchemy.types import TypeDecorator
6
- from sqlmodel import JSON, Column, Field, Relationship, SQLModel
7
+ from sqlmodel import (
8
+ JSON,
9
+ Column,
10
+ Field,
11
+ Relationship,
12
+ SQLModel,
13
+ )
7
14
 
15
+ from desdeo.emo.options.templates import PreferenceOptions, TemplateOptions
16
+ from desdeo.mcdm import ENautilusResult
17
+ from desdeo.problem import Tensor, VariableType
8
18
  from desdeo.tools import SolverResults
19
+ from desdeo.tools.score_bands import SCOREBandsResult
9
20
 
10
- from .preference import PreferenceDB, ReferencePoint
11
- from .problem import ProblemDB
12
- from .session import InteractiveSessionDB
21
+ from .preference import PreferenceType, ReferencePoint
13
22
 
23
+ if TYPE_CHECKING:
24
+ from .problem import RepresentativeNonDominatedSolutions
25
+ from .state_table import UserSavedSolutionDB
14
26
 
15
- class StateType(TypeDecorator):
16
- """SQLAlchemy custom type to convert states to JSON and back."""
27
+
28
+ class ResultsType(TypeDecorator):
29
+ """SQLAlchemy custom type to convert a `SolverResults` and similar to JSON and back."""
17
30
 
18
31
  impl = JSON
19
32
 
20
33
  def process_bind_param(self, value, dialect):
21
- """State to JSON."""
22
- if isinstance(value, RPMState):
34
+ """`SolverResults` to JSON."""
35
+ if value is None:
36
+ return None
37
+
38
+ if isinstance(value, list):
39
+ return [x.model_dump() if hasattr(x, "model_dump") else x for x in value]
40
+
41
+ if hasattr(value, "model_dump"):
23
42
  return value.model_dump()
24
43
 
25
- msg = f"No JSON serialization set for ste of type '{type(value)}'."
44
+ msg = f"No JSON serialization set for '{type(value)}'."
26
45
  print(msg)
27
46
 
28
47
  return value
29
48
 
30
49
  def process_result_value(self, value, dialect):
31
- """JSON to state."""
32
- if "method" in value:
33
- match value["method"]:
34
- case "reference_point_method":
35
- return RPMState.model_validate(value)
36
- case _:
37
- msg = f"No method '{value["method"]}' found."
38
- print(msg)
50
+ """JSON to `SolverResults` or similar."""
51
+ # Stupid way to to this, but works for now. Needs to add field
52
+ # to the corresponding models so that they may be identified in dict form.
53
+ # TODO: see above
54
+ if "closeness_measures" in value: # noqa: SIM108
55
+ model = ENautilusResult
56
+ else:
57
+ model = SolverResults
39
58
 
40
- return value
59
+ if value is None:
60
+ return None
61
+ if isinstance(value, list):
62
+ return [model.model_validate(x) for x in value]
41
63
 
64
+ return model.model_validate(value)
42
65
 
43
- class BaseState(SQLModel):
44
- """The base model for representing method state."""
45
66
 
46
- method: Literal["unset"] = "unset"
47
- phase: Literal["unset"] = "unset"
67
+ class ResultInterface:
68
+ @property
69
+ def result_objective_values(self) -> list[dict[str, float]]:
70
+ msg = (
71
+ f"Calling the method `result_objective_values`, which has not been implemented "
72
+ f"for the class `{type(self).__name__}`. Returning an empty list..."
73
+ )
48
74
 
75
+ warnings.warn(msg, category=RuntimeWarning, stacklevel=2)
49
76
 
50
- class RPMBaseState(BaseState):
51
- """The base sate for the reference point method (RPM).
77
+ return []
52
78
 
53
- Other states of the RPM should inherit from this.
54
- """
79
+ @property
80
+ def result_variable_values(self) -> list[dict[str, VariableType | Tensor]]:
81
+ msg = (
82
+ f"Calling the method `result_variable_values`, which has not been implemented "
83
+ f"for the class `{type(self).__name__}`. Returning an empty list..."
84
+ )
85
+
86
+ warnings.warn(msg, category=RuntimeWarning, stacklevel=2)
87
+
88
+ return []
89
+
90
+ @property
91
+ def num_solutions(self) -> int:
92
+ msg = (
93
+ f"Calling the method `num_solutions`, which has not been implemented "
94
+ f"for the class `{type(self).__name__}`. Returning a zero."
95
+ )
96
+
97
+ warnings.warn(msg, category=RuntimeWarning, stacklevel=2)
98
+
99
+ return 0
100
+
101
+
102
+ class RPMState(SQLModel, table=True):
103
+ """Reference Point Method (k+1 candidates)."""
104
+
105
+ id: int | None = Field(default=None, primary_key=True, foreign_key="states.id")
106
+
107
+ # inputs
108
+ preferences: ReferencePoint = Field(sa_column=Column(PreferenceType))
109
+ scalarization_options: dict[str, float | str | bool] | None = Field(sa_column=Column(JSON), default=None)
110
+ solver: str | None = None
111
+ solver_options: dict[str, float | str | bool] | None = Field(sa_column=Column(JSON), default=None)
112
+
113
+ # results
114
+ solver_results: list[SolverResults] = Field(sa_column=Column(ResultsType))
115
+
116
+
117
+ class NIMBUSClassificationState(ResultInterface, SQLModel, table=True):
118
+ """NIMBUS: classification / solve candidates."""
119
+
120
+ id: int | None = Field(default=None, primary_key=True, foreign_key="states.id")
121
+
122
+ preferences: ReferencePoint = Field(sa_column=Column(PreferenceType))
123
+ scalarization_options: dict[str, float | str | bool] | None = Field(sa_column=Column(JSON), default=None)
124
+ solver: str | None = None
125
+ solver_options: dict[str, float | str | bool] | None = Field(sa_column=Column(JSON), default=None)
126
+ current_objectives: dict[str, float] = Field(sa_column=Column(JSON), default_factory=dict)
127
+ num_desired: int | None = 1
128
+ previous_preferences: ReferencePoint = Field(sa_column=Column(PreferenceType))
129
+
130
+ # results
131
+ solver_results: list[SolverResults] = Field(sa_column=Column(ResultsType))
132
+
133
+ @property
134
+ def result_objective_values(self) -> list[dict[str, float]]:
135
+ return [x.optimal_objectives for x in self.solver_results]
136
+
137
+ @property
138
+ def result_variable_values(self) -> list[dict[str, VariableType | Tensor]]:
139
+ return [x.optimal_variables for x in self.solver_results]
140
+
141
+ @property
142
+ def num_solutions(self) -> int:
143
+ return len(self.solver_results)
55
144
 
56
- method: Literal["reference_point_method"] = "reference_point_method"
57
145
 
146
+ class NIMBUSSaveState(ResultInterface, SQLModel, table=True):
147
+ """NIMBUS: save solutions."""
58
148
 
59
- class RPMState(RPMBaseState):
60
- """State of the reference point method for computing solutions."""
149
+ id: int | None = Field(default=None, primary_key=True, foreign_key="states.id")
61
150
 
62
- phase: Literal["solve_candidates"] = "solve_candidates"
151
+ solutions: list["UserSavedSolutionDB"] = Relationship(
152
+ sa_relationship_kwargs={
153
+ # tell SQLA which FK on the child points back to THIS parent
154
+ "foreign_keys": "[UserSavedSolutionDB.save_state_id]",
155
+ "primaryjoin": "NIMBUSSaveState.id == UserSavedSolutionDB.save_state_id",
156
+ "cascade": "all, delete-orphan",
157
+ "lazy": "selectin",
158
+ }
159
+ )
160
+
161
+ @property
162
+ def result_objective_values(self) -> list[dict[str, float]]:
163
+ return [x.objective_values for x in self.solutions]
164
+
165
+ @property
166
+ def result_variable_values(self) -> list[dict[str, VariableType | Tensor]]:
167
+ return [x.variable_values for x in self.solutions]
168
+
169
+ @property
170
+ def num_solutions(self) -> int:
171
+ return len(self.solutions)
63
172
 
64
- # to compute k+1 solutions
173
+
174
+ class NIMBUSInitializationState(ResultInterface, SQLModel, table=True):
175
+ """NIMBUS: initialization."""
176
+
177
+ id: int | None = Field(default=None, primary_key=True, foreign_key="states.id")
178
+
179
+ reference_point: dict[str, float] | None = Field(sa_column=Column(JSON), default=None)
65
180
  scalarization_options: dict[str, float | str | bool] | None = Field(sa_column=Column(JSON), default=None)
66
- solver: str | None = Field(default=None)
181
+ solver: str | None = None
67
182
  solver_options: dict[str, float | str | bool] | None = Field(sa_column=Column(JSON), default=None)
68
183
 
184
+ # Results
185
+ solver_results: "SolverResults" = Field(sa_column=Column(ResultsType), default_factory=list)
186
+
187
+ @property
188
+ def result_objective_values(self) -> list[dict[str, float]]:
189
+ return [self.solver_results.optimal_objectives]
190
+
191
+ @property
192
+ def result_variable_values(self) -> list[dict[str, VariableType | Tensor]]:
193
+ return [self.solver_results.optimal_variables]
194
+
195
+ @property
196
+ def num_solutions(self) -> int:
197
+ return 1
198
+
199
+
200
+ class NIMBUSFinalState(ResultInterface, SQLModel, table=True):
201
+ """NIMBUS: The Final State.
202
+
203
+ NOTE: Despite this being the "final" state, I think the user should
204
+ still be allowed to use this as a basis for new iterations. Therefore
205
+ I think this should behave/have necessary elements for that to be the case.
206
+ """
207
+
208
+ id: int | None = Field(default=None, primary_key=True, foreign_key="states.id")
209
+
210
+ solution_origin_state_id: int = Field(description="The state from which the solution originates.")
211
+ solution_result_index: int = Field(description="The index within that state.")
212
+
213
+ solver_results: "SolverResults" = Field(
214
+ sa_column=Column(ResultsType), default_factory=list
215
+ )
216
+
217
+ @property
218
+ def result_objective_values(self) -> list[dict[str, float]]:
219
+ return [self.solver_results.optimal_objectives]
220
+
221
+ @property
222
+ def result_variable_values(self) -> list[dict[str, VariableType | Tensor]]:
223
+ return [self.solver_results.optimal_variables]
224
+
225
+ @property
226
+ def num_solutions(self) -> int:
227
+ return 1
228
+
229
+
230
+ class GNIMBUSOptimizationState(ResultInterface, SQLModel, table=True):
231
+ """GNIMBUS: classification / solving."""
232
+
233
+ id: int | None = Field(default=None, primary_key=True, foreign_key="states.id")
234
+
235
+ # Preferences that went in
236
+ reference_points: dict[int, dict[str, float]] = Field(sa_column=Column(JSON))
237
+ # Results that came out
238
+ solver_results: list[SolverResults] = Field(sa_column=Column(ResultsType))
239
+
240
+ @property
241
+ def result_objective_values(self) -> list[dict[str, float]]:
242
+ return [x.optimal_objectives for x in self.solver_results]
243
+
244
+ @property
245
+ def result_variable_values(self) -> list[dict[str, VariableType | Tensor]]:
246
+ return [x.optimal_variables for x in self.solver_results]
247
+
248
+ @property
249
+ def num_solutions(self) -> int:
250
+ return len(self.solver_results)
251
+
252
+
253
+ class GNIMBUSVotingState(ResultInterface, SQLModel, table=True):
254
+ """GNIMBUS: voting."""
255
+
256
+ id: int | None = Field(default=None, primary_key=True, foreign_key="states.id")
257
+
258
+ # Preferences that went in
259
+ votes: dict[int, int] = Field(sa_column=Column(JSON))
260
+ # Results that came out
261
+ solver_results: list[SolverResults] = Field(sa_column=Column(ResultsType))
262
+
263
+ @property
264
+ def result_objective_values(self) -> list[dict[str, float]]:
265
+ return [x.optimal_objectives for x in self.solver_results]
266
+
267
+ @property
268
+ def result_variable_values(self) -> list[dict[str, VariableType | Tensor]]:
269
+ return [x.optimal_variables for x in self.solver_results]
270
+
271
+ @property
272
+ def num_solutions(self) -> int:
273
+ return 1
274
+
275
+
276
+ class GNIMBUSEndState(ResultInterface, SQLModel, table=True):
277
+ """GNIMBUS: ending. We check if everyone's happy with the solution and end if yes."""
278
+
279
+ id: int | None = Field(default=None, primary_key=True, foreign_key="states.id")
280
+
281
+ # Preferences that went in
282
+ votes: dict[int, bool] = Field(sa_column=Column(JSON))
283
+ # Success?
284
+ success: bool = Field()
285
+ # Results that came out
286
+ solver_results: list[SolverResults] = Field(sa_column=Column(ResultsType))
287
+
288
+ @property
289
+ def result_objective_values(self) -> list[dict[str, float]]:
290
+ return [x.optimal_objectives for x in self.solver_results]
291
+
292
+ @property
293
+ def result_variable_values(self) -> list[dict[str, VariableType | Tensor]]:
294
+ return [x.optimal_variables for x in self.solver_results]
295
+
296
+ @property
297
+ def num_solutions(self) -> int:
298
+ return 1
299
+
300
+
301
+ class EMOIterateState(ResultInterface, SQLModel, table=True):
302
+ """EMO run (NSGA3, RVEA, etc.)."""
303
+
304
+ id: int | None = Field(default=None, primary_key=True, foreign_key="states.id")
305
+
306
+ # algorithm flavor
307
+ template_options: list[TemplateOptions] = Field(
308
+ sa_column=Column(JSON)
309
+ ) # TODO: This should probably be ids to another table
310
+ # preferences
311
+ preference_options: PreferenceOptions | None = Field(sa_column=Column(JSON))
312
+
313
+ # results
314
+ decision_variables: dict[str, list[VariableType]] | None = Field(
315
+ sa_column=Column(JSON), description="Optimization results (decision variables)", default=None
316
+ ) ## Unlike other methods, we have a very large number of solutions. So maybe we should store them together?
317
+ objective_values: dict[str, list[float]] | None = Field(
318
+ sa_column=Column(JSON), description="Optimization outputs", default=None
319
+ )
320
+ constraint_values: dict[str, list[float]] | None = Field(
321
+ sa_column=Column(JSON), description="Constraint values of the solutions", default=None
322
+ )
323
+ extra_func_values: dict[str, list[float]] | None = Field(
324
+ sa_column=Column(JSON), description="Extra function values of the solutions", default=None
325
+ )
326
+
327
+ @property
328
+ def result_objective_values(self) -> dict[str, list[float]]:
329
+ if self.objective_values is None:
330
+ raise ValueError("No objective values stored in this state.")
331
+ return self.objective_values
332
+
333
+ @property
334
+ def result_variable_values(self) -> dict[str, list[VariableType]]:
335
+ if self.decision_variables is None:
336
+ raise ValueError("No decision variables stored in this state.")
337
+ return self.decision_variables
338
+
339
+ @property
340
+ def result_constraint_values(self) -> dict[str, list[float]] | None:
341
+ return self.constraint_values
342
+
343
+ @property
344
+ def result_extra_func_values(self) -> dict[str, list[float]] | None:
345
+ return self.extra_func_values
346
+
347
+ @property
348
+ def num_solutions(self) -> int:
349
+ if self.objective_values:
350
+ first_key = next(iter(self.objective_values))
351
+ return len(self.objective_values[first_key])
352
+ return 0
353
+
354
+
355
+ class EMOFetchState(SQLModel, table=True):
356
+ """Request model for fetching EMO solutions."""
357
+
358
+ id: int | None = Field(default=None, primary_key=True, foreign_key="states.id")
359
+ # More fields can be added here if needed in the future. E.g., number of solutions to fetch, filters, etc.
360
+
361
+
362
+ class EMOSCOREState(SQLModel, table=True):
363
+ """EMO: SCORE iteration."""
364
+
365
+ id: int | None = Field(default=None, primary_key=True, foreign_key="states.id")
366
+
367
+ result: SCOREBandsResult = Field(sa_column=Column(JSON))
368
+
369
+
370
+ class EMOSaveState(SQLModel, table=True):
371
+ """EMO: save solutions."""
372
+
373
+ id: int | None = Field(default=None, primary_key=True, foreign_key="states.id")
374
+
69
375
  # results
70
- solver_results: list[SolverResults] = Field(sa_column=Column(JSON))
376
+ decision_variables: dict[str, list[VariableType]] = Field(
377
+ sa_column=Column(JSON), description="Optimization results (decision variables)", default_factory=dict
378
+ ) ## Unlike other methods, we have a very large number of solutions. So maybe we should store them together?
379
+ objective_values: dict[str, list[float]] = Field(
380
+ sa_column=Column(JSON), description="Optimization outputs", default_factory=dict
381
+ )
382
+ constraint_values: dict[str, list[float]] | None = Field(
383
+ sa_column=Column(JSON), description="Constraint values of the solutions", default_factory=dict
384
+ )
385
+ extra_func_values: dict[str, list[float]] | None = Field(
386
+ sa_column=Column(JSON), description="Extra function values of the solutions", default_factory=dict
387
+ )
388
+ names: list[str | None] = Field(
389
+ default_factory=list, sa_column=Column(JSON), description="Names of the saved solutions"
390
+ )
391
+
392
+ @property
393
+ def result_objective_values(self) -> dict[str, list[float]]:
394
+ return self.objective_values
395
+
396
+ @property
397
+ def result_variable_values(self) -> dict[str, list[VariableType]]:
398
+ return self.decision_variables
71
399
 
400
+ @property
401
+ def result_constraint_values(self) -> dict[str, list[float]] | None:
402
+ return self.constraint_values
72
403
 
73
- class StateDB(SQLModel, table=True):
74
- """Database model to store interactive method state."""
404
+ @property
405
+ def result_extra_func_values(self) -> dict[str, list[float]] | None:
406
+ return self.extra_func_values
75
407
 
76
- id: int | None = Field(primary_key=True, default=None)
77
- problem_id: int | None = Field(foreign_key="problemdb.id", default=None)
78
- preference_id: int | None = Field(foreign_key="preferencedb.id", default=None)
79
- session_id: int | None = Field(foreign_key="interactivesessiondb.id", default=None)
408
+ @property
409
+ def num_solutions(self) -> int:
410
+ if self.objective_values:
411
+ first_key = next(iter(self.objective_values))
412
+ return len(self.objective_values[first_key])
413
+ return 0
80
414
 
81
- # Reference to other StateDB
82
- parent_id: int | None = Field(foreign_key="statedb.id", default=None)
83
415
 
84
- state: BaseState | None = Field(sa_column=Column(StateType), default=None)
416
+ class IntermediateSolutionState(SQLModel, ResultInterface, table=True):
417
+ """Generic intermediate solutions requested by other methods."""
85
418
 
86
- # Back populates
87
- session: "InteractiveSessionDB" = Relationship(back_populates="states")
88
- parent: "StateDB" = Relationship(back_populates="children", sa_relationship_kwargs={"remote_side": "StateDB.id"})
89
- # if a parent node is killed, so are all its children (blood for the blood God)
90
- children: list["StateDB"] = Relationship(
91
- back_populates="parent", sa_relationship_kwargs={"cascade": "all, delete-orphan"}
419
+ id: int | None = Field(default=None, primary_key=True, foreign_key="states.id")
420
+
421
+ context: str | None = Field(
422
+ default=None,
423
+ description="Originating method context (e.g., 'nimbus', 'rpm') that requested these solutions",
92
424
  )
425
+ scalarization_options: dict[str, float | str | bool] | None = Field(sa_column=Column(JSON), default=None)
426
+ solver: str | None = None
427
+ solver_options: dict[str, float | str | bool] | None = Field(sa_column=Column(JSON), default=None)
428
+ num_desired: int | None = 1
429
+ reference_solution_1: dict[str, float] | None = Field(sa_column=Column(JSON), default=None)
430
+ reference_solution_2: dict[str, float] | None = Field(sa_column=Column(JSON), default=None)
431
+
432
+ # results
433
+ solver_results: list[SolverResults] = Field(sa_column=Column(ResultsType))
434
+
435
+ @property
436
+ def result_objective_values(self) -> list[dict[str, float]]:
437
+ return [x.optimal_objectives for x in self.solver_results]
438
+
439
+ @property
440
+ def result_variable_values(self) -> list[dict[str, VariableType]]:
441
+ return [x.optimal_variables for x in self.solver_results]
442
+
443
+ @property
444
+ def num_solutions(self) -> int:
445
+ return len(self.solver_results)
446
+
447
+
448
+ class ENautilusState(SQLModel, table=True):
449
+ """E-NAUTILUS: one stepping iteration."""
450
+
451
+ id: int | None = Field(default=None, primary_key=True, foreign_key="states.id")
452
+
453
+ non_dominated_solutions_id: int | None = Field(foreign_key="representativenondominatedsolutions.id", default=None)
454
+
455
+ current_iteration: int
456
+ iterations_left: int
457
+ selected_point: dict[str, float] | None = Field(sa_column=Column(JSON), default=None)
458
+ reachable_point_indices: list[int] = Field(sa_column=Column(JSON), default_factory=list)
459
+ number_of_intermediate_points: int
460
+
461
+ enautilus_results: "ENautilusResult" = Field(sa_column=Column(ResultsType))
93
462
 
94
- # Parents
95
- preference: "PreferenceDB" = Relationship()
96
- problem: "ProblemDB" = Relationship()
463
+ non_dominated_solutions: "RepresentativeNonDominatedSolutions" = Relationship()
desdeo/api/models/user.py CHANGED
@@ -3,12 +3,12 @@
3
3
  from enum import Enum
4
4
  from typing import TYPE_CHECKING
5
5
 
6
- from sqlmodel import Field, Relationship, SQLModel
6
+ from sqlmodel import JSON, Column, Field, Relationship, SQLModel
7
7
 
8
8
  if TYPE_CHECKING:
9
- from .archive import ArchiveEntryDB
9
+ from .archive import UserSavedSolutionDB
10
10
  from .preference import PreferenceDB
11
- from .schemas import ProblemDB
11
+ from .problem import ProblemDB
12
12
  from .session import InteractiveSessionDB
13
13
 
14
14
 
@@ -33,11 +33,12 @@ class User(UserBase, table=True):
33
33
  id: int | None = Field(primary_key=True, default=None)
34
34
  password_hash: str = Field()
35
35
  role: UserRole = Field()
36
- group: str = Field(default="")
36
+ group: str | None = Field(default="") # TODO: Get rid of this and use proper group systems
37
+ group_ids: list[int] = Field(sa_column=Column(JSON), default=[]) # The user is either a member of a group or an owner of a group
37
38
  active_session_id: int | None = Field(default=None)
38
39
 
39
40
  # Back populates
40
- archive: list["ArchiveEntryDB"] = Relationship(back_populates="user")
41
+ archive: list["UserSavedSolutionDB"] = Relationship(back_populates="user")
41
42
  preferences: list["PreferenceDB"] = Relationship(back_populates="user")
42
43
  problems: list["ProblemDB"] = Relationship(back_populates="user")
43
44
  sessions: list["InteractiveSessionDB"] = Relationship(back_populates="user")
@@ -48,4 +49,4 @@ class UserPublic(UserBase):
48
49
 
49
50
  id: int
50
51
  role: UserRole
51
- group: str
52
+ group_ids: list[int] | None
@@ -0,0 +1,25 @@
1
+ """Request and response models for Utopia endpoint."""
2
+
3
+ from typing import Any
4
+
5
+ from sqlmodel import Field, SQLModel
6
+
7
+ from desdeo.api.models.generic import SolutionInfo
8
+
9
+
10
+ class UtopiaRequest(SQLModel):
11
+ """The request for an Utopia map."""
12
+
13
+ problem_id: int = Field(description="Problem for which the map is generated")
14
+ solution: SolutionInfo = Field(description="Solution for which to generate the map")
15
+
16
+
17
+ class UtopiaResponse(SQLModel):
18
+ """The response to an UtopiaRequest."""
19
+
20
+ is_utopia: bool = Field(description="True if map exists for this problem.")
21
+ map_name: str = Field(description="Name of the map.")
22
+ map_json: dict[str, Any] = Field(description="MapJSON representation of the geography.")
23
+ options: dict[str, Any] = Field(description="A dict with given years as keys containing options for each year.")
24
+ description: str = Field(description="Description shown above the map.")
25
+ years: list[str] = Field(description="A list of years for which the maps have been generated.")