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,128 @@
1
+ """Models specific to the nimbus method."""
2
+
3
+ from pydantic import ConfigDict
4
+ from sqlmodel import JSON, Column, Field, SQLModel
5
+
6
+ from desdeo.emo.options.templates import PreferenceOptions, TemplateOptions
7
+ from desdeo.tools.score_bands import SCOREBandsConfig, SCOREBandsResult
8
+
9
+
10
+ class EMOIterateRequest(SQLModel):
11
+ """Model of the request to iterate an EMO method."""
12
+
13
+ model_config = ConfigDict(use_attribute_docstrings=True)
14
+
15
+ problem_id: int
16
+ """Database ID of the problem to solve."""
17
+ session_id: int | None = Field(default=None)
18
+ parent_state_id: int | None = Field(default=None)
19
+ """State ID of the parent state, if any. Should be None if this is the first state in a session."""
20
+
21
+ template_options: list[TemplateOptions] | None = Field(default=None)
22
+ """Options for the template to use. A list of options can be given if multiple templates are used in parallel."""
23
+ preference_options: PreferenceOptions | None = Field(default=None)
24
+ """Options for the preference handling."""
25
+
26
+
27
+ class EMOFetchRequest(SQLModel):
28
+ """Model of the request to fetch solutions from an EMO method."""
29
+
30
+ model_config = ConfigDict(use_attribute_docstrings=True)
31
+
32
+ problem_id: int
33
+ """Database ID of the problem to solve."""
34
+ session_id: int | None = Field(default=None)
35
+ parent_state_id: int | None = Field(default=None)
36
+ """State ID of the parent state, if any. Should be None if this is the first state in a session."""
37
+
38
+ num_solutions: int = Field(default=0)
39
+ """Number of solutions to fetch. If 0, fetch all solutions."""
40
+
41
+
42
+ class EMOSaveRequest(SQLModel):
43
+ """Request model for saving solutions from any method's state."""
44
+
45
+ model_config = ConfigDict(use_attribute_docstrings=True)
46
+
47
+ problem_id: int
48
+ """Database ID of the problem to solve."""
49
+ session_id: int | None = Field(default=None)
50
+ parent_state_id: int | None = Field(default=None)
51
+ """State ID of the parent state, if any. Should be None if this is the first state in a session."""
52
+
53
+ solution_ids: list[int] = Field()
54
+ """List of solution IDs to save."""
55
+ solution_names: list[str | None] | None = Field(default=None)
56
+ """List of names for the solutions to save. If None, no names are given."""
57
+
58
+
59
+ class EMOScoreRequest(SQLModel):
60
+ """Request model for getting SCORE bands visualization data from state."""
61
+
62
+ model_config = ConfigDict(use_attribute_docstrings=True)
63
+
64
+ problem_id: int
65
+ """Database ID of the problem to solve."""
66
+ session_id: int | None = Field(default=None)
67
+ parent_state_id: int | None = Field(default=None)
68
+ """State ID of the parent state, if any."""
69
+
70
+ config: SCOREBandsConfig | None = Field(default=None)
71
+ """Configuration for the SCORE bands visualization."""
72
+
73
+ solution_ids: list[int] = Field()
74
+ """List of solution IDs to score."""
75
+
76
+
77
+ class EMOIterateResponse(SQLModel):
78
+ """Model of the response to an EMO iterate request."""
79
+
80
+ model_config = ConfigDict(use_attribute_docstrings=True)
81
+
82
+ method_ids: list[str]
83
+ """IDs of the EMO methods using websockets to get/send updates."""
84
+ client_id: str
85
+ """Client ID to use when connecting to the websockets."""
86
+ state_id: int
87
+ """The state ID of the newly created state."""
88
+
89
+
90
+ class Solution(SQLModel):
91
+ solution_id: int
92
+ """ID of the solution"""
93
+ objective_values: dict[str, list[float]]
94
+ """Values of the objectives. Each inner list corresponds to a solution."""
95
+ constraint_values: dict[str, list[float]] | None = None
96
+ """Values of the constraints. Each inner list corresponds to a solution."""
97
+ variable_values: dict[str, list[float | int | bool]]
98
+ """Values of the decision variables. Each inner list corresponds to a solution."""
99
+ extra_func_values: dict[str, list[float]] | None = None
100
+ """Values of the extra functions. Each inner list corresponds to a solution."""
101
+
102
+
103
+ class EMOFetchResponse(SQLModel):
104
+ """Model of the response to an EMO fetch request."""
105
+
106
+ model_config = ConfigDict(use_attribute_docstrings=True)
107
+
108
+ state_id: int
109
+ """The state ID of the newly created state."""
110
+ objective_values: dict[str, list[float]]
111
+ """Values of the objectives. Each inner list corresponds to a solution."""
112
+ constraint_values: dict[str, list[float]] | None = None
113
+ """Values of the constraints. Each inner list corresponds to a solution."""
114
+ variable_values: dict[str, list[float | int | bool]]
115
+ """Values of the decision variables. Each inner list corresponds to a solution."""
116
+ extra_func_values: dict[str, list[float]] | None = None
117
+ """Values of the extra functions. Each inner list corresponds to a solution."""
118
+
119
+
120
+ class EMOScoreResponse(SQLModel):
121
+ """Model of the response to an EMO score request."""
122
+
123
+ model_config = ConfigDict(use_attribute_docstrings=True)
124
+
125
+ state_id: int | None = Field(default=None)
126
+ """The state ID of the newly created state."""
127
+
128
+ result: SCOREBandsResult
@@ -0,0 +1,69 @@
1
+ """Models specific to the E-NAUTILUS point method."""
2
+
3
+ from sqlmodel import JSON, Column, Field, SQLModel
4
+
5
+ from desdeo.tools import SolverResults
6
+
7
+
8
+ class ENautilusStepRequest(SQLModel):
9
+ """Model of the request to the E-NAUTILUS method."""
10
+
11
+ problem_id: int
12
+ session_id: int | None = Field(default=None)
13
+ parent_state_id: int | None = Field(default=None)
14
+ # non_dominated points fetched from problem metadata in endpoints
15
+ representative_solutions_id: int = Field(description="The id of the representative solutions to be used.")
16
+
17
+ current_iteration: int = Field(description="The number of the current iteration.")
18
+ iterations_left: int = Field(description="The number of iterations left.")
19
+ selected_point: dict[str, float] | None = Field(
20
+ sa_column=Column(JSON),
21
+ description=(
22
+ "The selected intermediate point. If first iteration, set this to be the (approximated) nadir point. "
23
+ "If not set, then the point is assumed to be the nadir point of the current approximating set."
24
+ ),
25
+ )
26
+ reachable_point_indices: list[int] = Field(
27
+ description=(
28
+ "The indices indicating the point on the non-dominated set that are "
29
+ "reachable from the currently selected point."
30
+ )
31
+ )
32
+ number_of_intermediate_points: int = Field(description="The number of intermediate points to be generated.")
33
+
34
+
35
+ class ENautilusStepResponse(SQLModel):
36
+ """The response from E-NAUTILUS step endpoint."""
37
+
38
+ state_id: int | None = Field(description="The id of the state created by the request that generated this response")
39
+
40
+ current_iteration: int = Field(description="Number of the current iteration.")
41
+ iterations_left: int = Field(description="Number of iterations left.")
42
+ intermediate_points: list[dict[str, float]] = Field(sa_column=Column(JSON), description="New intermediate points")
43
+ reachable_best_bounds: list[dict[str, float]] = Field(
44
+ sa_column=Column(JSON),
45
+ description="Best bounds of the objective function values reachable from each intermediate point.",
46
+ )
47
+ reachable_worst_bounds: list[dict[str, float]] = Field(
48
+ sa_column=Column(JSON),
49
+ description="Worst bounds of the objective function values reachable from each intermediate point.",
50
+ )
51
+ closeness_measures: list[float] = Field(description="Closeness measures of each intermediate point.")
52
+ reachable_point_indices: list[list[int]] = Field(
53
+ description="Indices of the reachable points from each intermediate point."
54
+ )
55
+
56
+
57
+ class ENautilusStateResponse(SQLModel):
58
+ """The response model when requesting a state in E-NAUTILUS."""
59
+
60
+ request: ENautilusStepRequest = Field(description="The original request for generating the state.")
61
+ response: ENautilusStepResponse = Field(description="The state generated by the request.")
62
+
63
+
64
+ class ENautilusRepresentativeSolutionsResponse(SQLModel):
65
+ """Model of the response when requesting representative solutions from E-NAUTILUS."""
66
+
67
+ solutions: list[SolverResults] = Field(
68
+ description="The solutions on the non-dominated front closest to the intermediate points."
69
+ )
@@ -0,0 +1,139 @@
1
+ """Classes for group decision making, aggregating all different types of data classes."""
2
+
3
+ import json
4
+
5
+ from sqlalchemy.types import TypeDecorator
6
+ from sqlmodel import JSON, Column, Field, Relationship, SQLModel
7
+
8
+ from desdeo.api.models.gdm.gdm_base import BaseGroupInfoContainer
9
+ from desdeo.api.models.gdm.gdm_score_bands import GDMSCOREBandFinalSelection, GDMSCOREBandInformation
10
+ from desdeo.api.models.gdm.gnimbus import EndProcessPreference, OptimizationPreference, VotingPreference
11
+ from desdeo.tools import SolverResults
12
+
13
+
14
+ class PreferenceType(TypeDecorator):
15
+ """A converter of Preference types."""
16
+
17
+ impl = JSON
18
+
19
+ # Serialize
20
+ def process_bind_param(self, value, dialect):
21
+ """Turns a preference item into json."""
22
+ if isinstance(value, BaseGroupInfoContainer):
23
+ return value.model_dump_json()
24
+ return None
25
+
26
+ # Deserialize
27
+ def process_result_value(self, value, dialect):
28
+ """And the other way around."""
29
+ jsoned = json.loads(value)
30
+ if jsoned is not None:
31
+ match jsoned["method"]:
32
+ case "voting":
33
+ return VotingPreference.model_validate(jsoned)
34
+ case "optimization":
35
+ return OptimizationPreference.model_validate(jsoned)
36
+ # As the different methods are implemented, add new types
37
+ case "end":
38
+ return EndProcessPreference.model_validate(jsoned)
39
+ case "gdm-score-bands":
40
+ return GDMSCOREBandInformation.model_validate(jsoned)
41
+ case "gdm-score-bands-final":
42
+ return GDMSCOREBandFinalSelection.model_validate(jsoned)
43
+ case _:
44
+ print(f"Unable to deserialize Preference with method {jsoned['method']}.")
45
+ return None
46
+ return None
47
+
48
+
49
+ class GroupBase(SQLModel):
50
+ """Base class for group table model and group response model."""
51
+
52
+
53
+ class Group(GroupBase, table=True):
54
+ """Table model for Group."""
55
+
56
+ id: int | None = Field(primary_key=True, default=None)
57
+ name: str | None = Field(default=None)
58
+
59
+ owner_id: int | None = Field(foreign_key="user.id", default=None)
60
+ user_ids: list[int] | None = Field(sa_column=Column(JSON))
61
+
62
+ problem_id: int = Field(default=None)
63
+
64
+ """The id of the head GroupIteration."""
65
+ head_iteration_id: int | None
66
+
67
+
68
+ class GroupPublic(GroupBase):
69
+ """Response model for Group."""
70
+
71
+ id: int
72
+ name: str
73
+ owner_id: int
74
+ user_ids: list[int]
75
+ problem_id: int
76
+
77
+
78
+ class GroupIteration(SQLModel, table=True):
79
+ """Table model for Group Iteration (we could extend this in various ways)."""
80
+
81
+ id: int | None = Field(primary_key=True, default=None)
82
+ problem_id: int | None = Field(default=None)
83
+
84
+ """ID of the associated Group."""
85
+ group_id: int
86
+
87
+ """The preferences are stored in this item while the iteration is in progress."""
88
+ info_container: BaseGroupInfoContainer = Field(sa_column=Column(PreferenceType))
89
+ # NOTE: This used to be called "preferences" and the class used to be called "BasePreference"
90
+
91
+ notified: dict[int, bool] = Field(sa_column=Column(JSON))
92
+
93
+ """State for storing post optimization/voting related data (dec vars, objectives, etc.)"""
94
+ state_id: int | None = Field()
95
+
96
+ """Linked list emerges."""
97
+ parent_id: int | None = Field(foreign_key="groupiteration.id", default=None)
98
+ parent: "GroupIteration" = Relationship(
99
+ back_populates="children", sa_relationship_kwargs={"remote_side": "GroupIteration.id"}
100
+ )
101
+ # If parent is removed, remove the child too
102
+ children: list["GroupIteration"] = Relationship(
103
+ back_populates="parent", sa_relationship_kwargs={"cascade": "all, delete-orphan"}
104
+ )
105
+
106
+
107
+ class GroupInfoRequest(SQLModel):
108
+ """Class for requesting group information."""
109
+
110
+ group_id: int
111
+
112
+ class GroupRevertRequest(SQLModel):
113
+ """Class for requesting reverting to certain iteration."""
114
+
115
+ group_id: int = Field(description="The ID of the group we wish to revert.")
116
+ state_id: int = Field(
117
+ description="The state's ID to which we want to revert to. "\
118
+ "Corresponds to state_id in GroupIteration."
119
+ )
120
+
121
+
122
+ class GroupResult(SQLModel):
123
+ """Class for group's result."""
124
+
125
+ solver_results: list[SolverResults]
126
+
127
+
128
+ class GroupModifyRequest(SQLModel):
129
+ """Used for adding a user into group and removing a user from group."""
130
+
131
+ group_id: int
132
+ user_id: int
133
+
134
+
135
+ class GroupCreateRequest(SQLModel):
136
+ """Used for requesting a group to be created."""
137
+
138
+ group_name: str
139
+ problem_id: int
@@ -0,0 +1,69 @@
1
+ """Base classes for Group Decision Making. Here one can add some common data types for serialization."""
2
+
3
+ import json
4
+
5
+ from pydantic import ValidationError
6
+ from sqlalchemy.types import TypeDecorator
7
+ from sqlmodel import JSON, SQLModel
8
+
9
+ from desdeo.api.models.preference import ReferencePoint
10
+
11
+
12
+ class ReferencePointDictType(TypeDecorator):
13
+ """A converter for dict of int and preferences."""
14
+
15
+ impl = JSON
16
+
17
+ def process_bind_param(self, value, dialect):
18
+ """Turns a reference point dict into json."""
19
+ if isinstance(value, dict):
20
+ for key, item in value.items():
21
+ if isinstance(item, ReferencePoint):
22
+ value[key] = item.model_dump_json()
23
+ return json.dumps(value)
24
+ return None
25
+
26
+ def process_result_value(self, value, dialect):
27
+ """And the other way around."""
28
+ dictionary = json.loads(value)
29
+ for key, item in dictionary.items():
30
+ if item is None:
31
+ print("Something's wrong... Database has a NoneType entry.")
32
+ try:
33
+ dictionary[key] = ReferencePoint.model_validate(json.loads(item))
34
+ except ValidationError as e:
35
+ print(f"Validation error when deserializing PreferencePoint: {e}")
36
+ return dictionary
37
+
38
+
39
+ class BooleanDictTypeDecorator(TypeDecorator):
40
+ """A converter of bool into json, surprising that this needs to exists."""
41
+
42
+ impl = JSON
43
+
44
+ def process_bind_param(self, value, dialect):
45
+ """Turns boolean dict to json."""
46
+ if isinstance(value, dict):
47
+ for key, item in value.items():
48
+ if isinstance(item, bool):
49
+ value[key] = json.dumps(item)
50
+ return json.dumps(value)
51
+ return None
52
+
53
+ def process_result_value(self, value, dialect):
54
+ """And the other way around."""
55
+ dictionary = json.loads(value)
56
+ for key, item in dictionary.items():
57
+ if item is None:
58
+ print("Something's wrong... Database has a NoneType entry.")
59
+ try:
60
+ dictionary[key] = bool(item)
61
+ except Exception as e:
62
+ print(f"Validation error when deserializing boolean: {e}")
63
+ return dictionary
64
+
65
+
66
+ class BaseGroupInfoContainer(SQLModel):
67
+ """A base class for a method specific iteration information."""
68
+
69
+ method: str = "unset"
@@ -0,0 +1,114 @@
1
+ """Models for GDM Score Bands.
2
+
3
+ Idea is that in the very first iteration, the filtered indices contains the clustering
4
+ information on the entire data. Since on each iteration, the clustering is different,
5
+ we need to include the indices over and over again. Of course with time the amount of
6
+ indices will get smaller and smaller, and eventually be only ~10 solutions.
7
+
8
+ The names of the classes can be renamed to fit the purpose better, currently they are
9
+ more or less just the first thing that came to my mind.
10
+ """
11
+
12
+ from sqlmodel import JSON, Column, Field, SQLModel
13
+
14
+ from desdeo.api.models.gdm.gdm_base import BaseGroupInfoContainer
15
+ from desdeo.gdm.score_bands import SCOREBandsGDMConfig, SCOREBandsGDMResult
16
+ from desdeo.problem.schema import VariableType
17
+ from desdeo.tools.score_bands import SCOREBandsResult
18
+
19
+
20
+ class GDMSCOREBandInformation(BaseGroupInfoContainer):
21
+ """Class for containing info on which band was voted for."""
22
+ method: str = "gdm-score-bands"
23
+ user_votes: dict[str, int] = Field(
24
+ description="Dictionary of votes."
25
+ )
26
+ user_confirms: list[int] = Field(
27
+ description="List of users who want to move on."
28
+ )
29
+ score_bands_config: SCOREBandsGDMConfig = Field(
30
+ description="The configuration that led to this classification."
31
+ )
32
+ score_bands_result: SCOREBandsGDMResult = Field(
33
+ description="The results of the score bands."
34
+ )
35
+
36
+
37
+ class GDMSCOREBandFinalSelection(BaseGroupInfoContainer):
38
+ """Class for containing the final 10 or less solutions, the final solution and the votes that led to it."""
39
+ method: str = "gdm-score-bands-final"
40
+ user_votes: dict[str, int] = Field(
41
+ description="Dictionary of votes."
42
+ )
43
+ user_confirms: list[int] = Field(
44
+ description="List of users who want to move on."
45
+ )
46
+
47
+ """The 10 or less solutions to choose from"""
48
+ solution_variables: dict[str, list[VariableType]] = Field(sa_column=Column(JSON))
49
+ solution_objectives: dict[str, list[float]] = Field(sa_column=Column(JSON))
50
+
51
+ """The selected (or generated??) of those 10 or less."""
52
+ winner_solution_variables: dict[str, VariableType] = Field(sa_column=Column(JSON))
53
+ winner_solution_objectives: dict[str, float] = Field(sa_column=Column(JSON))
54
+
55
+
56
+ class GDMScoreBandsInitializationRequest(SQLModel):
57
+ """Request class for initialization of score bands."""
58
+ group_id: int = Field(
59
+ description="The group to be initialized."
60
+ )
61
+ score_bands_config: SCOREBandsGDMConfig = Field(
62
+ description="The configuration for the initial score banding."
63
+ )
64
+
65
+ class GDMScoreBandsVoteRequest(SQLModel):
66
+ """Request for voting for a band."""
67
+ group_id: int = Field(
68
+ description="ID of the group in question"
69
+ )
70
+ vote: int = Field(
71
+ description="The vote. Vaalisalaisuus."
72
+ )
73
+
74
+ class GDMSCOREBandsRevertRequest(SQLModel):
75
+ """Request for reverting to a previous setup."""
76
+ group_id: int = Field(
77
+ description="Group ID."
78
+ )
79
+ iteration_number: int = Field(
80
+ description="The number of the iteration that we want to revert to."
81
+ )
82
+
83
+ class GDMSCOREBandsResponse(SQLModel):
84
+ """Response class for GDMSCOREBands, whether it is initialization or not."""
85
+ method: str = "gdm-score-bands"
86
+ group_id: int = Field(
87
+ description="The group in question."
88
+ )
89
+ group_iter_id: int = Field(
90
+ description="ID of the latest group iteration."
91
+ )
92
+ latest_iteration: int = Field(
93
+ description="The latest GDM iteration number. Different from Group Iteration id."
94
+ )
95
+ result: SCOREBandsResult = Field(
96
+ description="The results of the score bands procedure."
97
+ )
98
+
99
+ class GDMSCOREBandsDecisionResponse(SQLModel):
100
+ """Response class for gdm score bands that includes the last 10 or less solutions."""
101
+ method: str = "gdm-score-bands-final"
102
+ group_id: int = Field(
103
+ description="The group in question."
104
+ )
105
+ group_iter_id: int = Field(
106
+ description="ID of the latest group iteration."
107
+ )
108
+ result: GDMSCOREBandFinalSelection = Field(
109
+ description="The container for the solutions and the winner solution."
110
+ )
111
+
112
+ class GDMSCOREBandsHistoryResponse(SQLModel):
113
+ """Response class for all history. Allows for going to a previous iteration."""
114
+ history: list[GDMSCOREBandsResponse | GDMSCOREBandsDecisionResponse]
@@ -0,0 +1,138 @@
1
+ """GNIMBUS specific data classes; REMEMBER to add SER/DES into aggregator!"""
2
+
3
+ import json
4
+
5
+ from pydantic import ValidationError
6
+ from sqlmodel import JSON, Column, Field, SQLModel, TypeDecorator
7
+
8
+ from desdeo.api.models.gdm.gdm_base import (
9
+ BaseGroupInfoContainer,
10
+ BooleanDictTypeDecorator,
11
+ ReferencePoint,
12
+ ReferencePointDictType,
13
+ )
14
+ from desdeo.api.models.generic_states import SolutionReference, SolutionReferenceLite
15
+ from desdeo.tools import SolverResults
16
+
17
+
18
+ class SolverResultType(TypeDecorator):
19
+ """Not sure why the solver results wouldn't serialize but this should do the trick."""
20
+
21
+ impl = JSON
22
+
23
+ def process_bind_param(self, value, dialect):
24
+ """Turns solver results into json."""
25
+ if isinstance(value, list):
26
+ solver_list = []
27
+ for item in value:
28
+ if isinstance(item, SolverResults):
29
+ solver_list.append(item.model_dump_json())
30
+ return json.dumps(solver_list)
31
+ return None
32
+
33
+ def process_result_value(self, value, dialect):
34
+ """And back."""
35
+ if value is not None:
36
+ value_list = json.loads(value)
37
+ solver_list: SolverResults = []
38
+ for item in value_list:
39
+ item_dict = json.loads(item)
40
+ try:
41
+ solver_list.append(SolverResults.model_validate(item_dict))
42
+ except ValidationError as e:
43
+ print(f"Validation error when deserializing SolverResults: {e}")
44
+ continue
45
+ return solver_list
46
+ return None
47
+
48
+
49
+ class GNIMBUSSwitchPhaseRequest(SQLModel):
50
+ """A request for a certain phase. Comes from the group owner/analyst."""
51
+
52
+ group_id: int
53
+ new_phase: str
54
+
55
+
56
+ class GNIMBUSSwitchPhaseResponse(SQLModel):
57
+ """A response for the above request."""
58
+
59
+ old_phase: str
60
+ new_phase: str
61
+
62
+
63
+ class OptimizationPreference(BaseGroupInfoContainer):
64
+ """A structure for storing optimization preferences. See GNIMBUS for details."""
65
+
66
+ method: str = "optimization"
67
+ phase: str = Field(default="learning")
68
+ set_preferences: dict[int, ReferencePoint] = Field(sa_column=Column(ReferencePointDictType))
69
+
70
+
71
+ class VotingPreference(BaseGroupInfoContainer):
72
+ """A structure for storing voting preferences."""
73
+
74
+ method: str = "voting"
75
+ set_preferences: dict[int, int] = Field(
76
+ sa_column=Column(JSON)
77
+ ) # A user votes for an index from the results (or something)
78
+
79
+
80
+ class EndProcessPreference(BaseGroupInfoContainer):
81
+ """A structure for storing info on whether everyone is happy to end the gnimbus process."""
82
+
83
+ method: str = "end"
84
+ success: bool | None = Field()
85
+
86
+ # We check if everyone agrees to stop.
87
+ set_preferences: dict[int, bool] = Field(sa_column=Column(BooleanDictTypeDecorator))
88
+
89
+
90
+ class GNIMBUSResultResponse(SQLModel):
91
+ """The response for getting GNIMBUS results. NOTE: OBSOLETE!"""
92
+
93
+ method: str
94
+ phase: str
95
+ preferences: VotingPreference | OptimizationPreference
96
+ common_results: list[SolutionReference]
97
+ user_results: list[SolutionReference]
98
+ personal_result_index: int | None
99
+
100
+
101
+ class FullIteration(SQLModel):
102
+ """A full iteration item containing results from a complete or incomplete iteration.
103
+
104
+ This is a format to send information to the user interface.
105
+ """
106
+
107
+ phase: str = Field(
108
+ description="The phase of the iteration."
109
+ )
110
+ optimization_preferences: OptimizationPreference | None = Field(
111
+ description="The preferences related to the optimization stage of the full iteration."
112
+ )
113
+ voting_preferences: VotingPreference | EndProcessPreference | None = Field(
114
+ description="The preferences related to the voting phase of the iteration. \
115
+ either actual votes or a vote to see whether to just continue."
116
+ )
117
+ starting_result: SolutionReferenceLite | None = Field(
118
+ description="The starting result of the optimization process. Fetched from the previous \
119
+ iteration's final result."
120
+ )
121
+ common_results: list[SolutionReferenceLite] = Field(
122
+ description="The common results (1 to 4) generated by gnimbus."
123
+ )
124
+ user_results: list[SolutionReferenceLite] = Field(
125
+ description="The user specific results generated by gnimbus in phases learning and crp."
126
+ )
127
+ personal_result_index: int | None = Field(
128
+ description="The user result index of requester."
129
+ )
130
+ final_result: SolutionReferenceLite | None = Field(
131
+ description="The final result after voting."
132
+ )
133
+
134
+
135
+ class GNIMBUSAllIterationsResponse(SQLModel):
136
+ """The response model for getting all found solutions among others."""
137
+
138
+ all_full_iterations: list[FullIteration]