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
desdeo/api/schema.py CHANGED
@@ -47,8 +47,7 @@ class Methods(str, Enum):
47
47
  NAUTILUS = "nautilus"
48
48
  NAUT_NAVIGATOR = "NAUTILUS navigator"
49
49
  NAUTILUSII = "nautilusII"
50
- RVEA = "RVEA"
51
- NSGAIII = "NSGAIII"
50
+ EMO = "EMO"
52
51
 
53
52
 
54
53
  class MethodProperties(str, Enum):
@@ -57,6 +56,9 @@ class MethodProperties(str, Enum):
57
56
  INTERACTIVE = "interactive"
58
57
  REFERENCE_POINT = "reference_point"
59
58
  CLASSIFICATION = "classification"
59
+ BOUNDS = "bounds"
60
+ PREFERRED_SOLUTIONS = "preferred_solutions"
61
+ NON_PREFERRED_SOLUTIONS = "non_preferred_solutions"
60
62
  # TODO: Add more properties as needed.
61
63
 
62
64
 
@@ -73,8 +75,12 @@ class User(BaseModel):
73
75
  )
74
76
  password_hash: str = Field(description="SHA256 Hash of the user's password.")
75
77
  role: UserRole = Field(description="Role of the user.")
76
- privileges: list[UserPrivileges] = Field(description="List of privileges the user has.")
77
- user_group: str = Field(description="User group of the user. Used for group decision making.")
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
+ )
78
84
  # To allows for User to be initialized from database instead of just dicts.
79
85
  model_config = ConfigDict(from_attributes=True)
80
86
 
@@ -1,5 +1,9 @@
1
1
  """General fixtures for API tests are defined here."""
2
2
 
3
+ import io
4
+ from pathlib import Path
5
+
6
+ import polars as pl
3
7
  import pytest
4
8
  from fastapi.testclient import TestClient
5
9
  from sqlmodel import Session, SQLModel, create_engine
@@ -7,9 +11,16 @@ from sqlmodel.pool import StaticPool
7
11
 
8
12
  from desdeo.api.app import app
9
13
  from desdeo.api.db import get_session
10
- from desdeo.api.models import ProblemDB, User, UserRole
14
+ from desdeo.api.models import (
15
+ ForestProblemMetaData,
16
+ ProblemDB,
17
+ ProblemMetaDataDB,
18
+ RepresentativeNonDominatedSolutions,
19
+ User,
20
+ UserRole,
21
+ )
11
22
  from desdeo.api.routers.user_authentication import get_password_hash
12
- from desdeo.problem.testproblems import dtlz2, river_pollution_problem
23
+ from desdeo.problem.testproblems import dtlz2, river_pollution_problem, dmitry_forest_problem_disc
13
24
 
14
25
 
15
26
  @pytest.fixture(name="session_and_user", scope="function")
@@ -40,6 +51,48 @@ def session_fixture():
40
51
  session.commit()
41
52
  session.refresh(problem_db_river)
42
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
+
43
96
  yield {"session": session, "user": user_analyst}
44
97
  session.rollback()
45
98
 
@@ -57,3 +110,42 @@ def client_fixture(session_and_user):
57
110
  yield client
58
111
 
59
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
+ )
@@ -0,0 +1,330 @@
1
+ """Tests related to E-NAUTILUS models and routes."""
2
+
3
+ import json
4
+
5
+ import numpy as np
6
+ from fastapi.testclient import TestClient
7
+ from sqlmodel import Session, select
8
+
9
+ from desdeo.api.models import (
10
+ ENautilusRepresentativeSolutionsResponse,
11
+ ENautilusStateResponse,
12
+ ENautilusStepRequest,
13
+ ENautilusStepResponse,
14
+ ProblemDB,
15
+ ProblemMetaDataDB,
16
+ RepresentativeNonDominatedSolutions,
17
+ user,
18
+ )
19
+
20
+ from .conftest import get_json, login, post_json
21
+
22
+
23
+ def test_enautilus_step_request(session_and_user: dict[str, Session | list[user]]):
24
+ """Tests the E-NAUTILUS step request model."""
25
+ problem_id = 42
26
+ session_id = 69
27
+ parent_state_id = 20
28
+ representative_solutions_id = 10
29
+
30
+ current_iteration = 3
31
+ iterations_left = 4
32
+ selected_point = {"f_1": 2.3, "f_2": 4.3, "f_3": -22222.3}
33
+ reachable_point_indices = [1, 2, 3, 46, 9]
34
+ number_of_intermediate_points = 9001
35
+
36
+ request = ENautilusStepRequest(
37
+ problem_id=problem_id,
38
+ session_id=session_id,
39
+ parent_state_id=parent_state_id,
40
+ representative_solutions_id=representative_solutions_id,
41
+ current_iteration=current_iteration,
42
+ iterations_left=iterations_left,
43
+ selected_point=selected_point,
44
+ reachable_point_indices=reachable_point_indices,
45
+ number_of_intermediate_points=number_of_intermediate_points,
46
+ )
47
+
48
+ assert request.problem_id == problem_id
49
+ assert request.session_id == session_id
50
+ assert request.parent_state_id == parent_state_id
51
+ assert request.current_iteration == current_iteration
52
+ assert request.iterations_left == iterations_left
53
+ assert request.selected_point == selected_point
54
+ assert request.reachable_point_indices == reachable_point_indices
55
+ assert request.number_of_intermediate_points == number_of_intermediate_points
56
+
57
+
58
+ def test_enautilus_step_router(client: TestClient, session_and_user: dict[str, Session | list[user]]):
59
+ """Test the E-NAUTILUS stepping endpoint."""
60
+ session, user = session_and_user["session"], session_and_user["user"]
61
+ access_token = login(client)
62
+
63
+ problem_db = session.exec(select(ProblemDB).where(ProblemDB.name == "The river pollution problem")).first()
64
+
65
+ representative_solutions = session.exec(
66
+ select(RepresentativeNonDominatedSolutions)
67
+ .join(RepresentativeNonDominatedSolutions.metadata_instance)
68
+ .where(ProblemMetaDataDB.problem_id == problem_db.id)
69
+ ).first()
70
+
71
+ current_iteration = 0
72
+ total_iterations = 5
73
+ selected_point = None # nadir point
74
+ reachable_point_indices = [] # empty for first iteration
75
+ number_of_intermediate_points = 3
76
+
77
+ request = ENautilusStepRequest(
78
+ problem_id=problem_db.id,
79
+ session_id=None,
80
+ parent_state_id=None,
81
+ representative_solutions_id=representative_solutions.id,
82
+ current_iteration=current_iteration,
83
+ iterations_left=total_iterations,
84
+ selected_point=selected_point,
85
+ reachable_point_indices=reachable_point_indices,
86
+ number_of_intermediate_points=number_of_intermediate_points,
87
+ )
88
+
89
+ raw_response = post_json(client, "/method/enautilus/step", request.model_dump(), access_token)
90
+
91
+ assert raw_response.status_code == 200 # OK
92
+
93
+ step_response = ENautilusStepResponse.model_validate(json.loads(raw_response.content))
94
+
95
+ assert step_response.state_id == 1 # there should be no prior StateDB in the test instance of the DB
96
+ assert step_response.current_iteration == current_iteration + 1 # advances by 1
97
+ assert step_response.iterations_left == total_iterations - 1 # decrement by 1
98
+ assert len(step_response.intermediate_points) == number_of_intermediate_points
99
+ assert len(step_response.reachable_best_bounds) == number_of_intermediate_points # one for each point
100
+ assert len(step_response.reachable_worst_bounds) == number_of_intermediate_points # one for each point
101
+ assert len(step_response.closeness_measures) == number_of_intermediate_points # one for each point
102
+ assert len(step_response.reachable_point_indices) == number_of_intermediate_points # one set for each point
103
+
104
+ # second iteration
105
+ previous_step = step_response
106
+ new_number_of_intermediate_points = 2
107
+
108
+ request = ENautilusStepRequest(
109
+ problem_id=problem_db.id,
110
+ session_id=None,
111
+ parent_state_id=previous_step.state_id,
112
+ representative_solutions_id=representative_solutions.id,
113
+ current_iteration=previous_step.current_iteration,
114
+ iterations_left=previous_step.iterations_left,
115
+ selected_point=previous_step.intermediate_points[0],
116
+ reachable_point_indices=previous_step.reachable_point_indices[0],
117
+ number_of_intermediate_points=new_number_of_intermediate_points,
118
+ )
119
+
120
+ raw_response = post_json(client, "/method/enautilus/step", request.model_dump(), access_token)
121
+
122
+ assert raw_response.status_code == 200 # OK
123
+
124
+ step_response = ENautilusStepResponse.model_validate(json.loads(raw_response.content))
125
+
126
+ assert step_response.state_id == 2 # there should be one prior StateDB in the test instance of the DB
127
+ assert step_response.current_iteration == previous_step.current_iteration + 1 # advances by 1
128
+ assert step_response.iterations_left == previous_step.iterations_left - 1 # decrement by 1
129
+ assert len(step_response.intermediate_points) == new_number_of_intermediate_points
130
+ assert len(step_response.reachable_best_bounds) == new_number_of_intermediate_points # one for each point
131
+ assert len(step_response.reachable_worst_bounds) == new_number_of_intermediate_points # one for each point
132
+ assert len(step_response.closeness_measures) == new_number_of_intermediate_points # one for each point
133
+ assert len(step_response.reachable_point_indices) == new_number_of_intermediate_points # one set for each point
134
+
135
+
136
+ def test_enautilus_get_state(client: TestClient, session_and_user: dict[str, Session | list[user]]):
137
+ """Test fetching a previous state for the the E-NAUTILUS method."""
138
+ session, _ = session_and_user["session"], session_and_user["user"]
139
+ access_token = login(client)
140
+
141
+ problem_db = session.exec(select(ProblemDB).where(ProblemDB.name == "The river pollution problem")).first()
142
+
143
+ representative_solutions = session.exec(
144
+ select(RepresentativeNonDominatedSolutions)
145
+ .join(RepresentativeNonDominatedSolutions.metadata_instance)
146
+ .where(ProblemMetaDataDB.problem_id == problem_db.id)
147
+ ).first()
148
+
149
+ current_iteration = 0
150
+ total_iterations = 5
151
+ selected_point = None # nadir point
152
+ reachable_point_indices = [] # empty for first iteration
153
+ number_of_intermediate_points = 3
154
+
155
+ session_id = None
156
+ parent_state_id = None
157
+
158
+ request = ENautilusStepRequest(
159
+ problem_id=problem_db.id,
160
+ session_id=session_id,
161
+ parent_state_id=parent_state_id,
162
+ representative_solutions_id=representative_solutions.id,
163
+ current_iteration=current_iteration,
164
+ iterations_left=total_iterations,
165
+ selected_point=selected_point,
166
+ reachable_point_indices=reachable_point_indices,
167
+ number_of_intermediate_points=number_of_intermediate_points,
168
+ )
169
+
170
+ raw_response = post_json(client, "/method/enautilus/step", request.model_dump(), access_token)
171
+
172
+ assert raw_response.status_code == 200 # OK
173
+
174
+ step1_response = ENautilusStepResponse.model_validate(json.loads(raw_response.content))
175
+
176
+ assert step1_response.state_id == 1 # there should be no prior StateDB in the test instance of the DB
177
+ assert step1_response.current_iteration == current_iteration + 1 # advances by 1
178
+ assert step1_response.iterations_left == total_iterations - 1 # decrement by 1
179
+ assert len(step1_response.intermediate_points) == number_of_intermediate_points
180
+ assert len(step1_response.reachable_best_bounds) == number_of_intermediate_points # one for each point
181
+ assert len(step1_response.reachable_worst_bounds) == number_of_intermediate_points # one for each point
182
+ assert len(step1_response.closeness_measures) == number_of_intermediate_points # one for each point
183
+ assert len(step1_response.reachable_point_indices) == number_of_intermediate_points # one set for each point
184
+
185
+ # second iteration
186
+ previous_step = step1_response
187
+ new_number_of_intermediate_points = 2
188
+
189
+ request = ENautilusStepRequest(
190
+ problem_id=problem_db.id,
191
+ session_id=None,
192
+ parent_state_id=previous_step.state_id,
193
+ representative_solutions_id=representative_solutions.id,
194
+ current_iteration=previous_step.current_iteration,
195
+ iterations_left=previous_step.iterations_left,
196
+ selected_point=previous_step.intermediate_points[0],
197
+ reachable_point_indices=previous_step.reachable_point_indices[0],
198
+ number_of_intermediate_points=new_number_of_intermediate_points,
199
+ )
200
+
201
+ raw_response = post_json(client, "/method/enautilus/step", request.model_dump(), access_token)
202
+
203
+ assert raw_response.status_code == 200 # OK
204
+
205
+ step2_response = ENautilusStepResponse.model_validate(json.loads(raw_response.content))
206
+
207
+ assert step2_response.state_id == 2 # there should be one prior StateDB in the test instance of the DB
208
+ assert step2_response.current_iteration == previous_step.current_iteration + 1 # advances by 1
209
+ assert step2_response.iterations_left == previous_step.iterations_left - 1 # decrement by 1
210
+ assert len(step2_response.intermediate_points) == new_number_of_intermediate_points
211
+ assert len(step2_response.reachable_best_bounds) == new_number_of_intermediate_points # one for each point
212
+ assert len(step2_response.reachable_worst_bounds) == new_number_of_intermediate_points # one for each point
213
+ assert len(step2_response.closeness_measures) == new_number_of_intermediate_points # one for each point
214
+ assert len(step2_response.reachable_point_indices) == new_number_of_intermediate_points # one set for each point
215
+
216
+ raw_response = get_json(client, f"/method/enautilus/get_state/{step1_response.state_id}", access_token)
217
+
218
+ assert raw_response.status_code == 200 # OK
219
+
220
+ state_response = ENautilusStateResponse.model_validate(json.loads(raw_response.content))
221
+
222
+ previous_request = state_response.request
223
+ previous_response = state_response.response
224
+
225
+ # response is the result of the state
226
+ assert previous_response.state_id == step1_response.state_id
227
+ assert previous_response.current_iteration == step1_response.current_iteration
228
+ assert previous_response.iterations_left == step1_response.iterations_left
229
+ assert previous_response.intermediate_points == step1_response.intermediate_points
230
+ assert previous_response.reachable_best_bounds == step1_response.reachable_best_bounds
231
+ assert previous_response.reachable_worst_bounds == step1_response.reachable_worst_bounds
232
+ assert previous_response.closeness_measures == step1_response.closeness_measures
233
+ assert previous_response.reachable_point_indices == step1_response.reachable_point_indices
234
+
235
+ nadir_point = {
236
+ f"{obj.symbol}": (-1 if obj.maximize else 1)
237
+ * np.max(representative_solutions.solution_data[f"{obj.symbol}_min"])
238
+ for obj in problem_db.objectives
239
+ }
240
+
241
+ reachable_point_indices_ = list(range(len(representative_solutions.solution_data[problem_db.objectives[0].symbol])))
242
+
243
+ # request is the origin of the state
244
+ assert previous_request.problem_id == problem_db.id
245
+ assert previous_request.session_id == session_id
246
+ assert previous_request.parent_state_id == parent_state_id
247
+ assert previous_request.representative_solutions_id == representative_solutions.id
248
+ assert previous_request.current_iteration == current_iteration
249
+ assert previous_request.iterations_left == total_iterations
250
+ assert previous_request.selected_point == nadir_point # should be the nadir, because first iteration
251
+ assert previous_request.reachable_point_indices == reachable_point_indices_ # all in the first iteration
252
+ assert previous_request.number_of_intermediate_points == number_of_intermediate_points
253
+
254
+
255
+ def test_enautilus_get_representative(client: TestClient, session_and_user: dict[str, Session | list[user]]):
256
+ """Test the E-NAUTILUS endpoint for getting representative solutions."""
257
+ session, _ = session_and_user["session"], session_and_user["user"]
258
+ access_token = login(client)
259
+
260
+ problem_db = session.exec(select(ProblemDB).where(ProblemDB.name == "The river pollution problem")).first()
261
+
262
+ representative_solutions = session.exec(
263
+ select(RepresentativeNonDominatedSolutions)
264
+ .join(RepresentativeNonDominatedSolutions.metadata_instance)
265
+ .where(ProblemMetaDataDB.problem_id == problem_db.id)
266
+ ).first()
267
+
268
+ current_iteration = 0
269
+ total_iterations = 5
270
+ selected_point = None # nadir point
271
+ reachable_point_indices = [] # empty for first iteration
272
+ number_of_intermediate_points = 3
273
+
274
+ request = ENautilusStepRequest(
275
+ problem_id=problem_db.id,
276
+ session_id=None,
277
+ parent_state_id=None,
278
+ representative_solutions_id=representative_solutions.id,
279
+ current_iteration=current_iteration,
280
+ iterations_left=total_iterations,
281
+ selected_point=selected_point,
282
+ reachable_point_indices=reachable_point_indices,
283
+ number_of_intermediate_points=number_of_intermediate_points,
284
+ )
285
+
286
+ raw_response = post_json(client, "/method/enautilus/step", request.model_dump(), access_token)
287
+
288
+ assert raw_response.status_code == 200 # OK
289
+
290
+ step_response = ENautilusStepResponse.model_validate(json.loads(raw_response.content))
291
+
292
+ # iterate until last
293
+ while step_response.iterations_left > 0:
294
+ previous_step = step_response
295
+
296
+ request = ENautilusStepRequest(
297
+ problem_id=problem_db.id,
298
+ session_id=None,
299
+ parent_state_id=previous_step.state_id,
300
+ representative_solutions_id=representative_solutions.id,
301
+ current_iteration=previous_step.current_iteration,
302
+ iterations_left=previous_step.iterations_left,
303
+ selected_point=previous_step.intermediate_points[0],
304
+ reachable_point_indices=previous_step.reachable_point_indices[0],
305
+ number_of_intermediate_points=number_of_intermediate_points,
306
+ )
307
+
308
+ raw_response = post_json(client, "/method/enautilus/step", request.model_dump(), access_token)
309
+
310
+ assert raw_response.status_code == 200 # OK
311
+
312
+ step_response = ENautilusStepResponse.model_validate(json.loads(raw_response.content))
313
+
314
+ # no iterations left, last iteration
315
+ raw_response = get_json(client, "/method/enautilus/get_representative/3", access_token)
316
+
317
+ assert raw_response.status_code == 200
318
+
319
+ int_response = ENautilusRepresentativeSolutionsResponse.model_validate(json.loads(raw_response.content))
320
+
321
+ assert len(int_response.solutions) == number_of_intermediate_points
322
+
323
+ # try also a previous iteration
324
+ raw_response = get_json(client, "/method/enautilus/get_representative/2", access_token)
325
+
326
+ assert raw_response.status_code == 200
327
+
328
+ int_response = ENautilusRepresentativeSolutionsResponse.model_validate(json.loads(raw_response.content))
329
+
330
+ assert len(int_response.solutions) == number_of_intermediate_points