desdeo 1.2__py3-none-any.whl → 2.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. desdeo/__init__.py +8 -8
  2. desdeo/adm/ADMAfsar.py +551 -0
  3. desdeo/adm/ADMChen.py +414 -0
  4. desdeo/adm/BaseADM.py +119 -0
  5. desdeo/adm/__init__.py +11 -0
  6. desdeo/api/README.md +73 -0
  7. desdeo/api/__init__.py +15 -0
  8. desdeo/api/app.py +50 -0
  9. desdeo/api/config.py +90 -0
  10. desdeo/api/config.toml +64 -0
  11. desdeo/api/db.py +27 -0
  12. desdeo/api/db_init.py +85 -0
  13. desdeo/api/db_models.py +164 -0
  14. desdeo/api/malaga_db_init.py +27 -0
  15. desdeo/api/models/__init__.py +266 -0
  16. desdeo/api/models/archive.py +23 -0
  17. desdeo/api/models/emo.py +128 -0
  18. desdeo/api/models/enautilus.py +69 -0
  19. desdeo/api/models/gdm/gdm_aggregate.py +139 -0
  20. desdeo/api/models/gdm/gdm_base.py +69 -0
  21. desdeo/api/models/gdm/gdm_score_bands.py +114 -0
  22. desdeo/api/models/gdm/gnimbus.py +138 -0
  23. desdeo/api/models/generic.py +104 -0
  24. desdeo/api/models/generic_states.py +401 -0
  25. desdeo/api/models/nimbus.py +158 -0
  26. desdeo/api/models/preference.py +128 -0
  27. desdeo/api/models/problem.py +717 -0
  28. desdeo/api/models/reference_point_method.py +18 -0
  29. desdeo/api/models/session.py +49 -0
  30. desdeo/api/models/state.py +463 -0
  31. desdeo/api/models/user.py +52 -0
  32. desdeo/api/models/utopia.py +25 -0
  33. desdeo/api/routers/_EMO.backup +309 -0
  34. desdeo/api/routers/_NAUTILUS.py +245 -0
  35. desdeo/api/routers/_NAUTILUS_navigator.py +233 -0
  36. desdeo/api/routers/_NIMBUS.py +765 -0
  37. desdeo/api/routers/__init__.py +5 -0
  38. desdeo/api/routers/emo.py +497 -0
  39. desdeo/api/routers/enautilus.py +237 -0
  40. desdeo/api/routers/gdm/gdm_aggregate.py +234 -0
  41. desdeo/api/routers/gdm/gdm_base.py +420 -0
  42. desdeo/api/routers/gdm/gdm_score_bands/gdm_score_bands_manager.py +398 -0
  43. desdeo/api/routers/gdm/gdm_score_bands/gdm_score_bands_routers.py +377 -0
  44. desdeo/api/routers/gdm/gnimbus/gnimbus_manager.py +698 -0
  45. desdeo/api/routers/gdm/gnimbus/gnimbus_routers.py +591 -0
  46. desdeo/api/routers/generic.py +233 -0
  47. desdeo/api/routers/nimbus.py +705 -0
  48. desdeo/api/routers/problem.py +307 -0
  49. desdeo/api/routers/reference_point_method.py +93 -0
  50. desdeo/api/routers/session.py +100 -0
  51. desdeo/api/routers/test.py +16 -0
  52. desdeo/api/routers/user_authentication.py +520 -0
  53. desdeo/api/routers/utils.py +187 -0
  54. desdeo/api/routers/utopia.py +230 -0
  55. desdeo/api/schema.py +100 -0
  56. desdeo/api/tests/__init__.py +0 -0
  57. desdeo/api/tests/conftest.py +151 -0
  58. desdeo/api/tests/test_enautilus.py +330 -0
  59. desdeo/api/tests/test_models.py +1179 -0
  60. desdeo/api/tests/test_routes.py +1075 -0
  61. desdeo/api/utils/_database.py +263 -0
  62. desdeo/api/utils/_logger.py +29 -0
  63. desdeo/api/utils/database.py +36 -0
  64. desdeo/api/utils/emo_database.py +40 -0
  65. desdeo/core.py +34 -0
  66. desdeo/emo/__init__.py +159 -0
  67. desdeo/emo/hooks/archivers.py +188 -0
  68. desdeo/emo/methods/EAs.py +541 -0
  69. desdeo/emo/methods/__init__.py +0 -0
  70. desdeo/emo/methods/bases.py +12 -0
  71. desdeo/emo/methods/templates.py +111 -0
  72. desdeo/emo/operators/__init__.py +1 -0
  73. desdeo/emo/operators/crossover.py +1282 -0
  74. desdeo/emo/operators/evaluator.py +114 -0
  75. desdeo/emo/operators/generator.py +459 -0
  76. desdeo/emo/operators/mutation.py +1224 -0
  77. desdeo/emo/operators/scalar_selection.py +202 -0
  78. desdeo/emo/operators/selection.py +1778 -0
  79. desdeo/emo/operators/termination.py +286 -0
  80. desdeo/emo/options/__init__.py +108 -0
  81. desdeo/emo/options/algorithms.py +435 -0
  82. desdeo/emo/options/crossover.py +164 -0
  83. desdeo/emo/options/generator.py +131 -0
  84. desdeo/emo/options/mutation.py +260 -0
  85. desdeo/emo/options/repair.py +61 -0
  86. desdeo/emo/options/scalar_selection.py +66 -0
  87. desdeo/emo/options/selection.py +127 -0
  88. desdeo/emo/options/templates.py +383 -0
  89. desdeo/emo/options/termination.py +143 -0
  90. desdeo/explanations/__init__.py +6 -0
  91. desdeo/explanations/explainer.py +100 -0
  92. desdeo/explanations/utils.py +90 -0
  93. desdeo/gdm/__init__.py +22 -0
  94. desdeo/gdm/gdmtools.py +45 -0
  95. desdeo/gdm/score_bands.py +114 -0
  96. desdeo/gdm/voting_rules.py +50 -0
  97. desdeo/mcdm/__init__.py +41 -0
  98. desdeo/mcdm/enautilus.py +338 -0
  99. desdeo/mcdm/gnimbus.py +484 -0
  100. desdeo/mcdm/nautili.py +345 -0
  101. desdeo/mcdm/nautilus.py +477 -0
  102. desdeo/mcdm/nautilus_navigator.py +656 -0
  103. desdeo/mcdm/nimbus.py +417 -0
  104. desdeo/mcdm/pareto_navigator.py +269 -0
  105. desdeo/mcdm/reference_point_method.py +186 -0
  106. desdeo/problem/__init__.py +83 -0
  107. desdeo/problem/evaluator.py +561 -0
  108. desdeo/problem/external/__init__.py +18 -0
  109. desdeo/problem/external/core.py +356 -0
  110. desdeo/problem/external/pymoo_provider.py +266 -0
  111. desdeo/problem/external/runtime.py +44 -0
  112. desdeo/problem/gurobipy_evaluator.py +562 -0
  113. desdeo/problem/infix_parser.py +341 -0
  114. desdeo/problem/json_parser.py +944 -0
  115. desdeo/problem/pyomo_evaluator.py +487 -0
  116. desdeo/problem/schema.py +1829 -0
  117. desdeo/problem/simulator_evaluator.py +348 -0
  118. desdeo/problem/sympy_evaluator.py +244 -0
  119. desdeo/problem/testproblems/__init__.py +88 -0
  120. desdeo/problem/testproblems/benchmarks_server.py +120 -0
  121. desdeo/problem/testproblems/binh_and_korn_problem.py +88 -0
  122. desdeo/problem/testproblems/cake_problem.py +185 -0
  123. desdeo/problem/testproblems/dmitry_forest_problem_discrete.py +71 -0
  124. desdeo/problem/testproblems/dtlz2_problem.py +102 -0
  125. desdeo/problem/testproblems/forest_problem.py +283 -0
  126. desdeo/problem/testproblems/knapsack_problem.py +163 -0
  127. desdeo/problem/testproblems/mcwb_problem.py +831 -0
  128. desdeo/problem/testproblems/mixed_variable_dimenrions_problem.py +83 -0
  129. desdeo/problem/testproblems/momip_problem.py +172 -0
  130. desdeo/problem/testproblems/multi_valued_constraints.py +119 -0
  131. desdeo/problem/testproblems/nimbus_problem.py +143 -0
  132. desdeo/problem/testproblems/pareto_navigator_problem.py +89 -0
  133. desdeo/problem/testproblems/re_problem.py +492 -0
  134. desdeo/problem/testproblems/river_pollution_problems.py +440 -0
  135. desdeo/problem/testproblems/rocket_injector_design_problem.py +140 -0
  136. desdeo/problem/testproblems/simple_problem.py +351 -0
  137. desdeo/problem/testproblems/simulator_problem.py +92 -0
  138. desdeo/problem/testproblems/single_objective.py +289 -0
  139. desdeo/problem/testproblems/spanish_sustainability_problem.py +945 -0
  140. desdeo/problem/testproblems/zdt_problem.py +274 -0
  141. desdeo/problem/utils.py +245 -0
  142. desdeo/tools/GenerateReferencePoints.py +181 -0
  143. desdeo/tools/__init__.py +120 -0
  144. desdeo/tools/desc_gen.py +22 -0
  145. desdeo/tools/generics.py +165 -0
  146. desdeo/tools/group_scalarization.py +3090 -0
  147. desdeo/tools/gurobipy_solver_interfaces.py +258 -0
  148. desdeo/tools/indicators_binary.py +117 -0
  149. desdeo/tools/indicators_unary.py +362 -0
  150. desdeo/tools/interaction_schema.py +38 -0
  151. desdeo/tools/intersection.py +54 -0
  152. desdeo/tools/iterative_pareto_representer.py +99 -0
  153. desdeo/tools/message.py +265 -0
  154. desdeo/tools/ng_solver_interfaces.py +199 -0
  155. desdeo/tools/non_dominated_sorting.py +134 -0
  156. desdeo/tools/patterns.py +283 -0
  157. desdeo/tools/proximal_solver.py +99 -0
  158. desdeo/tools/pyomo_solver_interfaces.py +477 -0
  159. desdeo/tools/reference_vectors.py +229 -0
  160. desdeo/tools/scalarization.py +2065 -0
  161. desdeo/tools/scipy_solver_interfaces.py +454 -0
  162. desdeo/tools/score_bands.py +627 -0
  163. desdeo/tools/utils.py +388 -0
  164. desdeo/tools/visualizations.py +67 -0
  165. desdeo/utopia_stuff/__init__.py +0 -0
  166. desdeo/utopia_stuff/data/1.json +15 -0
  167. desdeo/utopia_stuff/data/2.json +13 -0
  168. desdeo/utopia_stuff/data/3.json +15 -0
  169. desdeo/utopia_stuff/data/4.json +17 -0
  170. desdeo/utopia_stuff/data/5.json +15 -0
  171. desdeo/utopia_stuff/from_json.py +40 -0
  172. desdeo/utopia_stuff/reinit_user.py +38 -0
  173. desdeo/utopia_stuff/utopia_db_init.py +212 -0
  174. desdeo/utopia_stuff/utopia_problem.py +403 -0
  175. desdeo/utopia_stuff/utopia_problem_old.py +415 -0
  176. desdeo/utopia_stuff/utopia_reference_solutions.py +79 -0
  177. desdeo-2.1.0.dist-info/METADATA +186 -0
  178. desdeo-2.1.0.dist-info/RECORD +180 -0
  179. {desdeo-1.2.dist-info → desdeo-2.1.0.dist-info}/WHEEL +1 -1
  180. desdeo-2.1.0.dist-info/licenses/LICENSE +21 -0
  181. desdeo-1.2.dist-info/METADATA +0 -16
  182. desdeo-1.2.dist-info/RECORD +0 -4
@@ -0,0 +1,230 @@
1
+ """Utopia router."""
2
+
3
+ import json
4
+ from typing import Annotated
5
+
6
+ from fastapi import APIRouter, Depends
7
+ from sqlmodel import Session, select
8
+
9
+ from desdeo.api.db import get_session
10
+ from desdeo.api.models import (
11
+ ForestProblemMetaData,
12
+ NIMBUSFinalState,
13
+ NIMBUSInitializationState,
14
+ NIMBUSSaveState,
15
+ ProblemMetaDataDB,
16
+ StateDB,
17
+ User,
18
+ UtopiaRequest,
19
+ UtopiaResponse,
20
+ )
21
+ from desdeo.api.routers.user_authentication import get_current_user
22
+
23
+ router = APIRouter(prefix="/utopia")
24
+
25
+
26
+ @router.post("/")
27
+ def get_utopia_data(
28
+ request: UtopiaRequest,
29
+ user: Annotated[User, Depends(get_current_user)],
30
+ session: Annotated[Session, Depends(get_session)],
31
+ ) -> UtopiaResponse:
32
+ """Request and receive the Utopia map corresponding to the decision variables sent.
33
+
34
+ Args:
35
+ request (UtopiaRequest): the set of decision variables and problem for which the utopia forest map is requested
36
+ for.
37
+ user (Annotated[User, Depend(get_current_user)]) the current user
38
+ session (Annotated[Session, Depends(get_session)]) the current database session
39
+ Raises:
40
+ HTTPException:
41
+ Returns:
42
+ UtopiaResponse: the map for the forest, to be rendered in frontend
43
+ """
44
+ empty_response = UtopiaResponse(is_utopia=False, map_name="", map_json={}, options={}, description="", years=[])
45
+
46
+ state = session.exec(select(StateDB).where(StateDB.id == request.solution.state_id)).first()
47
+ if state is None or not hasattr(state, "state"):
48
+ return empty_response
49
+
50
+ actual_state = state.state
51
+
52
+ if type(actual_state) is NIMBUSSaveState:
53
+ decision_variables = actual_state.result_variable_values[0]
54
+
55
+ elif type(actual_state) in [NIMBUSInitializationState, NIMBUSFinalState]:
56
+ decision_variables = actual_state.solver_results.optimal_variables
57
+
58
+ else:
59
+ # Check if solver_results exists and has the needed index
60
+ if (
61
+ not hasattr(actual_state, "solver_results")
62
+ or request.solution.solution_index >= len(actual_state.solver_results)
63
+ or actual_state.solver_results[request.solution.solution_index] is None
64
+ ):
65
+ return empty_response
66
+
67
+ result = actual_state.solver_results[request.solution.solution_index]
68
+ if not hasattr(result, "optimal_variables") or not result.optimal_variables:
69
+ return empty_response
70
+ decision_variables = result.optimal_variables # expects a list of variables, won't work without.
71
+
72
+ from_db_metadata = session.exec(
73
+ select(ProblemMetaDataDB).where(ProblemMetaDataDB.problem_id == request.problem_id)
74
+ ).first()
75
+ if from_db_metadata is None:
76
+ return empty_response
77
+
78
+ # Get the last instance of forest related metadata from the database.
79
+ # If for some reason there's more than one forest metadata, return the latest.
80
+ forest_metadata: ForestProblemMetaData = [
81
+ metadata for metadata in from_db_metadata.all_metadata if metadata.metadata_type == "forest_problem_metadata"
82
+ ][-1]
83
+ if forest_metadata is None:
84
+ return empty_response
85
+
86
+ # Figure out the treatments from the decision variables and utopia data
87
+
88
+ def treatment_index(part: str) -> str:
89
+ if "clearcut" in part:
90
+ return 1
91
+ if "below" in part:
92
+ return 2
93
+ if "above" in part:
94
+ return 3
95
+ if "even" in part:
96
+ return 4
97
+ if "first" in part:
98
+ return 5
99
+ return -1
100
+
101
+ treatments_dict = {}
102
+ for key in decision_variables:
103
+ if not key.startswith("X"):
104
+ continue
105
+ # The dict keys get converted to ints to strings when it's loaded from database
106
+ try:
107
+ treatments = forest_metadata.schedule_dict[key][str(decision_variables[key].index(1))]
108
+ except ValueError as e:
109
+ # if the optimization didn't choose any decision alternative, it's safe to assume
110
+ # that nothing is being done at that forest stand
111
+ treatments = forest_metadata.schedule_dict[key]["0"]
112
+ # print(e)
113
+ treatments_dict[key] = {forest_metadata.years[0]: 0, forest_metadata.years[1]: 0, forest_metadata.years[2]: 0}
114
+ for year in treatments_dict[key]:
115
+ if year in treatments:
116
+ for part in treatments.split():
117
+ if year in part:
118
+ treatments_dict[key][year] = treatment_index(part)
119
+
120
+ # Create the options for the webui
121
+
122
+ treatment_colors = {
123
+ 0: "#4daf4a",
124
+ 1: "#e41a1c",
125
+ 2: "#984ea3",
126
+ 3: "#e3d802",
127
+ 4: "#ff7f00",
128
+ 5: "#377eb8",
129
+ }
130
+
131
+ description_dict = {
132
+ 0: "Do nothing",
133
+ 1: "Clearcut",
134
+ 2: "Thinning from below",
135
+ 3: "Thinning from above",
136
+ 4: "Even thinning",
137
+ 5: "First thinning",
138
+ }
139
+
140
+ map_name = "ForestMap" # This isn't visible anywhere on the ui
141
+
142
+ options = {}
143
+ for year in forest_metadata.years:
144
+ options[year] = {
145
+ "tooltip": {
146
+ "trigger": "item",
147
+ "showDelay": 0,
148
+ "transitionDuration": 0.2,
149
+ },
150
+ "visualMap": { # // vis eg. stock levels
151
+ "left": "right",
152
+ "showLabel": True,
153
+ "type": "piecewise", # // for different plans
154
+ "pieces": [],
155
+ "text": ["Management plans"],
156
+ "calculable": True,
157
+ },
158
+ # // predefined symbols for visumap'circle': 'rect': 'roundRect': 'triangle': 'diamond': 'pin':'arrow':
159
+ # // can give custom svgs also
160
+ "toolbox": {
161
+ "show": True,
162
+ # //orient: 'vertical',
163
+ "left": "left",
164
+ "top": "top",
165
+ "feature": {
166
+ "dataView": {"readOnly": True},
167
+ "restore": {},
168
+ "saveAsImage": {},
169
+ },
170
+ },
171
+ # // can draw graphic components to indicate different things at least
172
+ "series": [
173
+ {
174
+ "name": year,
175
+ "type": "map",
176
+ "roam": True,
177
+ "map": map_name,
178
+ "nameProperty": forest_metadata.stand_id_field,
179
+ "label": {
180
+ "show": False # Hide text labels on the map
181
+ },
182
+ # "colorBy": "data",
183
+ # "itemStyle": {"symbol": "triangle", "color": "red"},
184
+ "data": [],
185
+ "nameMap": {},
186
+ }
187
+ ],
188
+ }
189
+
190
+ for key in decision_variables:
191
+ if not key.startswith("X"):
192
+ continue
193
+ stand = int(forest_metadata.schedule_dict[key]["unit"])
194
+ treatment_id = treatments_dict[key][year]
195
+ piece = {
196
+ "value": treatment_id,
197
+ "symbol": "circle",
198
+ "label": description_dict[treatment_id],
199
+ "color": treatment_colors[treatment_id],
200
+ }
201
+ if piece not in options[year]["visualMap"]["pieces"]:
202
+ options[year]["visualMap"]["pieces"].append(piece)
203
+ if forest_metadata.stand_descriptor:
204
+ name = forest_metadata.stand_descriptor[str(stand)] + description_dict[treatment_id]
205
+ else:
206
+ name = "Stand " + str(stand) + " " + description_dict[treatment_id]
207
+ options[year]["series"][0]["data"].append(
208
+ {
209
+ "name": name,
210
+ "value": treatment_id,
211
+ }
212
+ )
213
+ options[year]["series"][0]["nameMap"][stand] = name
214
+
215
+ # Let's also generate a nice description for the map
216
+ map_description = (
217
+ f"Income from harvesting in the first period {int(decision_variables['P_1'])}€.\n"
218
+ + f"Income from harvesting in the second period {int(decision_variables['P_2'])}€.\n"
219
+ + f"Income from harvesting in the third period {int(decision_variables['P_3'])}€.\n"
220
+ + f"The discounted value of the remaining forest at the end of the plan {int(decision_variables['V_end'])}€."
221
+ )
222
+
223
+ return UtopiaResponse(
224
+ is_utopia=True,
225
+ map_name=map_name,
226
+ options=options,
227
+ map_json=json.loads(forest_metadata.map_json),
228
+ description=map_description,
229
+ years=forest_metadata.years,
230
+ )
desdeo/api/schema.py ADDED
@@ -0,0 +1,100 @@
1
+ """Pydantic schemas for the API."""
2
+
3
+ from enum import Enum
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+
7
+
8
+ class UserRole(str, Enum):
9
+ """Enum of user roles."""
10
+
11
+ GUEST = "guest"
12
+ DM = "dm"
13
+ ANALYST = "analyst"
14
+
15
+
16
+ class UserPrivileges(str, Enum):
17
+ """Enum of user privileges."""
18
+
19
+ CREATE_PROBLEMS = "Create problems"
20
+ CREATE_USERS = "Create users"
21
+ ACCESS_ALL_PROBLEMS = "Access all problems"
22
+ EDIT_USERS = "Change user privileges, roles, groups, etc."
23
+
24
+
25
+ class ProblemKind(str, Enum):
26
+ """Enum of problem kinds."""
27
+
28
+ CONTINUOUS = "continuous"
29
+ DISCRETE = "discrete"
30
+ MIXED = "mixed"
31
+ BINARY = "binary"
32
+
33
+
34
+ class ObjectiveKind(str, Enum):
35
+ """Enum of objective kinds."""
36
+
37
+ ANALYTICAL = "analytical"
38
+ DATABASED = "databased"
39
+ SIMULATED = "simulated"
40
+ SURROGATE = "surrogate"
41
+
42
+
43
+ class Methods(str, Enum):
44
+ """Enum of methods."""
45
+
46
+ NIMBUS = "nimbus"
47
+ NAUTILUS = "nautilus"
48
+ NAUT_NAVIGATOR = "NAUTILUS navigator"
49
+ NAUTILUSII = "nautilusII"
50
+ EMO = "EMO"
51
+
52
+
53
+ class MethodProperties(str, Enum):
54
+ """Enum of method properties."""
55
+
56
+ INTERACTIVE = "interactive"
57
+ REFERENCE_POINT = "reference_point"
58
+ CLASSIFICATION = "classification"
59
+ BOUNDS = "bounds"
60
+ PREFERRED_SOLUTIONS = "preferred_solutions"
61
+ NON_PREFERRED_SOLUTIONS = "non_preferred_solutions"
62
+ # TODO: Add more properties as needed.
63
+
64
+
65
+ class User(BaseModel):
66
+ """Model for a user. Temporary."""
67
+
68
+ username: str = Field(description="Username of the user.")
69
+ index: int | None = Field(
70
+ description=(
71
+ "Index of the user in the database. "
72
+ "Supposed to be automatically generated by the database. "
73
+ "So the programmer should not have to worry about it."
74
+ )
75
+ )
76
+ password_hash: str = Field(description="SHA256 Hash of the user's password.")
77
+ role: UserRole = Field(description="Role of the user.")
78
+ privileges: list[UserPrivileges] = Field(
79
+ description="List of privileges the user has."
80
+ )
81
+ user_group: str = Field(
82
+ description="User group of the user. Used for group decision making."
83
+ )
84
+ # To allows for User to be initialized from database instead of just dicts.
85
+ model_config = ConfigDict(from_attributes=True)
86
+
87
+
88
+ class Solvers(Enum):
89
+ """Enum of available solvers."""
90
+
91
+ # These should match available_solvers in desdeo.tools.utils
92
+
93
+ SCIPY_MIN = "scipy_minimize"
94
+ SCIPY_DE = "scipy_de"
95
+ PROXIMAL = "proximal"
96
+ NEVERGRAD = "nevergrad"
97
+ PYOMO_BONMIN = "pyomo_bonmin"
98
+ PYOMO_IPOPT = "pyomo_ipopt"
99
+ PYOMO_GUROBI = "pyomo_gurobi"
100
+ GUROBIPY = "gurobipy"
File without changes
@@ -0,0 +1,151 @@
1
+ """General fixtures for API tests are defined here."""
2
+
3
+ import io
4
+ from pathlib import Path
5
+
6
+ import polars as pl
7
+ import pytest
8
+ from fastapi.testclient import TestClient
9
+ from sqlmodel import Session, SQLModel, create_engine
10
+ from sqlmodel.pool import StaticPool
11
+
12
+ from desdeo.api.app import app
13
+ from desdeo.api.db import get_session
14
+ from desdeo.api.models import (
15
+ ForestProblemMetaData,
16
+ ProblemDB,
17
+ ProblemMetaDataDB,
18
+ RepresentativeNonDominatedSolutions,
19
+ User,
20
+ UserRole,
21
+ )
22
+ from desdeo.api.routers.user_authentication import get_password_hash
23
+ from desdeo.problem.testproblems import dtlz2, river_pollution_problem, dmitry_forest_problem_disc
24
+
25
+
26
+ @pytest.fixture(name="session_and_user", scope="function")
27
+ def session_fixture():
28
+ """Create a session for testing."""
29
+ engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool)
30
+
31
+ SQLModel.metadata.create_all(engine)
32
+
33
+ with Session(engine) as session:
34
+ user_analyst = User(
35
+ username="analyst",
36
+ password_hash=get_password_hash("analyst"),
37
+ role=UserRole.analyst,
38
+ group="test",
39
+ )
40
+ session.add(user_analyst)
41
+ session.commit()
42
+ session.refresh(user_analyst)
43
+
44
+ problem_db = ProblemDB.from_problem(dtlz2(5, 3), user=user_analyst)
45
+ session.add(problem_db)
46
+ session.commit()
47
+ session.refresh(problem_db)
48
+
49
+ problem_db_river = ProblemDB.from_problem(river_pollution_problem(), user=user_analyst)
50
+ session.add(problem_db_river)
51
+ session.commit()
52
+ session.refresh(problem_db_river)
53
+
54
+ metadata = ProblemMetaDataDB(problem_id=problem_db_river.id)
55
+ session.add(metadata)
56
+ session.commit()
57
+ session.refresh(metadata)
58
+
59
+ data_path = Path(__file__).parent.parent.parent.parent / "datasets" / "river_pollution_non_dom.parquet"
60
+ df = pl.read_parquet(data_path)
61
+ dict_data = df.to_dict(as_series=False)
62
+ river_nondominated_meta = RepresentativeNonDominatedSolutions(
63
+ metadata_id=metadata.id,
64
+ name="Non-dom-solutions",
65
+ description=(
66
+ "Set of non-dominated solutions representing the Pareto optimal "
67
+ "solutions of the river pollution problem."
68
+ ),
69
+ solution_data=dict_data,
70
+ ideal={},
71
+ nadir={},
72
+ )
73
+
74
+ session.add(river_nondominated_meta)
75
+ session.commit()
76
+
77
+ forest_metadata = ForestProblemMetaData(
78
+ metadata_id=metadata.id,
79
+ map_json="type: string",
80
+ schedule_dict={"type": "dict"},
81
+ years=["type:", "list", "of", "strings"],
82
+ stand_id_field="type: string",
83
+ )
84
+
85
+ session.add(forest_metadata)
86
+ session.commit()
87
+
88
+ problem_db_discrete = ProblemDB.from_problem(
89
+ dmitry_forest_problem_disc(),
90
+ user=user_analyst
91
+ )
92
+ session.add(problem_db_discrete)
93
+ session.commit()
94
+ session.refresh(problem_db_discrete)
95
+
96
+ yield {"session": session, "user": user_analyst}
97
+ session.rollback()
98
+
99
+
100
+ @pytest.fixture(name="client", scope="function")
101
+ def client_fixture(session_and_user):
102
+ """Create a client for testing."""
103
+
104
+ def get_session_override():
105
+ return session_and_user["session"]
106
+
107
+ app.dependency_overrides[get_session] = get_session_override
108
+ client = TestClient(app)
109
+
110
+ yield client
111
+
112
+ app.dependency_overrides.clear()
113
+
114
+
115
+ def login(client: TestClient, username="analyst", password="analyst") -> str: # noqa: S107
116
+ """Login, returns the access token."""
117
+ response_login = client.post(
118
+ "/login",
119
+ data={"username": username, "password": password, "grant_type": "password"},
120
+ headers={"content-type": "application/x-www-form-urlencoded"},
121
+ ).json()
122
+
123
+ return response_login["access_token"]
124
+
125
+
126
+ def post_json(client: TestClient, endpoint: str, json: dict, access_token: str):
127
+ """Makes a post request and returns the response."""
128
+ return client.post(
129
+ endpoint,
130
+ json=json,
131
+ headers={"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"},
132
+ )
133
+
134
+
135
+ def post_file_multipart(
136
+ client: TestClient, endpoint: str, file_bytes: bytes, access_token: str, filename: str = "test.json"
137
+ ):
138
+ """Makes a post request with an uploaded file and returns the response."""
139
+ return client.post(
140
+ endpoint,
141
+ files={"json_file": (filename, io.BytesIO(file_bytes), "application/json")},
142
+ headers={"Authorization": f"Bearer {access_token}"},
143
+ )
144
+
145
+
146
+ def get_json(client: TestClient, endpoint: str, access_token: str):
147
+ """Makes a get request and returns the response."""
148
+ return client.get(
149
+ endpoint,
150
+ headers={"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"},
151
+ )