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
@@ -0,0 +1,377 @@
1
+ """Necessary routers for GDM Score Bands.
2
+
3
+ I imagine these as simple interfaces to the GDMScoreBandsManager.
4
+ """
5
+
6
+ import logging
7
+ import sys
8
+ from typing import Annotated
9
+
10
+ import polars as pl
11
+ from fastapi import APIRouter, Depends, HTTPException, status
12
+ from fastapi.responses import JSONResponse
13
+ from sqlmodel import Session, select
14
+
15
+ from desdeo.api.db import get_session
16
+ from desdeo.api.models import (
17
+ GDMSCOREBandInformation,
18
+ GDMSCOREBandsDecisionResponse,
19
+ GDMSCOREBandsHistoryResponse,
20
+ GDMScoreBandsInitializationRequest,
21
+ GDMSCOREBandsResponse,
22
+ GDMSCOREBandsRevertRequest,
23
+ GDMScoreBandsVoteRequest,
24
+ Group,
25
+ GroupInfoRequest,
26
+ GroupIteration,
27
+ User,
28
+ )
29
+ from desdeo.api.routers.gdm.gdm_aggregate import manager
30
+ from desdeo.api.routers.gdm.gdm_score_bands.gdm_score_bands_manager import GDMScoreBandsManager
31
+ from desdeo.api.routers.user_authentication import get_current_user
32
+ from desdeo.gdm.score_bands import SCOREBandsGDMConfig, SCOREBandsGDMResult, score_bands_gdm
33
+
34
+ logging.basicConfig(
35
+ stream=sys.stdout, format="[%(filename)s:%(lineno)d] %(levelname)s: %(message)s", level=logging.INFO
36
+ )
37
+ logger = logging.getLogger(__name__)
38
+
39
+ router = APIRouter(prefix="/gdm-score-bands", tags=["GDM Score Bands"])
40
+
41
+
42
+ @router.post("/vote")
43
+ async def vote_for_a_band(
44
+ request: GDMScoreBandsVoteRequest,
45
+ user: Annotated[User, Depends(get_current_user)],
46
+ session: Annotated[Session, Depends(get_session)],
47
+ ):
48
+ """Vote for a band using this endpoint.
49
+
50
+ Args:
51
+ request (GDMScoreBandsVoteRequest): A container for the group id and the vote.
52
+ user (Annotated[User, Depends): the current user.
53
+ session (Annotated[Session, Depends): database session
54
+
55
+ Raises:
56
+ HTTPException: If something goes wrong. It hopefully let's you know what went wrong.
57
+
58
+ Returns:
59
+ JSONResponse: A quick confirmation that vote went through.
60
+ """
61
+ group_id = request.group_id
62
+ vote = request.vote
63
+ group = session.exec(select(Group).where(Group.id == group_id)).first()
64
+ if not group:
65
+ raise HTTPException(detail=f"Group with ID {group_id} does not exist!", status_code=status.HTTP_404_NOT_FOUND)
66
+ if user.id not in group.user_ids:
67
+ raise HTTPException(
68
+ detail=f"User with ID {user.id} is not part of group with ID {group.id}. Could be the owner though.",
69
+ status_code=status.HTTP_401_UNAUTHORIZED,
70
+ )
71
+ try:
72
+ group_mgr: GDMScoreBandsManager = await manager.get_group_manager(
73
+ group_id=group_id, method="gdm-score-bands", db_session=session
74
+ )
75
+ except Exception as e:
76
+ print(e)
77
+ raise
78
+
79
+ # This would be the better way to do things.
80
+ try:
81
+ await group_mgr.vote(user=user, group=group, voted_index=vote, session=session)
82
+ except Exception as e:
83
+ logger.exception("Found an error when issuing a vote for a band.")
84
+ raise HTTPException(
85
+ detail=f"Internal server error: {e}", status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
86
+ ) from e
87
+
88
+ return JSONResponse(content={"message": f"Voted for index {vote} by user with ID {user.id}"})
89
+
90
+
91
+ @router.post("/confirm")
92
+ async def confirm_vote(
93
+ request: GroupInfoRequest,
94
+ user: Annotated[User, Depends(get_current_user)],
95
+ session: Annotated[Session, Depends(get_session)],
96
+ ):
97
+ """Confim the vote. If all confirm, the clustering and new iteration begins.
98
+
99
+ Args:
100
+ request (GroupInfoRequest): Simple request to get the group ID.
101
+ user (Annotated[User, Depends): The current user.
102
+ session (Annotated[Session, Depends): Database session.
103
+
104
+ Raises:
105
+ HTTPException: If something goes awry. It should let you know what went wrong, though.
106
+
107
+ Returns:
108
+ JSONResponse: A simple confirmation that everything went ok and that vote went in.
109
+ """
110
+ group_id = request.group_id
111
+ group = session.exec(select(Group).where(Group.id == group_id)).first()
112
+ if not group:
113
+ raise HTTPException(detail=f"Group with ID {group_id} does not exist!", status_code=status.HTTP_404_NOT_FOUND)
114
+ if user.id not in group.user_ids:
115
+ raise HTTPException(
116
+ detail=f"User with ID {user.id} is not part of group with ID {group.id}. Could be the owner though.",
117
+ status_code=status.HTTP_401_UNAUTHORIZED,
118
+ )
119
+ group_mgr: GDMScoreBandsManager = await manager.get_group_manager(
120
+ group_id=group_id, method="gdm-score-bands", db_session=session
121
+ )
122
+ try:
123
+ await group_mgr.confirm(user=user, group=group, session=session)
124
+ except Exception as e:
125
+ logger.exception("Found and error when trying to confirm a vote.")
126
+ raise HTTPException(
127
+ detail=f"Internal server error: {e}", status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
128
+ ) from e
129
+
130
+ return JSONResponse(content={"message": f"Confirmed vote and moving on for user with ID {user.id}"})
131
+
132
+
133
+ @router.post("/get-or-initialize")
134
+ async def get_or_initialize(
135
+ request: GDMScoreBandsInitializationRequest,
136
+ user: Annotated[User, Depends(get_current_user)],
137
+ session: Annotated[Session, Depends(get_session)],
138
+ ) -> GDMSCOREBandsHistoryResponse:
139
+ """An endpoint for two things: Initializing the GDM Score Bands things and Fetching results.
140
+
141
+ If a group hasn't been initialized, initialize and then return initial clustering information.
142
+ If it has been initialized, just fetch the latest iteration's information (clustering, etc.)
143
+
144
+ Args:
145
+ request (GDMScoreBandsInitializationRequest): Request that contains necessary information for initialization.
146
+ user (Annotated[User, Depends): The current user.
147
+ session (Annotated[Session, Depends): Database session.
148
+
149
+ Raises:
150
+ HTTPException: It'll let you know.
151
+
152
+ Returns:
153
+ GDMSCOREBandsResponse: A response containing Group id, group iter id and ScoreBandsResponse.
154
+ """
155
+ group: Group = session.exec(select(Group).where(Group.id == request.group_id)).first()
156
+ if not group:
157
+ raise HTTPException(
158
+ detail=f"Group with ID {request.group_id} not found!", status_code=status.HTTP_404_NOT_FOUND
159
+ )
160
+ if group.head_iteration_id is not None:
161
+ # Actually, just return the newest score band data.
162
+ print("Group already initialized!")
163
+ group_iterations = session.exec(select(GroupIteration).where(GroupIteration.group_id == group.id)).all()
164
+ responses: list[GDMSCOREBandsResponse | GDMSCOREBandsDecisionResponse] = []
165
+ for giter in group_iterations:
166
+ match giter.info_container.method:
167
+ case "gdm-score-bands":
168
+ responses.append(
169
+ GDMSCOREBandsResponse(
170
+ group_id=group.id,
171
+ group_iter_id=giter.id,
172
+ latest_iteration=giter.info_container.score_bands_result.iteration,
173
+ result=giter.info_container.score_bands_result.score_bands_result,
174
+ )
175
+ )
176
+ case "gdm-score-bands-final":
177
+ responses.append(
178
+ GDMSCOREBandsDecisionResponse(
179
+ group_id=group.id, group_iter_id=giter.id, result=giter.info_container
180
+ )
181
+ )
182
+ return GDMSCOREBandsHistoryResponse(history=responses)
183
+ user_ids = group.user_ids
184
+ user_ids.append(group.owner_id)
185
+ if user.id not in user_ids:
186
+ raise HTTPException(
187
+ detail=f"User with ID {user.id} is not part of group with ID {group.id}",
188
+ status_code=status.HTTP_403_FORBIDDEN,
189
+ )
190
+ group_mgr: GDMScoreBandsManager = await manager.get_group_manager(
191
+ group_id=group.id, method="gdm-score-bands", db_session=session
192
+ )
193
+
194
+ score_bands_config = SCOREBandsGDMConfig() if request.score_bands_config is None else request.score_bands_config
195
+
196
+ # initial clustering for the objectives
197
+ discrete_representation_obj = group_mgr.discrete_representation.objective_values
198
+ objs = pl.DataFrame(discrete_representation_obj)
199
+ result: SCOREBandsGDMResult = score_bands_gdm(data=objs, config=score_bands_config, state=None)[-1]
200
+
201
+ score_bands_config.score_bands_config.axis_positions = result.score_bands_result.axis_positions
202
+
203
+ # store necessary data to the database. Currently all "voting" related is null bc no voting has happened yet.
204
+ score_bands_info = GDMSCOREBandInformation(
205
+ user_votes={}, user_confirms=[], score_bands_config=score_bands_config, score_bands_result=result
206
+ )
207
+
208
+ # Add group iteration and related stuff, then set new iteration to head.
209
+ iteration: GroupIteration = GroupIteration(
210
+ group_id=group.id,
211
+ problem_id=group.problem_id,
212
+ info_container=score_bands_info,
213
+ notified={},
214
+ state_id=None,
215
+ parent_id=None,
216
+ parent=None,
217
+ )
218
+
219
+ session.add(iteration)
220
+ session.commit()
221
+ session.refresh(iteration)
222
+
223
+ group.head_iteration_id = iteration.id
224
+ session.add(group)
225
+ session.commit()
226
+ session.refresh(group)
227
+
228
+ # Actually, return just the newly created score band data.
229
+ return GDMSCOREBandsHistoryResponse(
230
+ history=[
231
+ GDMSCOREBandsResponse(
232
+ group_id=group.id,
233
+ group_iter_id=group.head_iteration_id,
234
+ latest_iteration=result.iteration,
235
+ result=result.score_bands_result,
236
+ )
237
+ ]
238
+ )
239
+
240
+
241
+ @router.post("/get-votes-and-confirms")
242
+ def get_votes_and_confirms(
243
+ request: GroupInfoRequest,
244
+ user: Annotated[User, Depends(get_current_user)],
245
+ session: Annotated[Session, Depends(get_session)],
246
+ ) -> JSONResponse:
247
+ """Returns the current status of votes and confirmations in current iteration.
248
+
249
+ Args:
250
+ request (GroupInfoRequest): The group we'd like the info on.
251
+ user (Annotated[User, Depends): The user that requests the data.
252
+ session (Annotated[Session, Depends): The database session.
253
+
254
+ Raises:
255
+ HTTPException: If group doesn't exists etc errors.
256
+
257
+ Returns:
258
+ JSONResponse: A response containing the votes and confirmations.
259
+ """
260
+ group: Group = session.exec(select(Group).where(Group.id == request.group_id)).first()
261
+ if not Group:
262
+ raise HTTPException(
263
+ detail=f"Group with ID {request.group_id} not found!", status_code=status.HTTP_404_NOT_FOUND
264
+ )
265
+ if group.head_iteration_id is None:
266
+ raise HTTPException(detail="Group hasn't been initialized!", status_code=status.HTTP_400_BAD_REQUEST)
267
+ user_ids = group.user_ids
268
+ user_ids.append(group.owner_id)
269
+ if user.id not in user_ids:
270
+ raise HTTPException(detail="Unauthorized user!", status_code=status.HTTP_401_UNAUTHORIZED)
271
+
272
+ iteration = session.exec(select(GroupIteration).where(GroupIteration.id == group.head_iteration_id)).first()
273
+ votes = iteration.info_container.user_votes
274
+ confirms = iteration.info_container.user_confirms
275
+
276
+ return JSONResponse(content={"votes": votes, "confirms": confirms})
277
+
278
+
279
+ @router.post("/revert")
280
+ async def revert(
281
+ request: GDMSCOREBandsRevertRequest,
282
+ user: Annotated[User, Depends(get_current_user)],
283
+ session: Annotated[Session, Depends(get_session)],
284
+ ) -> JSONResponse:
285
+ """Revert to a previous iteration. Usable only by the analyst.
286
+
287
+ This implies that we're gonna need to see ALL previous iterations I'd say.
288
+
289
+ Args:
290
+ request (GDMSCOREBandsRevertRequest): The request containing group id and iteration number.
291
+ user (Annotated[User, Depends): The current user.
292
+ session (Annotated[Session, Depends): The database session.
293
+
294
+ Returns:
295
+ JSONResponse: Acknowledgement of the revert.
296
+ """
297
+ group: Group = session.exec(select(Group).where(Group.id == request.group_id)).first()
298
+ if user.id is not group.owner_id:
299
+ raise HTTPException(
300
+ detail="Reverting can only be done by the group owner!", status_code=status.HTTP_401_UNAUTHORIZED
301
+ )
302
+ if not group:
303
+ raise HTTPException(
304
+ detail=f"Group with ID {request.group_id} not found!", status_code=status.HTTP_404_NOT_FOUND
305
+ )
306
+ user_ids = group.user_ids
307
+ user_ids.append(group.owner_id)
308
+ if user.id not in user_ids:
309
+ raise HTTPException(
310
+ detail=f"User with ID {user.id} is not part of group with ID {group.id}",
311
+ status_code=status.HTTP_403_FORBIDDEN,
312
+ )
313
+ group_id = request.group_id
314
+ group_mgr: GDMScoreBandsManager = await manager.get_group_manager(
315
+ group_id=group_id, method="gdm-score-bands", db_session=session
316
+ )
317
+
318
+ try:
319
+ await group_mgr.revert(user=user, group=group, session=session, group_iteration_number=request.iteration_number)
320
+ except Exception as e:
321
+ logger.exception("Found an error when trying to revert to a previous iteration.")
322
+ raise HTTPException(
323
+ detail=f"Internal server error: {e}", status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
324
+ ) from e
325
+
326
+ return JSONResponse(content={"message": "Reverted iteration."})
327
+
328
+
329
+ @router.post("/configure")
330
+ async def configure_gdm(
331
+ config: SCOREBandsGDMConfig,
332
+ group_id: int,
333
+ user: Annotated[User, Depends(get_current_user)],
334
+ session: Annotated[Session, Depends(get_session)],
335
+ ) -> JSONResponse:
336
+ """Configure the SCORE Bands settings.
337
+
338
+ Args:
339
+ config (SCOREBandsGDMConfig): The configuration object
340
+ group_id (int): group id
341
+ user (Annotated[User, Depends): The user doing the request
342
+ session (Annotated[Session, Depends): The database session.
343
+
344
+ Returns:
345
+ JSONResponse: Acknowledgement that yeah ok reconfigured.
346
+ """
347
+ group: Group = session.exec(select(Group).where(Group.id == group_id)).first()
348
+ if user.id is not group.owner_id:
349
+ raise HTTPException(
350
+ detail="Reverting can only be done by the group owner!", status_code=status.HTTP_401_UNAUTHORIZED
351
+ )
352
+ if not group:
353
+ raise HTTPException(detail=f"Group with ID {group_id} not found!", status_code=status.HTTP_404_NOT_FOUND)
354
+ user_ids = group.user_ids
355
+ user_ids.append(group.owner_id)
356
+ if user.id not in user_ids:
357
+ raise HTTPException(
358
+ detail=f"User with ID {user.id} is not part of group with ID {group.id}",
359
+ status_code=status.HTTP_403_FORBIDDEN,
360
+ )
361
+ group_mgr: GDMScoreBandsManager = await manager.get_group_manager(
362
+ group_id=group_id, method="gdm-score-bands", db_session=session
363
+ )
364
+
365
+ try:
366
+ await group_mgr.configure(
367
+ config=config,
368
+ group=group,
369
+ session=session,
370
+ )
371
+ except Exception as e:
372
+ logger.exception("Found an error when trying to configure SCORE band settings.")
373
+ raise HTTPException(
374
+ detail=f"Internal server error: {e}", status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
375
+ ) from e
376
+
377
+ return JSONResponse(content={"message": "Configured. Re-clustered."})