desdeo 2.0.0__py3-none-any.whl → 2.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/__init__.py +6 -6
- desdeo/api/app.py +38 -28
- desdeo/api/config.py +65 -44
- desdeo/api/config.toml +23 -12
- desdeo/api/db.py +10 -8
- desdeo/api/db_init.py +12 -6
- desdeo/api/models/__init__.py +220 -20
- desdeo/api/models/archive.py +16 -27
- 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 +44 -6
- desdeo/api/models/problem.py +274 -64
- desdeo/api/models/session.py +4 -1
- desdeo/api/models/state.py +419 -52
- desdeo/api/models/user.py +7 -6
- desdeo/api/models/utopia.py +25 -0
- desdeo/api/routers/_EMO.backup +309 -0
- desdeo/api/routers/_NIMBUS.py +6 -3
- 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 +201 -4
- desdeo/api/routers/reference_point_method.py +20 -44
- desdeo/api/routers/session.py +50 -26
- desdeo/api/routers/user_authentication.py +180 -26
- desdeo/api/routers/utils.py +187 -0
- desdeo/api/routers/utopia.py +230 -0
- desdeo/api/schema.py +10 -4
- desdeo/api/tests/conftest.py +94 -2
- desdeo/api/tests/test_enautilus.py +330 -0
- desdeo/api/tests/test_models.py +550 -72
- desdeo/api/tests/test_routes.py +902 -43
- desdeo/api/utils/_database.py +263 -0
- desdeo/api/utils/database.py +28 -266
- desdeo/api/utils/emo_database.py +40 -0
- desdeo/core.py +7 -0
- desdeo/emo/__init__.py +154 -24
- desdeo/emo/hooks/archivers.py +18 -2
- desdeo/emo/methods/EAs.py +128 -5
- desdeo/emo/methods/bases.py +9 -56
- desdeo/emo/methods/templates.py +111 -0
- desdeo/emo/operators/crossover.py +544 -42
- desdeo/emo/operators/evaluator.py +10 -14
- desdeo/emo/operators/generator.py +127 -24
- desdeo/emo/operators/mutation.py +212 -41
- desdeo/emo/operators/scalar_selection.py +202 -0
- desdeo/emo/operators/selection.py +956 -214
- desdeo/emo/operators/termination.py +124 -16
- 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/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 +23 -1
- desdeo/mcdm/enautilus.py +338 -0
- desdeo/mcdm/gnimbus.py +484 -0
- desdeo/mcdm/nautilus_navigator.py +7 -6
- desdeo/mcdm/reference_point_method.py +70 -0
- desdeo/problem/__init__.py +5 -1
- 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/infix_parser.py +2 -2
- desdeo/problem/pyomo_evaluator.py +25 -6
- desdeo/problem/schema.py +69 -48
- desdeo/problem/simulator_evaluator.py +65 -15
- desdeo/problem/testproblems/__init__.py +26 -11
- desdeo/problem/testproblems/benchmarks_server.py +120 -0
- desdeo/problem/testproblems/cake_problem.py +185 -0
- desdeo/problem/testproblems/dmitry_forest_problem_discrete.py +71 -0
- desdeo/problem/testproblems/forest_problem.py +77 -69
- desdeo/problem/testproblems/multi_valued_constraints.py +119 -0
- desdeo/problem/testproblems/{river_pollution_problem.py → river_pollution_problems.py} +28 -22
- desdeo/problem/testproblems/single_objective.py +289 -0
- desdeo/problem/testproblems/zdt_problem.py +4 -1
- desdeo/tools/__init__.py +39 -21
- desdeo/tools/desc_gen.py +22 -0
- desdeo/tools/generics.py +22 -2
- desdeo/tools/group_scalarization.py +3090 -0
- desdeo/tools/indicators_binary.py +107 -1
- desdeo/tools/indicators_unary.py +3 -16
- desdeo/tools/message.py +33 -2
- desdeo/tools/non_dominated_sorting.py +4 -3
- desdeo/tools/patterns.py +9 -7
- desdeo/tools/pyomo_solver_interfaces.py +48 -35
- desdeo/tools/reference_vectors.py +118 -351
- desdeo/tools/scalarization.py +340 -1413
- desdeo/tools/score_bands.py +491 -328
- desdeo/tools/utils.py +117 -49
- desdeo/tools/visualizations.py +67 -0
- desdeo/utopia_stuff/utopia_problem.py +1 -1
- desdeo/utopia_stuff/utopia_problem_old.py +1 -1
- {desdeo-2.0.0.dist-info → desdeo-2.1.0.dist-info}/METADATA +46 -28
- desdeo-2.1.0.dist-info/RECORD +180 -0
- {desdeo-2.0.0.dist-info → desdeo-2.1.0.dist-info}/WHEEL +1 -1
- desdeo-2.0.0.dist-info/RECORD +0 -120
- /desdeo/api/utils/{logger.py → _logger.py} +0 -0
- {desdeo-2.0.0.dist-info → desdeo-2.1.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import itertools
|
|
4
4
|
from collections.abc import Iterable
|
|
5
5
|
from operator import eq as _eq
|
|
6
|
-
from operator import le as
|
|
6
|
+
from operator import le as _python_le
|
|
7
7
|
|
|
8
8
|
import pyomo.environ as pyomo
|
|
9
9
|
|
|
@@ -19,6 +19,18 @@ from desdeo.problem.schema import (
|
|
|
19
19
|
)
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
def _le(expr, rhs, name):
|
|
23
|
+
# scalar → single constraint
|
|
24
|
+
if not expr.is_indexed():
|
|
25
|
+
tmp_expr = _python_le(expr, rhs)
|
|
26
|
+
return pyomo.Constraint(expr=tmp_expr, name=name)
|
|
27
|
+
|
|
28
|
+
# indexed → one row per index
|
|
29
|
+
tmp = pyomo.Constraint(expr.index_set(), rule=lambda m, *idx: expr[idx] <= rhs)
|
|
30
|
+
tmp.construct()
|
|
31
|
+
return tmp
|
|
32
|
+
|
|
33
|
+
|
|
22
34
|
class PyomoEvaluatorError(Exception):
|
|
23
35
|
"""Raised when an error within the PyomoEvaluator class is encountered."""
|
|
24
36
|
|
|
@@ -159,7 +171,7 @@ class PyomoEvaluator:
|
|
|
159
171
|
else:
|
|
160
172
|
initial_value = (
|
|
161
173
|
var.initial_value
|
|
162
|
-
if var.lowerbound is None
|
|
174
|
+
if var.lowerbound is None or var.upperbound is None
|
|
163
175
|
else (var.lowerbound + var.upperbound) / 2
|
|
164
176
|
)
|
|
165
177
|
|
|
@@ -321,16 +333,17 @@ class PyomoEvaluator:
|
|
|
321
333
|
match con_type := cons.cons_type:
|
|
322
334
|
case ConstraintTypeEnum.LTE:
|
|
323
335
|
# constraints in DESDEO are defined such that they must be less than zero
|
|
324
|
-
pyomo_expr = _le(pyomo_expr, 0)
|
|
336
|
+
pyomo_expr = _le(pyomo_expr, 0, cons.name)
|
|
325
337
|
case ConstraintTypeEnum.EQ:
|
|
326
338
|
pyomo_expr = _eq(pyomo_expr, 0)
|
|
327
339
|
case _:
|
|
328
340
|
msg = f"Constraint type of {con_type} not supported. Must be one of {ConstraintTypeEnum}."
|
|
329
341
|
raise PyomoEvaluatorError(msg)
|
|
330
342
|
|
|
331
|
-
cons_expr = pyomo.Constraint(expr=pyomo_expr, name=cons.name)
|
|
343
|
+
# cons_expr = pyomo.Constraint(expr=pyomo_expr, name=cons.name)
|
|
332
344
|
|
|
333
|
-
setattr(model, cons.symbol,
|
|
345
|
+
setattr(model, cons.symbol, pyomo_expr)
|
|
346
|
+
# getattr(model, cons.symbol).construct()
|
|
334
347
|
|
|
335
348
|
return model
|
|
336
349
|
|
|
@@ -429,9 +442,15 @@ class PyomoEvaluator:
|
|
|
429
442
|
for extra in self.problem.extra_funcs:
|
|
430
443
|
result_dict[extra.symbol] = pyomo.value(getattr(self.model, extra.symbol))
|
|
431
444
|
|
|
445
|
+
# TODO: after implementing TensorConstraint, fix this
|
|
432
446
|
if self.problem.constraints is not None:
|
|
433
447
|
for const in self.problem.constraints:
|
|
434
|
-
|
|
448
|
+
obj = getattr(self.model, const.symbol)
|
|
449
|
+
|
|
450
|
+
if obj.is_indexed():
|
|
451
|
+
result_dict[const.symbol] = {k: pyomo.value(obj[k]) for k in obj}
|
|
452
|
+
else:
|
|
453
|
+
result_dict[const.symbol] = pyomo.value(obj)
|
|
435
454
|
|
|
436
455
|
if self.problem.scalarization_funcs is not None:
|
|
437
456
|
for scal in self.problem.scalarization_funcs:
|
desdeo/problem/schema.py
CHANGED
|
@@ -16,7 +16,7 @@ from collections.abc import Iterable
|
|
|
16
16
|
from enum import Enum
|
|
17
17
|
from itertools import product
|
|
18
18
|
from pathlib import Path
|
|
19
|
-
from typing import TYPE_CHECKING, Annotated, Any, Literal, TypeAliasType
|
|
19
|
+
from typing import TYPE_CHECKING, Annotated, Any, Literal, Self, TypeAliasType
|
|
20
20
|
|
|
21
21
|
import numpy as np
|
|
22
22
|
from pydantic import (
|
|
@@ -260,7 +260,7 @@ class ObjectiveTypeEnum(str, Enum):
|
|
|
260
260
|
class Constant(BaseModel):
|
|
261
261
|
"""Model for a constant."""
|
|
262
262
|
|
|
263
|
-
model_config = ConfigDict(frozen=True, from_attributes=True)
|
|
263
|
+
model_config = ConfigDict(frozen=True, from_attributes=True, extra="forbid")
|
|
264
264
|
|
|
265
265
|
name: str = Field(
|
|
266
266
|
description=(
|
|
@@ -284,7 +284,7 @@ class Constant(BaseModel):
|
|
|
284
284
|
class TensorConstant(BaseModel):
|
|
285
285
|
"""Model for a tensor containing constant values."""
|
|
286
286
|
|
|
287
|
-
model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True, from_attributes=True)
|
|
287
|
+
model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True, from_attributes=True, extra="forbid")
|
|
288
288
|
|
|
289
289
|
name: str = Field(description="Descriptive name of the tensor representing the values. E.g., 'distances'")
|
|
290
290
|
"""Descriptive name of the tensor representing the values. E.g., 'distances'"""
|
|
@@ -377,7 +377,7 @@ class TensorConstant(BaseModel):
|
|
|
377
377
|
class Variable(BaseModel):
|
|
378
378
|
"""Model for a variable."""
|
|
379
379
|
|
|
380
|
-
model_config = ConfigDict(frozen=True, from_attributes=True)
|
|
380
|
+
model_config = ConfigDict(frozen=True, from_attributes=True, extra="forbid")
|
|
381
381
|
|
|
382
382
|
name: str = Field(
|
|
383
383
|
description="Descriptive name of the variable. This can be used in UI and visualizations. Example: 'velocity'."
|
|
@@ -407,7 +407,7 @@ class Variable(BaseModel):
|
|
|
407
407
|
class TensorVariable(BaseModel):
|
|
408
408
|
"""Model for a tensor, e.g., vector variable."""
|
|
409
409
|
|
|
410
|
-
model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True, from_attributes=True)
|
|
410
|
+
model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True, from_attributes=True, extra="forbid")
|
|
411
411
|
|
|
412
412
|
name: str = Field(
|
|
413
413
|
description="Descriptive name of the variable. This can be used in UI and visualizations. Example: 'velocity'."
|
|
@@ -585,7 +585,7 @@ class ExtraFunction(BaseModel):
|
|
|
585
585
|
they are needed for other computations related to the problem.
|
|
586
586
|
"""
|
|
587
587
|
|
|
588
|
-
model_config = ConfigDict(frozen=True, from_attributes=True)
|
|
588
|
+
model_config = ConfigDict(frozen=True, from_attributes=True, extra="forbid")
|
|
589
589
|
|
|
590
590
|
name: str = Field(
|
|
591
591
|
description=("Descriptive name of the function. Example: 'normalization'."),
|
|
@@ -663,7 +663,7 @@ class ExtraFunction(BaseModel):
|
|
|
663
663
|
class ScalarizationFunction(BaseModel):
|
|
664
664
|
"""Model for scalarization of the problem."""
|
|
665
665
|
|
|
666
|
-
model_config = ConfigDict(from_attributes=True)
|
|
666
|
+
model_config = ConfigDict(from_attributes=True, extra="forbid")
|
|
667
667
|
|
|
668
668
|
name: str = Field(description=("Name of the scalarization function."))
|
|
669
669
|
"""Name of the scalarization function."""
|
|
@@ -712,10 +712,35 @@ class ScalarizationFunction(BaseModel):
|
|
|
712
712
|
)
|
|
713
713
|
|
|
714
714
|
|
|
715
|
+
class Url(BaseModel):
|
|
716
|
+
"""Model for a URL."""
|
|
717
|
+
|
|
718
|
+
model_config = ConfigDict(frozen=True, from_attributes=True, extra="forbid")
|
|
719
|
+
|
|
720
|
+
url: str = Field(
|
|
721
|
+
description=(
|
|
722
|
+
"A URL to the simulator. A GET request to this URL should be used to evaluate solutions in batches."
|
|
723
|
+
)
|
|
724
|
+
)
|
|
725
|
+
"""A URL to the simulator. A GET request to this URL should be used to evaluate solutions in batches."""
|
|
726
|
+
|
|
727
|
+
auth: tuple[str, str] | None = Field(
|
|
728
|
+
description=(
|
|
729
|
+
"Optional. A tuple of username and password to be used for authentication when making requests to the URL."
|
|
730
|
+
),
|
|
731
|
+
default=None,
|
|
732
|
+
)
|
|
733
|
+
"""Optional. A tuple of username and password to be used for authentication when making requests to the URL."""
|
|
734
|
+
# Add headers and stuff for a proper HTTP request if needed in the future idk
|
|
735
|
+
|
|
736
|
+
|
|
715
737
|
class Simulator(BaseModel):
|
|
716
|
-
"""Model for simulator data.
|
|
738
|
+
"""Model for simulator data.
|
|
739
|
+
|
|
740
|
+
One of `file` or `url` must be provided, but not both.
|
|
741
|
+
"""
|
|
717
742
|
|
|
718
|
-
model_config = ConfigDict(frozen=True, from_attributes=True)
|
|
743
|
+
model_config = ConfigDict(frozen=True, from_attributes=True, extra="forbid")
|
|
719
744
|
|
|
720
745
|
name: str = Field(
|
|
721
746
|
description=("Descriptive name of the simulator. This can be used in UI and visualizations."),
|
|
@@ -727,10 +752,15 @@ class Simulator(BaseModel):
|
|
|
727
752
|
" It may also be used in UIs and visualizations."
|
|
728
753
|
),
|
|
729
754
|
)
|
|
730
|
-
file: Path = Field(
|
|
731
|
-
description=("Path to a python file with the connection to simulators."),
|
|
732
|
-
)
|
|
755
|
+
file: Path | None = Field(description=("Path to a python file with the connection to simulators."), default=None)
|
|
733
756
|
"""Path to a python file with the connection to simulators."""
|
|
757
|
+
url: Url | None = Field(
|
|
758
|
+
description=(
|
|
759
|
+
"Optional. A URL to the simulator. A GET request to this URL should be used to evaluate solutions in batches."
|
|
760
|
+
),
|
|
761
|
+
default=None,
|
|
762
|
+
)
|
|
763
|
+
"""Optional. A URL to the simulator. A GET request to this URL should be used to evaluate solutions in batches."""
|
|
734
764
|
parameter_options: dict | None = Field(
|
|
735
765
|
description=(
|
|
736
766
|
"Parameters to the simulator that are not decision variables, but affect the results."
|
|
@@ -741,12 +771,30 @@ class Simulator(BaseModel):
|
|
|
741
771
|
"""Parameters to the simulator that are not decision variables, but affect the results.
|
|
742
772
|
Format is similar to decision variables. Can be 'None'."""
|
|
743
773
|
|
|
774
|
+
# Check that either file or url is provided, but not both
|
|
775
|
+
@model_validator(mode="after")
|
|
776
|
+
def check_file_or_url(self) -> Self:
|
|
777
|
+
"""Ensure that either file or url is provided, but not both."""
|
|
778
|
+
if self.file is None and self.url is None:
|
|
779
|
+
raise ValueError("Either 'file' or 'url' must be provided.")
|
|
780
|
+
if self.file is not None and self.url is not None:
|
|
781
|
+
raise ValueError("Only one of 'file' or 'url' can be provided.")
|
|
782
|
+
return self
|
|
783
|
+
|
|
744
784
|
|
|
745
785
|
class Objective(BaseModel):
|
|
746
786
|
"""Model for an objective function."""
|
|
747
787
|
|
|
748
|
-
model_config = ConfigDict(frozen=True, from_attributes=True)
|
|
788
|
+
model_config = ConfigDict(frozen=True, from_attributes=True, extra="forbid")
|
|
749
789
|
|
|
790
|
+
"""A longer description for the objective."""
|
|
791
|
+
description: str | None = Field(
|
|
792
|
+
description=(
|
|
793
|
+
"A longer description of the objective function. This can be used in UI and visualizations. \
|
|
794
|
+
Meant to have longer text than what name should have."
|
|
795
|
+
),
|
|
796
|
+
default=None,
|
|
797
|
+
)
|
|
750
798
|
name: str = Field(
|
|
751
799
|
description=(
|
|
752
800
|
"Descriptive name of the objective function. This can be used in UI and visualizations. Example: 'time'."
|
|
@@ -786,9 +834,9 @@ class Objective(BaseModel):
|
|
|
786
834
|
variable/constant/extra function. Can be 'None' for 'data_based', 'simulator' or
|
|
787
835
|
'surrogate' objective functions. If 'None', either 'simulator_path' or 'surrogates' must
|
|
788
836
|
not be 'None'."""
|
|
789
|
-
simulator_path: Path | None = Field(
|
|
837
|
+
simulator_path: Path | Url | None = Field(
|
|
790
838
|
description=(
|
|
791
|
-
"Path to a python file with the connection to simulators. Must be a valid Path."
|
|
839
|
+
"Path to a python file or http server with the connection to simulators. Must be a valid Path or url."
|
|
792
840
|
"Can be 'None' for 'analytical', 'data_based' or 'surrogate' objective functions."
|
|
793
841
|
"If 'None', either 'func' or 'surrogates' must not be 'None'."
|
|
794
842
|
),
|
|
@@ -857,7 +905,7 @@ class Objective(BaseModel):
|
|
|
857
905
|
class Constraint(BaseModel):
|
|
858
906
|
"""Model for a constraint function."""
|
|
859
907
|
|
|
860
|
-
model_config = ConfigDict(frozen=True, from_attributes=True)
|
|
908
|
+
model_config = ConfigDict(frozen=True, from_attributes=True, extra="forbid")
|
|
861
909
|
|
|
862
910
|
name: str = Field(
|
|
863
911
|
description=(
|
|
@@ -903,7 +951,7 @@ class Constraint(BaseModel):
|
|
|
903
951
|
function must match objective/variable/constant symbols.
|
|
904
952
|
Can be 'None' if either 'simulator_path' or 'surrogates' is not 'None'.
|
|
905
953
|
If 'None', either 'simulator_path' or 'surrogates' must not be 'None'."""
|
|
906
|
-
simulator_path: Path | None = Field(
|
|
954
|
+
simulator_path: Path | Url | None = Field(
|
|
907
955
|
description=(
|
|
908
956
|
"Path to a python file with the connection to simulators. Must be a valid Path."
|
|
909
957
|
"Can be 'None' for if either 'func' or 'surrogates' is not 'None'."
|
|
@@ -960,7 +1008,7 @@ class DiscreteRepresentation(BaseModel):
|
|
|
960
1008
|
found at `objective_values['f_i'][j]` for all `i` and some `j`.
|
|
961
1009
|
"""
|
|
962
1010
|
|
|
963
|
-
model_config = ConfigDict(frozen=True, from_attributes=True)
|
|
1011
|
+
model_config = ConfigDict(frozen=True, from_attributes=True, extra="forbid")
|
|
964
1012
|
|
|
965
1013
|
variable_values: dict[str, list[VariableType]] = Field(
|
|
966
1014
|
description=(
|
|
@@ -999,7 +1047,7 @@ class DiscreteRepresentation(BaseModel):
|
|
|
999
1047
|
class Problem(BaseModel):
|
|
1000
1048
|
"""Model for a problem definition."""
|
|
1001
1049
|
|
|
1002
|
-
model_config = ConfigDict(frozen=True)
|
|
1050
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
1003
1051
|
|
|
1004
1052
|
_scalarization_index: int = PrivateAttr(default=1)
|
|
1005
1053
|
# TODO: make init to communicate the _scalarization_index to a new model
|
|
@@ -1017,7 +1065,6 @@ class Problem(BaseModel):
|
|
|
1017
1065
|
is_convex=db_instance.is_convex,
|
|
1018
1066
|
is_linear=db_instance.is_linear,
|
|
1019
1067
|
is_twice_differentiable=db_instance.is_twice_differentiable,
|
|
1020
|
-
variable_domain=db_instance.variable_domain,
|
|
1021
1068
|
scenario_keys=db_instance.scenario_keys,
|
|
1022
1069
|
constants=constants if constants != [] else None,
|
|
1023
1070
|
variables=[Variable.model_validate(var) for var in db_instance.variables]
|
|
@@ -1564,7 +1611,7 @@ class Problem(BaseModel):
|
|
|
1564
1611
|
|
|
1565
1612
|
"""
|
|
1566
1613
|
json_content = self.model_dump_json(indent=4)
|
|
1567
|
-
path.write_text(json_content)
|
|
1614
|
+
path.write_text(json_content, encoding="utf-8")
|
|
1568
1615
|
|
|
1569
1616
|
@classmethod
|
|
1570
1617
|
def load_json(cls, path: Path) -> "Problem":
|
|
@@ -1578,33 +1625,7 @@ class Problem(BaseModel):
|
|
|
1578
1625
|
"""
|
|
1579
1626
|
json_data = path.read_text()
|
|
1580
1627
|
|
|
1581
|
-
return cls.model_validate_json(json_data)
|
|
1582
|
-
|
|
1583
|
-
@model_validator(mode="after")
|
|
1584
|
-
def set_is_twice_differentiable(cls, values):
|
|
1585
|
-
"""If "is_twice_differentiable" is explicitly provided to the model, we set it to that value."""
|
|
1586
|
-
if "is_twice_differentiable" in values and values["is_twice_differentiable"] is not None:
|
|
1587
|
-
values["is_twice_differentiable_"] = values["is_twice_differentiable"]
|
|
1588
|
-
|
|
1589
|
-
return values
|
|
1590
|
-
|
|
1591
|
-
@model_validator(mode="after")
|
|
1592
|
-
@classmethod
|
|
1593
|
-
def set_is_linear(cls, values):
|
|
1594
|
-
"""If "is_linear" is explicitly provided to the model, we set it to that value."""
|
|
1595
|
-
if "is_linear" in values and values["is_linear"] is not None:
|
|
1596
|
-
values["is_linear_"] = values["is_linear"]
|
|
1597
|
-
|
|
1598
|
-
return values
|
|
1599
|
-
|
|
1600
|
-
@model_validator(mode="after")
|
|
1601
|
-
@classmethod
|
|
1602
|
-
def set_is_convex(cls, values):
|
|
1603
|
-
"""If "is_convex" is explicitly provided to the model, we set it to that value."""
|
|
1604
|
-
if "is_convex" in values and values["is_convex"] is not None:
|
|
1605
|
-
values["is_convex_"] = values["is_convex"]
|
|
1606
|
-
|
|
1607
|
-
return values
|
|
1628
|
+
return cls.model_validate_json(json_data, by_name=True)
|
|
1608
1629
|
|
|
1609
1630
|
name: str = Field(
|
|
1610
1631
|
description="Name of the problem.",
|
|
@@ -5,29 +5,38 @@ import subprocess
|
|
|
5
5
|
import sys
|
|
6
6
|
from inspect import getfullargspec
|
|
7
7
|
from pathlib import Path
|
|
8
|
+
from urllib.parse import urlparse
|
|
8
9
|
|
|
9
10
|
import joblib
|
|
10
11
|
import numpy as np
|
|
11
12
|
import polars as pl
|
|
12
|
-
|
|
13
|
+
import requests
|
|
13
14
|
|
|
14
15
|
from desdeo.problem import (
|
|
16
|
+
MathParser,
|
|
15
17
|
ObjectiveTypeEnum,
|
|
16
18
|
PolarsEvaluator,
|
|
17
19
|
PolarsEvaluatorModesEnum,
|
|
18
20
|
Problem,
|
|
19
21
|
)
|
|
22
|
+
from desdeo.problem.external import ProviderParams, get_resolver, supported_schemes
|
|
23
|
+
|
|
24
|
+
# external resolver to resolve providers for problems defined externally of DESDEO
|
|
25
|
+
_external_resolver = get_resolver()
|
|
20
26
|
|
|
21
27
|
|
|
22
28
|
class EvaluatorError(Exception):
|
|
23
29
|
"""Error raised when exceptions are encountered in an Evaluator."""
|
|
24
30
|
|
|
25
31
|
|
|
26
|
-
class
|
|
32
|
+
class SimulatorEvaluator:
|
|
27
33
|
"""A class for creating evaluators for simulator based and surrogate based objectives, constraints and extras."""
|
|
28
34
|
|
|
29
|
-
def __init__(
|
|
30
|
-
self,
|
|
35
|
+
def __init__( # noqa: PLR0912
|
|
36
|
+
self,
|
|
37
|
+
problem: Problem,
|
|
38
|
+
params: dict[str, dict] | ProviderParams | None = None,
|
|
39
|
+
surrogate_paths: dict[str, Path] | None = None,
|
|
31
40
|
):
|
|
32
41
|
"""Creating an evaluator for simulator based and surrogate based objectives, constraints and extras.
|
|
33
42
|
|
|
@@ -62,6 +71,15 @@ class Evaluator:
|
|
|
62
71
|
obj.symbol
|
|
63
72
|
for obj in list(filter(lambda x: x.objective_type == ObjectiveTypeEnum.surrogate, problem.objectives))
|
|
64
73
|
]
|
|
74
|
+
if problem.scalarization_funcs is not None:
|
|
75
|
+
parser = MathParser()
|
|
76
|
+
self.scalarization_funcs = [
|
|
77
|
+
(func.symbol, parser.parse(func.func))
|
|
78
|
+
for func in problem.scalarization_funcs
|
|
79
|
+
if func.symbol is not None
|
|
80
|
+
]
|
|
81
|
+
else:
|
|
82
|
+
self.scalarization_funcs = []
|
|
65
83
|
# Gather any constraints' symbols
|
|
66
84
|
if problem.constraints is not None:
|
|
67
85
|
self.analytical_symbols = self.analytical_symbols + [
|
|
@@ -93,7 +111,10 @@ class Evaluator:
|
|
|
93
111
|
|
|
94
112
|
# Gather the possible simulators
|
|
95
113
|
self.simulators = problem.simulators if problem.simulators is not None else []
|
|
114
|
+
|
|
96
115
|
# Gather the possibly given parameters
|
|
116
|
+
if params and not isinstance(params, dict):
|
|
117
|
+
params = params.model_dump()
|
|
97
118
|
self.params = {}
|
|
98
119
|
for sim in self.simulators:
|
|
99
120
|
sim_params = params.get(sim.name, {}) if params is not None else {}
|
|
@@ -136,15 +157,37 @@ class Evaluator:
|
|
|
136
157
|
for sim in self.simulators:
|
|
137
158
|
# gather the possible parameters for the simulator
|
|
138
159
|
params = self.params.get(sim.name, {})
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
160
|
+
if sim.file is not None:
|
|
161
|
+
# call the simulator with the decision variable values and parameters as dicts
|
|
162
|
+
res = subprocess.run(
|
|
163
|
+
[sys.executable, sim.file, "-d", str(xs), "-p", str(params)], capture_output=True, text=True
|
|
164
|
+
)
|
|
165
|
+
if res.returncode == 0:
|
|
166
|
+
# gather the simulation results (a dict) into the results dataframe
|
|
167
|
+
res_df = res_df.hstack(pl.DataFrame(json.loads(res.stdout)))
|
|
168
|
+
else:
|
|
169
|
+
raise EvaluatorError(res.stderr)
|
|
170
|
+
elif sim.url is not None:
|
|
171
|
+
# call the endpoint
|
|
172
|
+
try:
|
|
173
|
+
if isinstance(xs, pl.DataFrame):
|
|
174
|
+
# if xs is a polars dataframe, convert it to a dict
|
|
175
|
+
xs = xs.to_dict(as_series=False)
|
|
176
|
+
scheme = urlparse(sim.url.url).scheme
|
|
177
|
+
if scheme in supported_schemes:
|
|
178
|
+
# desdeo
|
|
179
|
+
res = _external_resolver.evaluate(sim.url.url, params, xs)
|
|
180
|
+
res_df = res_df.hstack(pl.DataFrame(res))
|
|
181
|
+
# parse res
|
|
182
|
+
else:
|
|
183
|
+
# http, https, etc...
|
|
184
|
+
res = requests.get(sim.url.url, auth=sim.url.auth, json={"d": xs, "p": params})
|
|
185
|
+
res.raise_for_status() # raise an error if the request failed
|
|
186
|
+
res_df = res_df.hstack(pl.DataFrame(res.json()))
|
|
187
|
+
except requests.RequestException as e:
|
|
188
|
+
raise EvaluatorError(
|
|
189
|
+
f"Failed to call the simulator at {sim.url}. Is the simulator server running?"
|
|
190
|
+
) from e
|
|
148
191
|
|
|
149
192
|
# Evaluate the minimization form of the objective functions
|
|
150
193
|
min_obj_columns = pl.DataFrame()
|
|
@@ -153,7 +196,11 @@ class Evaluator:
|
|
|
153
196
|
min_obj_columns = min_obj_columns.hstack(
|
|
154
197
|
res_df.select((min_max_mult * pl.col(f"{symbol}")).alias(f"{symbol}_min"))
|
|
155
198
|
)
|
|
156
|
-
|
|
199
|
+
|
|
200
|
+
res_df = res_df.hstack(min_obj_columns)
|
|
201
|
+
# If there are scalarization functions, evaluate them as well
|
|
202
|
+
scalarization_columns = res_df.select(*[expr.alias(symbol) for symbol, expr in self.scalarization_funcs])
|
|
203
|
+
return res_df.hstack(scalarization_columns)
|
|
157
204
|
|
|
158
205
|
def _evaluate_surrogates(self, xs: dict[str, list[int | float]]) -> pl.DataFrame:
|
|
159
206
|
"""Evaluate the problem for the given decision variables using the surrogate models.
|
|
@@ -194,7 +241,10 @@ class Evaluator:
|
|
|
194
241
|
min_obj_columns = min_obj_columns.hstack(
|
|
195
242
|
res.select((min_max_mult * pl.col(f"{symbol}")).alias(f"{symbol}_min"))
|
|
196
243
|
)
|
|
197
|
-
|
|
244
|
+
res_df = res.hstack(min_obj_columns)
|
|
245
|
+
# If there are scalarization functions, evaluate them as well
|
|
246
|
+
scalarization_columns = res_df.select(*[expr.alias(symbol) for symbol, expr in self.scalarization_funcs])
|
|
247
|
+
return res_df.hstack(scalarization_columns)
|
|
198
248
|
|
|
199
249
|
def _load_surrogates(self, surrogate_paths: dict[str, Path] | None = None):
|
|
200
250
|
"""Load the surrogate models from disk and store them within the evaluator.
|
|
@@ -4,15 +4,21 @@ Pre-defined problems for, e.g.,
|
|
|
4
4
|
testing and illustration purposed are defined here.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
__all__ = [
|
|
7
|
+
__all__ = [ # noqa: RUF022
|
|
8
8
|
"binh_and_korn",
|
|
9
9
|
"dtlz2",
|
|
10
10
|
"forest_problem",
|
|
11
11
|
"forest_problem_discrete",
|
|
12
|
-
"
|
|
13
|
-
"
|
|
12
|
+
"mcwb_equilateral_tbeam_problem",
|
|
13
|
+
"mcwb_hollow_rectangular_problem",
|
|
14
|
+
"mcwb_ragsdell1976_problem",
|
|
15
|
+
"mcwb_solid_rectangular_problem",
|
|
16
|
+
"mcwb_square_channel_problem",
|
|
17
|
+
"mcwb_tapered_channel_problem",
|
|
18
|
+
"mixed_variable_dimensions_problem",
|
|
14
19
|
"momip_ti2",
|
|
15
20
|
"momip_ti7",
|
|
21
|
+
"multi_valued_constraint_problem",
|
|
16
22
|
"nimbus_test_problem",
|
|
17
23
|
"pareto_navigator_test_problem",
|
|
18
24
|
"re21",
|
|
@@ -22,8 +28,11 @@ __all__ = [
|
|
|
22
28
|
"river_pollution_problem",
|
|
23
29
|
"river_pollution_problem_discrete",
|
|
24
30
|
"river_pollution_scenario",
|
|
31
|
+
"rocket_injector_design",
|
|
25
32
|
"simple_data_problem",
|
|
26
33
|
"simple_integer_test_problem",
|
|
34
|
+
"simple_knapsack",
|
|
35
|
+
"simple_knapsack_vectors",
|
|
27
36
|
"simple_linear_test_problem",
|
|
28
37
|
"simple_scenario_test_problem",
|
|
29
38
|
"simple_test_problem",
|
|
@@ -33,22 +42,32 @@ __all__ = [
|
|
|
33
42
|
"zdt1",
|
|
34
43
|
"zdt2",
|
|
35
44
|
"zdt3",
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"mcwb_solid_rectangular_problem"
|
|
45
|
+
"best_cake_problem",
|
|
46
|
+
"dmitry_forest_problem_disc",
|
|
39
47
|
]
|
|
40
48
|
|
|
41
49
|
|
|
42
50
|
from .binh_and_korn_problem import binh_and_korn
|
|
51
|
+
from .cake_problem import best_cake_problem
|
|
52
|
+
from .dmitry_forest_problem_discrete import dmitry_forest_problem_disc
|
|
43
53
|
from .dtlz2_problem import dtlz2
|
|
44
54
|
from .forest_problem import forest_problem, forest_problem_discrete
|
|
45
55
|
from .knapsack_problem import simple_knapsack, simple_knapsack_vectors
|
|
56
|
+
from .mcwb_problem import (
|
|
57
|
+
mcwb_equilateral_tbeam_problem,
|
|
58
|
+
mcwb_hollow_rectangular_problem,
|
|
59
|
+
mcwb_ragsdell1976_problem,
|
|
60
|
+
mcwb_solid_rectangular_problem,
|
|
61
|
+
mcwb_square_channel_problem,
|
|
62
|
+
mcwb_tapered_channel_problem,
|
|
63
|
+
)
|
|
46
64
|
from .mixed_variable_dimenrions_problem import mixed_variable_dimensions_problem
|
|
47
65
|
from .momip_problem import momip_ti2, momip_ti7
|
|
66
|
+
from .multi_valued_constraints import multi_valued_constraint_problem
|
|
48
67
|
from .nimbus_problem import nimbus_test_problem
|
|
49
68
|
from .pareto_navigator_problem import pareto_navigator_test_problem
|
|
50
69
|
from .re_problem import re21, re22, re23, re24
|
|
51
|
-
from .
|
|
70
|
+
from .river_pollution_problems import (
|
|
52
71
|
river_pollution_problem,
|
|
53
72
|
river_pollution_problem_discrete,
|
|
54
73
|
river_pollution_scenario,
|
|
@@ -67,7 +86,3 @@ from .spanish_sustainability_problem import (
|
|
|
67
86
|
spanish_sustainability_problem_discrete,
|
|
68
87
|
)
|
|
69
88
|
from .zdt_problem import zdt1, zdt2, zdt3
|
|
70
|
-
|
|
71
|
-
from .mcwb_problem import (mcwb_solid_rectangular_problem, mcwb_hollow_rectangular_problem,
|
|
72
|
-
mcwb_equilateral_tbeam_problem, mcwb_square_channel_problem, mcwb_tapered_channel_problem,
|
|
73
|
-
mcwb_ragsdell1976_problem)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# A FastAPI server to expose pymoo benchmark problems
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import polars as pl
|
|
5
|
+
import requests
|
|
6
|
+
from fastapi import FastAPI
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
from pymoo.problems import get_problem
|
|
9
|
+
|
|
10
|
+
from desdeo.problem.schema import Objective, Problem, Simulator, Url, Variable
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PymooParameters(BaseModel):
|
|
14
|
+
"""Parameters for a pymoo problem instance."""
|
|
15
|
+
|
|
16
|
+
name: str
|
|
17
|
+
n_var: int
|
|
18
|
+
n_obj: int
|
|
19
|
+
minus: bool = False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ProblemInfo(BaseModel):
|
|
23
|
+
"""Information about a pymoo problem instance."""
|
|
24
|
+
|
|
25
|
+
lower_bounds: dict[str, float]
|
|
26
|
+
"""Lower bounds of the decision variables. Keys are the names of the decision variables, e.g. "x_1", "x_2", etc."""
|
|
27
|
+
upper_bounds: dict[str, float]
|
|
28
|
+
"""Upper bounds of the decision variables."""
|
|
29
|
+
objective_names: list[str]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
app = FastAPI()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_pymoo_problem(p: PymooParameters):
|
|
36
|
+
"""Get a pymoo problem instance by name, number of variables, and number of objectives."""
|
|
37
|
+
params = p.model_dump()
|
|
38
|
+
params.pop("minus")
|
|
39
|
+
return get_problem(**params)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@app.get("/evaluate")
|
|
43
|
+
def evaluate(d: dict[str, list[float]], p: PymooParameters) -> dict[str, Any]:
|
|
44
|
+
"""Evaluate a pymoo problem instance with given parameters and input values."""
|
|
45
|
+
problem = get_pymoo_problem(p)
|
|
46
|
+
|
|
47
|
+
xs_df = pl.DataFrame(d)
|
|
48
|
+
|
|
49
|
+
output = problem.evaluate(xs_df.to_numpy())
|
|
50
|
+
output_df = pl.DataFrame(output, schema=[f"f_{i + 1}" for i in range(problem.n_obj)])
|
|
51
|
+
|
|
52
|
+
return d | output_df.to_dict(as_series=False)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@app.get("/info")
|
|
56
|
+
def info(p: PymooParameters) -> ProblemInfo:
|
|
57
|
+
"""Get information about a pymoo problem instance, including bounds and objective names."""
|
|
58
|
+
problem = get_pymoo_problem(p)
|
|
59
|
+
bounds = problem.bounds()
|
|
60
|
+
|
|
61
|
+
return ProblemInfo(
|
|
62
|
+
lower_bounds={f"x_{i + 1}": bounds[0][i] for i in range(problem.n_var)},
|
|
63
|
+
upper_bounds={f"x_{i + 1}": bounds[1][i] for i in range(problem.n_var)},
|
|
64
|
+
objective_names=[f"f_{i + 1}" for i in range(problem.n_obj)],
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
url = "http://127.0.0.1"
|
|
69
|
+
port = 8000
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def server_problem(parameters: PymooParameters) -> Problem:
|
|
73
|
+
"""Create a Problem instance from pymoo parameters."""
|
|
74
|
+
try:
|
|
75
|
+
info = requests.get(url + f":{port}/info", json=parameters.model_dump())
|
|
76
|
+
info.raise_for_status()
|
|
77
|
+
except requests.RequestException as e:
|
|
78
|
+
raise RuntimeError("Failed to fetch problem info. Is the server running?") from e
|
|
79
|
+
info: ProblemInfo = ProblemInfo.model_validate(info.json())
|
|
80
|
+
|
|
81
|
+
simulator_url = Url(url=f"{url}:{port}/evaluate")
|
|
82
|
+
|
|
83
|
+
return Problem(
|
|
84
|
+
name=parameters.name,
|
|
85
|
+
description=f"Problem {parameters.name} with {parameters.n_var} variables and {parameters.n_obj} objectives.",
|
|
86
|
+
variables=[
|
|
87
|
+
Variable(
|
|
88
|
+
name=f"x_{i + 1}",
|
|
89
|
+
symbol=f"x_{i + 1}",
|
|
90
|
+
lowerbound=info.lower_bounds[f"x_{i + 1}"],
|
|
91
|
+
upperbound=info.upper_bounds[f"x_{i + 1}"],
|
|
92
|
+
variable_type="real",
|
|
93
|
+
)
|
|
94
|
+
for i in range(parameters.n_var)
|
|
95
|
+
],
|
|
96
|
+
objectives=[
|
|
97
|
+
Objective(
|
|
98
|
+
name=f"f_{i + 1}",
|
|
99
|
+
symbol=f"f_{i + 1}",
|
|
100
|
+
simulator_path=simulator_url,
|
|
101
|
+
objective_type="simulator",
|
|
102
|
+
maximize=parameters.minus,
|
|
103
|
+
)
|
|
104
|
+
for i in range(parameters.n_obj)
|
|
105
|
+
],
|
|
106
|
+
simulators=[
|
|
107
|
+
Simulator(
|
|
108
|
+
name="s1",
|
|
109
|
+
symbol="s1",
|
|
110
|
+
url=simulator_url,
|
|
111
|
+
parameter_options=parameters.model_dump(),
|
|
112
|
+
)
|
|
113
|
+
],
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
if __name__ == "__main__":
|
|
118
|
+
import uvicorn
|
|
119
|
+
|
|
120
|
+
uvicorn.run(app)
|