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.
- desdeo/__init__.py +8 -8
- desdeo/adm/ADMAfsar.py +551 -0
- desdeo/adm/ADMChen.py +414 -0
- desdeo/adm/BaseADM.py +119 -0
- desdeo/adm/__init__.py +11 -0
- desdeo/api/README.md +73 -0
- desdeo/api/__init__.py +15 -0
- desdeo/api/app.py +50 -0
- desdeo/api/config.py +90 -0
- desdeo/api/config.toml +64 -0
- desdeo/api/db.py +27 -0
- desdeo/api/db_init.py +85 -0
- desdeo/api/db_models.py +164 -0
- desdeo/api/malaga_db_init.py +27 -0
- desdeo/api/models/__init__.py +266 -0
- desdeo/api/models/archive.py +23 -0
- desdeo/api/models/emo.py +128 -0
- desdeo/api/models/enautilus.py +69 -0
- desdeo/api/models/gdm/gdm_aggregate.py +139 -0
- desdeo/api/models/gdm/gdm_base.py +69 -0
- desdeo/api/models/gdm/gdm_score_bands.py +114 -0
- desdeo/api/models/gdm/gnimbus.py +138 -0
- desdeo/api/models/generic.py +104 -0
- desdeo/api/models/generic_states.py +401 -0
- desdeo/api/models/nimbus.py +158 -0
- desdeo/api/models/preference.py +128 -0
- desdeo/api/models/problem.py +717 -0
- desdeo/api/models/reference_point_method.py +18 -0
- desdeo/api/models/session.py +49 -0
- desdeo/api/models/state.py +463 -0
- desdeo/api/models/user.py +52 -0
- desdeo/api/models/utopia.py +25 -0
- desdeo/api/routers/_EMO.backup +309 -0
- desdeo/api/routers/_NAUTILUS.py +245 -0
- desdeo/api/routers/_NAUTILUS_navigator.py +233 -0
- desdeo/api/routers/_NIMBUS.py +765 -0
- desdeo/api/routers/__init__.py +5 -0
- desdeo/api/routers/emo.py +497 -0
- desdeo/api/routers/enautilus.py +237 -0
- desdeo/api/routers/gdm/gdm_aggregate.py +234 -0
- desdeo/api/routers/gdm/gdm_base.py +420 -0
- desdeo/api/routers/gdm/gdm_score_bands/gdm_score_bands_manager.py +398 -0
- desdeo/api/routers/gdm/gdm_score_bands/gdm_score_bands_routers.py +377 -0
- desdeo/api/routers/gdm/gnimbus/gnimbus_manager.py +698 -0
- desdeo/api/routers/gdm/gnimbus/gnimbus_routers.py +591 -0
- desdeo/api/routers/generic.py +233 -0
- desdeo/api/routers/nimbus.py +705 -0
- desdeo/api/routers/problem.py +307 -0
- desdeo/api/routers/reference_point_method.py +93 -0
- desdeo/api/routers/session.py +100 -0
- desdeo/api/routers/test.py +16 -0
- desdeo/api/routers/user_authentication.py +520 -0
- desdeo/api/routers/utils.py +187 -0
- desdeo/api/routers/utopia.py +230 -0
- desdeo/api/schema.py +100 -0
- desdeo/api/tests/__init__.py +0 -0
- desdeo/api/tests/conftest.py +151 -0
- desdeo/api/tests/test_enautilus.py +330 -0
- desdeo/api/tests/test_models.py +1179 -0
- desdeo/api/tests/test_routes.py +1075 -0
- desdeo/api/utils/_database.py +263 -0
- desdeo/api/utils/_logger.py +29 -0
- desdeo/api/utils/database.py +36 -0
- desdeo/api/utils/emo_database.py +40 -0
- desdeo/core.py +34 -0
- desdeo/emo/__init__.py +159 -0
- desdeo/emo/hooks/archivers.py +188 -0
- desdeo/emo/methods/EAs.py +541 -0
- desdeo/emo/methods/__init__.py +0 -0
- desdeo/emo/methods/bases.py +12 -0
- desdeo/emo/methods/templates.py +111 -0
- desdeo/emo/operators/__init__.py +1 -0
- desdeo/emo/operators/crossover.py +1282 -0
- desdeo/emo/operators/evaluator.py +114 -0
- desdeo/emo/operators/generator.py +459 -0
- desdeo/emo/operators/mutation.py +1224 -0
- desdeo/emo/operators/scalar_selection.py +202 -0
- desdeo/emo/operators/selection.py +1778 -0
- desdeo/emo/operators/termination.py +286 -0
- desdeo/emo/options/__init__.py +108 -0
- desdeo/emo/options/algorithms.py +435 -0
- desdeo/emo/options/crossover.py +164 -0
- desdeo/emo/options/generator.py +131 -0
- desdeo/emo/options/mutation.py +260 -0
- desdeo/emo/options/repair.py +61 -0
- desdeo/emo/options/scalar_selection.py +66 -0
- desdeo/emo/options/selection.py +127 -0
- desdeo/emo/options/templates.py +383 -0
- desdeo/emo/options/termination.py +143 -0
- desdeo/explanations/__init__.py +6 -0
- desdeo/explanations/explainer.py +100 -0
- desdeo/explanations/utils.py +90 -0
- desdeo/gdm/__init__.py +22 -0
- desdeo/gdm/gdmtools.py +45 -0
- desdeo/gdm/score_bands.py +114 -0
- desdeo/gdm/voting_rules.py +50 -0
- desdeo/mcdm/__init__.py +41 -0
- desdeo/mcdm/enautilus.py +338 -0
- desdeo/mcdm/gnimbus.py +484 -0
- desdeo/mcdm/nautili.py +345 -0
- desdeo/mcdm/nautilus.py +477 -0
- desdeo/mcdm/nautilus_navigator.py +656 -0
- desdeo/mcdm/nimbus.py +417 -0
- desdeo/mcdm/pareto_navigator.py +269 -0
- desdeo/mcdm/reference_point_method.py +186 -0
- desdeo/problem/__init__.py +83 -0
- desdeo/problem/evaluator.py +561 -0
- desdeo/problem/external/__init__.py +18 -0
- desdeo/problem/external/core.py +356 -0
- desdeo/problem/external/pymoo_provider.py +266 -0
- desdeo/problem/external/runtime.py +44 -0
- desdeo/problem/gurobipy_evaluator.py +562 -0
- desdeo/problem/infix_parser.py +341 -0
- desdeo/problem/json_parser.py +944 -0
- desdeo/problem/pyomo_evaluator.py +487 -0
- desdeo/problem/schema.py +1829 -0
- desdeo/problem/simulator_evaluator.py +348 -0
- desdeo/problem/sympy_evaluator.py +244 -0
- desdeo/problem/testproblems/__init__.py +88 -0
- desdeo/problem/testproblems/benchmarks_server.py +120 -0
- desdeo/problem/testproblems/binh_and_korn_problem.py +88 -0
- desdeo/problem/testproblems/cake_problem.py +185 -0
- desdeo/problem/testproblems/dmitry_forest_problem_discrete.py +71 -0
- desdeo/problem/testproblems/dtlz2_problem.py +102 -0
- desdeo/problem/testproblems/forest_problem.py +283 -0
- desdeo/problem/testproblems/knapsack_problem.py +163 -0
- desdeo/problem/testproblems/mcwb_problem.py +831 -0
- desdeo/problem/testproblems/mixed_variable_dimenrions_problem.py +83 -0
- desdeo/problem/testproblems/momip_problem.py +172 -0
- desdeo/problem/testproblems/multi_valued_constraints.py +119 -0
- desdeo/problem/testproblems/nimbus_problem.py +143 -0
- desdeo/problem/testproblems/pareto_navigator_problem.py +89 -0
- desdeo/problem/testproblems/re_problem.py +492 -0
- desdeo/problem/testproblems/river_pollution_problems.py +440 -0
- desdeo/problem/testproblems/rocket_injector_design_problem.py +140 -0
- desdeo/problem/testproblems/simple_problem.py +351 -0
- desdeo/problem/testproblems/simulator_problem.py +92 -0
- desdeo/problem/testproblems/single_objective.py +289 -0
- desdeo/problem/testproblems/spanish_sustainability_problem.py +945 -0
- desdeo/problem/testproblems/zdt_problem.py +274 -0
- desdeo/problem/utils.py +245 -0
- desdeo/tools/GenerateReferencePoints.py +181 -0
- desdeo/tools/__init__.py +120 -0
- desdeo/tools/desc_gen.py +22 -0
- desdeo/tools/generics.py +165 -0
- desdeo/tools/group_scalarization.py +3090 -0
- desdeo/tools/gurobipy_solver_interfaces.py +258 -0
- desdeo/tools/indicators_binary.py +117 -0
- desdeo/tools/indicators_unary.py +362 -0
- desdeo/tools/interaction_schema.py +38 -0
- desdeo/tools/intersection.py +54 -0
- desdeo/tools/iterative_pareto_representer.py +99 -0
- desdeo/tools/message.py +265 -0
- desdeo/tools/ng_solver_interfaces.py +199 -0
- desdeo/tools/non_dominated_sorting.py +134 -0
- desdeo/tools/patterns.py +283 -0
- desdeo/tools/proximal_solver.py +99 -0
- desdeo/tools/pyomo_solver_interfaces.py +477 -0
- desdeo/tools/reference_vectors.py +229 -0
- desdeo/tools/scalarization.py +2065 -0
- desdeo/tools/scipy_solver_interfaces.py +454 -0
- desdeo/tools/score_bands.py +627 -0
- desdeo/tools/utils.py +388 -0
- desdeo/tools/visualizations.py +67 -0
- desdeo/utopia_stuff/__init__.py +0 -0
- desdeo/utopia_stuff/data/1.json +15 -0
- desdeo/utopia_stuff/data/2.json +13 -0
- desdeo/utopia_stuff/data/3.json +15 -0
- desdeo/utopia_stuff/data/4.json +17 -0
- desdeo/utopia_stuff/data/5.json +15 -0
- desdeo/utopia_stuff/from_json.py +40 -0
- desdeo/utopia_stuff/reinit_user.py +38 -0
- desdeo/utopia_stuff/utopia_db_init.py +212 -0
- desdeo/utopia_stuff/utopia_problem.py +403 -0
- desdeo/utopia_stuff/utopia_problem_old.py +415 -0
- desdeo/utopia_stuff/utopia_reference_solutions.py +79 -0
- desdeo-2.1.0.dist-info/METADATA +186 -0
- desdeo-2.1.0.dist-info/RECORD +180 -0
- {desdeo-1.2.dist-info → desdeo-2.1.0.dist-info}/WHEEL +1 -1
- desdeo-2.1.0.dist-info/licenses/LICENSE +21 -0
- desdeo-1.2.dist-info/METADATA +0 -16
- 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
|
+
)
|