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.
Files changed (126) 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 +5 -1
  87. desdeo/problem/external/__init__.py +18 -0
  88. desdeo/problem/external/core.py +356 -0
  89. desdeo/problem/external/pymoo_provider.py +266 -0
  90. desdeo/problem/external/runtime.py +44 -0
  91. desdeo/problem/infix_parser.py +2 -2
  92. desdeo/problem/pyomo_evaluator.py +25 -6
  93. desdeo/problem/schema.py +69 -48
  94. desdeo/problem/simulator_evaluator.py +65 -15
  95. desdeo/problem/testproblems/__init__.py +26 -11
  96. desdeo/problem/testproblems/benchmarks_server.py +120 -0
  97. desdeo/problem/testproblems/cake_problem.py +185 -0
  98. desdeo/problem/testproblems/dmitry_forest_problem_discrete.py +71 -0
  99. desdeo/problem/testproblems/forest_problem.py +77 -69
  100. desdeo/problem/testproblems/multi_valued_constraints.py +119 -0
  101. desdeo/problem/testproblems/{river_pollution_problem.py → river_pollution_problems.py} +28 -22
  102. desdeo/problem/testproblems/single_objective.py +289 -0
  103. desdeo/problem/testproblems/zdt_problem.py +4 -1
  104. desdeo/tools/__init__.py +39 -21
  105. desdeo/tools/desc_gen.py +22 -0
  106. desdeo/tools/generics.py +22 -2
  107. desdeo/tools/group_scalarization.py +3090 -0
  108. desdeo/tools/indicators_binary.py +107 -1
  109. desdeo/tools/indicators_unary.py +3 -16
  110. desdeo/tools/message.py +33 -2
  111. desdeo/tools/non_dominated_sorting.py +4 -3
  112. desdeo/tools/patterns.py +9 -7
  113. desdeo/tools/pyomo_solver_interfaces.py +48 -35
  114. desdeo/tools/reference_vectors.py +118 -351
  115. desdeo/tools/scalarization.py +340 -1413
  116. desdeo/tools/score_bands.py +491 -328
  117. desdeo/tools/utils.py +117 -49
  118. desdeo/tools/visualizations.py +67 -0
  119. desdeo/utopia_stuff/utopia_problem.py +1 -1
  120. desdeo/utopia_stuff/utopia_problem_old.py +1 -1
  121. {desdeo-2.0.0.dist-info → desdeo-2.1.0.dist-info}/METADATA +46 -28
  122. desdeo-2.1.0.dist-info/RECORD +180 -0
  123. {desdeo-2.0.0.dist-info → desdeo-2.1.0.dist-info}/WHEEL +1 -1
  124. desdeo-2.0.0.dist-info/RECORD +0 -120
  125. /desdeo/api/utils/{logger.py → _logger.py} +0 -0
  126. {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 _le
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 and var.upperbound 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, cons_expr)
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
- result_dict[const.symbol] = pyomo.value(getattr(self.model, const.symbol))
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
- # import skops.io as sio
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 Evaluator:
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, problem: Problem, params: dict[str, dict] | None = None, surrogate_paths: dict[str, Path] | None = None
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
- # call the simulator with the decision variable values and parameters as dicts
140
- res = subprocess.run(
141
- [sys.executable, sim.file, "-d", str(xs), "-p", str(params)], capture_output=True, text=True
142
- )
143
- if res.returncode == 0:
144
- # gather the simulation results (a dict) into the results dataframe
145
- res_df = res_df.hstack(pl.DataFrame(json.loads(res.stdout)))
146
- else:
147
- raise EvaluatorError(res.stderr)
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
- return res_df.hstack(min_obj_columns)
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
- return res.hstack(min_obj_columns)
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
- "simple_knapsack",
13
- "simple_knapsack_vectors",
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
- "rocket_injector_design",
37
- "mixed_variable_dimensions_problem",
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 .river_pollution_problem import (
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)