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
@@ -33,7 +33,7 @@ class PolarsEvaluatorModesEnum(str, Enum):
33
33
  mixed = "mixed"
34
34
  """Indicates that the problem has analytical and simulator and/or surrogate
35
35
  based objectives, constraints and extra functions. In this mode, the evaluator
36
- only handles data-based and analytical functions. For data-bsed objectives,
36
+ only handles data-based and analytical functions. For data-based objectives,
37
37
  it assumes that the variables are to be evaluated by finding the closest
38
38
  variables values in the data compare to the input, and evaluating the result
39
39
  to be the matching objective function values that match to the closest
@@ -74,7 +74,7 @@ def variable_dimension_enumerate(problem: Problem) -> VariableDimensionEnum:
74
74
  enum = VariableDimensionEnum.scalar
75
75
  for var in problem.variables:
76
76
  if isinstance(var, TensorVariable):
77
- if len(var.shape) == 1 or len(var.shape) == 2 and not (var.shape[0] > 1 and var.shape[1] > 1): # noqa: PLR2004
77
+ if len(var.shape) == 1 or (len(var.shape) == 2 and not (var.shape[0] > 1 and var.shape[1] > 1)): # noqa: PLR2004
78
78
  enum = VariableDimensionEnum.vector
79
79
  else:
80
80
  return VariableDimensionEnum.tensor
@@ -187,7 +187,7 @@ class PolarsEvaluator:
187
187
  f"Provided 'evaluator_mode' {evaluator_mode} not supported. Must be one of {PolarsEvaluatorModesEnum}."
188
188
  )
189
189
 
190
- def _polars_init(self): # noqa: C901, PLR0912
190
+ def _polars_init(self): # noqa: C901
191
191
  """Initialization of the evaluator for parser type 'polars'."""
192
192
  # If any constants are defined in problem, replace their symbol with the defined numerical
193
193
  # value in all the function expressions found in the Problem.
@@ -211,8 +211,7 @@ class PolarsEvaluator:
211
211
  parsed_obj_funcs[f"{obj.symbol}"] = None
212
212
  else:
213
213
  msg = (
214
- f"Incorrect objective-type {obj.objective_type} encountered. "
215
- f"Must be one of {ObjectiveTypeEnum}"
214
+ f"Incorrect objective-type {obj.objective_type} encountered. Must be one of {ObjectiveTypeEnum}"
216
215
  )
217
216
  raise PolarsEvaluatorError(msg)
218
217
 
@@ -0,0 +1,18 @@
1
+ """Export of the external module."""
2
+
3
+ from .core import ProviderParams
4
+ from .pymoo_provider import PymooProblemParams, PymooProvider, create_pymoo_problem
5
+ from .runtime import get_registry, get_resolver, register_provider, supported_schemes
6
+
7
+ # register default providers here
8
+ register_provider("pymoo", PymooProvider())
9
+
10
+ __all__ = [
11
+ "ProviderParams",
12
+ "PymooProblemParams",
13
+ "create_pymoo_problem",
14
+ "get_registry",
15
+ "get_resolver",
16
+ "register_provider",
17
+ "supported_schemes",
18
+ ]
@@ -0,0 +1,356 @@
1
+ """Implements a interface to interface into external (test) problem suites."""
2
+
3
+ import json
4
+ from typing import Any, Literal, Protocol
5
+ from urllib.parse import urlparse
6
+
7
+ import requests
8
+ from pydantic import BaseModel, Field
9
+
10
+ from desdeo.problem import ConstraintTypeEnum, VariableType, VariableTypeEnum
11
+
12
+ Operation = Literal["info", "evaluate"]
13
+ Scheme = Literal["desdeo", "http"]
14
+
15
+
16
+ class ExternalProblemInfo(BaseModel):
17
+ """A model to represent problem information of problems, which are not native to DESDEO."""
18
+
19
+ name: str = Field(description="Name of the problem")
20
+ description: str | None = Field(description="Description of the problem. Default to 'None'.", default=None)
21
+ variable_symbols: list[str] = Field(description="The symbols of the variables.")
22
+ variable_names: dict[str, str] | None = Field(
23
+ description=(
24
+ "The names of the variables. It is expected that the keys are the same as the provided symbols. "
25
+ "Defaults to 'None', in which case the symbols are used."
26
+ )
27
+ )
28
+ variable_type: dict[str, VariableTypeEnum] | None = Field(
29
+ description=(
30
+ "The type of each variable (real, integer, binary). It is "
31
+ "expected that the keys are the same as the provided symbols. If 'None', "
32
+ "the type 'real' is assumed for all variables. Defaults to 'None'."
33
+ )
34
+ )
35
+ variable_lower_bounds: dict[str, VariableType | None] | None = Field(
36
+ description=(
37
+ "The lower bound of each variable. It is "
38
+ "expected that the keys are the same as the provided symbols. If 'None', "
39
+ "the value is None, no bounds are assumed for all lower bounds. variables. Defaults to 'None'."
40
+ )
41
+ )
42
+ variable_upper_bounds: dict[str, VariableType | None] | None = Field(
43
+ description=(
44
+ "The upper bound of each variable. It is "
45
+ "expected that the keys are the same as the provided symbols. If 'None', "
46
+ "the value is None, no bounds are assumed for all upper bounds. variables. Defaults to 'None'."
47
+ )
48
+ )
49
+ objective_symbols: list[str] = Field(description="The names of the objective functions.")
50
+ objective_names: dict[str, str] | None = Field(
51
+ description=(
52
+ "The names of the objectives. It is expected that the keys are the same as the provided symbols. "
53
+ "Defaults to 'None', in which case the symbols are used."
54
+ )
55
+ )
56
+ objective_maximize: dict[str, bool] | None = Field(
57
+ description=(
58
+ "Whether objective are to be maximized. It is "
59
+ "expected that the keys are the same as the provided symbols. "
60
+ "Defaults to 'None', in which case minimization is assumed."
61
+ )
62
+ )
63
+ ideal_point: dict[str, float] | None = Field(
64
+ description=(
65
+ "The ideal point of the problem. The keys should match those of the objective functions. "
66
+ "Defaults to 'None'."
67
+ ),
68
+ default=None,
69
+ )
70
+ nadir_point: dict[str, float] | None = Field(
71
+ description=(
72
+ "The nadir point of the problem. The keys should match those of the objective functions. "
73
+ "Defaults to 'None'."
74
+ ),
75
+ default=None,
76
+ )
77
+ constraint_symbols: list[str] | None = Field(
78
+ description=(
79
+ "The symbols of constraints. If 'None', no constraints are defined for the problem. Defaults to None."
80
+ )
81
+ )
82
+ constraint_names: dict[str, str] | None = Field(
83
+ description=(
84
+ "The names of the constraints. It is expected that the keys are the same as the provided symbols. "
85
+ "Defaults to 'None', in which case the symbols are used."
86
+ )
87
+ )
88
+ constraint_types: dict[str, ConstraintTypeEnum] | None = Field(
89
+ description=(
90
+ "The types (LTE, EQ...) of the constraints. It is expected that the "
91
+ "keys are the same as the provided symbols. Defaults to 'None', in "
92
+ "which case no symbols are assumed to be defined."
93
+ )
94
+ )
95
+
96
+
97
+ class ExternalProblemParams(BaseModel):
98
+ """A model to represent parameters that can be used to generate problems, which are not native to DESDEO."""
99
+
100
+
101
+ class ProviderParams(BaseModel):
102
+ """A model to represent parameters that external libraries can use to build problem."""
103
+
104
+
105
+ class Locator(BaseModel):
106
+ """A model to represent a locator, i.e., a URI."""
107
+
108
+ scheme: Scheme = Field(description="The source of the problem.")
109
+ provider: str | None = Field(description="The provider of the operation. Defaults to 'None'.", default=None)
110
+ op: Operation | None = Field(description="The associated operator. Defaults to 'None'.", default=None)
111
+ raw: str = Field(description="The raw uri.")
112
+
113
+ # for http
114
+ http_url: str | None = Field(description="Uri for web-access. Defaults to 'None'.", default=None)
115
+
116
+
117
+ class LocatorParseError(ValueError):
118
+ """Raised when errors emerge parsing a locator."""
119
+
120
+
121
+ def parse_locator(uri: str) -> Locator:
122
+ """Parses a string representing a uri into a Locator model.
123
+
124
+ Args:
125
+ uri (str): a string containing a uri. Should follow the format 'scheme://provider/operation'.
126
+
127
+ Raises:
128
+ LocatorParsesError: when parsing the uri goes wrong for one reason or another.
129
+
130
+ Returns:
131
+ Locator: a locator with the information parsed from the uri.
132
+ """
133
+ p = urlparse(uri)
134
+ expected_segments = 2
135
+
136
+ if p.scheme in ("http", "https"):
137
+ segments = [s for s in p.path.split("/") if s]
138
+ if len(segments) == expected_segments:
139
+ provider, op = segments
140
+ else:
141
+ raise LocatorParseError(f"Invalid path; expected 2 segments in {uri!r}")
142
+
143
+ return Locator(
144
+ scheme="http",
145
+ op=None,
146
+ provider=None,
147
+ raw=uri,
148
+ http_url=uri,
149
+ )
150
+
151
+ if p.scheme != "desdeo":
152
+ raise LocatorParseError(f"Unsupported scheme: {p.scheme!r} in {uri!r}")
153
+
154
+ segments = [s for s in p.path.split("/") if s]
155
+ if len(segments) == expected_segments:
156
+ provider, op = segments
157
+ else:
158
+ raise LocatorParseError(f"Invalid path; expected 2 segments in {uri!r}")
159
+
160
+ if op not in ("info", "evaluate"):
161
+ raise LocatorParseError(f"Unsupported operation {op!r} in {uri!r}")
162
+
163
+ if not provider or not (provider[0].isalpha() and all(c.isalnum() or c in "_-" for c in provider)):
164
+ raise LocatorParseError(f"Invalid provider identifier {provider!r} in {uri!r}")
165
+
166
+ return Locator(
167
+ scheme="desdeo",
168
+ op=op,
169
+ provider=provider,
170
+ raw=uri,
171
+ http_url=None,
172
+ )
173
+
174
+
175
+ class Provider(Protocol):
176
+ """The minimal interface any implemented provider should fulfill."""
177
+
178
+ def info(self, params: ExternalProblemParams) -> ExternalProblemInfo:
179
+ """Return the information of an external problem the provider exposes.
180
+
181
+ Args:
182
+ params (ExternalProblemParams): the parameters to generate the external problem.
183
+
184
+ Returns:
185
+ ExternalProblemInfo: information on the external problem.
186
+ """
187
+
188
+ def evaluate(
189
+ self, xs: dict[str, VariableType | list[VariableType]], params: ProviderParams | dict[str, Any]
190
+ ) -> dict[str, float]:
191
+ """Given a set of variable values, evalate the external problem.
192
+
193
+ Args:
194
+ xs (dict[str, VariableType]): a set of variables to be evaluated.
195
+ Expected format is {variable symbol: [values]}.
196
+ params (ProviderParams | dict[str, Any]): the parameters that can be used to generate the external problem.
197
+ Can also be a dict.
198
+
199
+ Returns:
200
+ dict[str, float]: a dict with keys corresponding to evaluated fields of the problem, e.g.,
201
+ objective functions, constraints, etc., and values consisting of lists.
202
+
203
+ Note:
204
+ When multiple values are provided for the variables, it is assumed that
205
+ the external problem is evaluated pointwise and that the returned values
206
+ correspond to the input values in the same order.
207
+ """
208
+
209
+
210
+ class UnknownProviderError(KeyError):
211
+ """Raised when a non-existing provider is encountered."""
212
+
213
+
214
+ class ProviderRegistry:
215
+ """A registry of available providers."""
216
+
217
+ def __init__(self) -> None:
218
+ """Initializes the registry with an empty listing of providers."""
219
+ self._providers: dict[str, Provider] = {}
220
+
221
+ def register(self, name: str, provider: Provider) -> None:
222
+ """Register a new provider.
223
+
224
+ Args:
225
+ name (str): the name of the provider.
226
+ provider (Provider): the provider to be registered.
227
+ """
228
+ self._providers[name] = provider
229
+
230
+ def get(self, name: str) -> Provider:
231
+ """Get a provider from the registry based on its name."""
232
+ try:
233
+ return self._providers[name]
234
+ except KeyError as e:
235
+ raise UnknownProviderError(f"Unknown provider: {name!r}") from e
236
+
237
+ def has(self, name: str) -> bool:
238
+ """Checks if a provider already exists in the registry.
239
+
240
+ Args:
241
+ name (str): name of the provider.
242
+
243
+ Returns:
244
+ bool: whether it exists in the registry.
245
+ """
246
+ return name in self._providers
247
+
248
+ def items(self) -> list[Provider]:
249
+ """Return the current providers in the registry.
250
+
251
+ Returns:
252
+ list[Provider]: list with the providers in the registry.
253
+ """
254
+ return self._providers.items()
255
+
256
+
257
+ class ProviderResolverError(RuntimeError):
258
+ """Raised when the resolver fails to resolve to an existing simulator."""
259
+
260
+
261
+ def _stable_json(d: dict[str, Any]) -> str:
262
+ # stable key for caching etc.
263
+ return json.dumps(d, sort_keys=True, separators=(",", ":"))
264
+
265
+
266
+ class ProviderResolver:
267
+ """Defines a resolver for registered providers."""
268
+
269
+ def __init__(self, registry: ProviderRegistry) -> None:
270
+ """Initialize the resolver with a registry of providers.
271
+
272
+ Args:
273
+ registry (ProviderRegistry): a registry of providers the resolver should be aware of.
274
+ """
275
+ self.registry = registry
276
+ self._info_cache: dict[tuple[str, str], dict[str, Any]] = {}
277
+
278
+ def clear_caches(self) -> None:
279
+ """Clears the info cache."""
280
+ self._info_cache.clear()
281
+
282
+ def info(self, locator_uri: str, params: dict[str, Any]) -> ExternalProblemInfo:
283
+ """Get info on the problem from the resolved provider.
284
+
285
+ Args:
286
+ locator_uri (str): the uri to locate the provider of the problem's info.
287
+ params (dict[str, Any]): parameters given to the provider.
288
+
289
+ Raises:
290
+ ProviderResolverError: when the provider cannot be resolved.
291
+
292
+ Returns:
293
+ dict[str, Any]:
294
+ """
295
+ loc = parse_locator(locator_uri)
296
+
297
+ if loc.scheme == "http":
298
+ raise ProviderResolverError("TODO: HTTP info not implemented.")
299
+
300
+ if loc.provider is None:
301
+ raise ProviderResolverError("Could not resolve the provider.")
302
+
303
+ cache_key = (loc.provider, _stable_json(params))
304
+
305
+ if cache_key in self._info_cache:
306
+ return self._info_cache[cache_key]
307
+
308
+ provider = self.registry.get(loc.provider)
309
+ out = provider.info(params)
310
+
311
+ self._info_cache[cache_key] = out
312
+
313
+ return out
314
+
315
+ def evaluate(
316
+ self,
317
+ locator_uri: str,
318
+ params: ProviderParams | dict[str, Any],
319
+ xs: dict[str, VariableType | list[VariableType]],
320
+ ) -> dict[str, float]:
321
+ """Evaluate the problem with a resolved provider.
322
+
323
+ Args:
324
+ locator_uri (str): the uri to locate the provider.
325
+ params (ProviderParams | dict[str, Any]): parameters given to the provider.
326
+ xs (dict[str, VariableType]): a set of variables to be evaluated.
327
+ Expected format is {variable symbol: [values]}.
328
+
329
+ Raises:
330
+ ProviderResolverError: could not evaluate the problem.
331
+
332
+ Returns:
333
+ dict[str, float]: a dict with keys corresponding to evaluated fields of the problem, e.g.,
334
+ objective functions, constraints, etc., and values consisting of lists.
335
+
336
+ Note:
337
+ When multiple values are provided for the variables, it is assumed that
338
+ the external problem is evaluated pointwise and that the returned values
339
+ correspond to the input values in the same order.
340
+ """
341
+ loc = parse_locator(locator_uri)
342
+ if loc.scheme == "http":
343
+ # remote
344
+ try:
345
+ r = requests.post(loc.http_url, json={"params": params, "X": xs}, timeout=30)
346
+ r.raise_for_status()
347
+ return r.json()
348
+ except requests.RequestException as e:
349
+ raise ProviderResolverError(f"HTTP evaluation failed for {loc.http_url!r}") from e
350
+
351
+ if loc.provider is None:
352
+ raise ProviderResolverError("Could not resolve the provider.")
353
+
354
+ provider = self.registry.get(loc.provider)
355
+
356
+ return provider.evaluate(xs, params)
@@ -0,0 +1,266 @@
1
+ """Implements a provider for the test problems found in Pymoo."""
2
+
3
+ import json
4
+ from functools import lru_cache
5
+ from typing import Any
6
+
7
+ import numpy as np
8
+ from pydantic import Field
9
+ from pymoo.problems import get_problem as pymoo_get_problem
10
+
11
+ from desdeo.problem import (
12
+ Constraint,
13
+ ConstraintTypeEnum,
14
+ Objective,
15
+ Problem,
16
+ Simulator,
17
+ Url,
18
+ Variable,
19
+ VariableType,
20
+ VariableTypeEnum,
21
+ )
22
+
23
+ from .core import ExternalProblemInfo, ExternalProblemParams, Provider
24
+
25
+ _pymoo_locator_evaluate = "desdeo://external/pymoo/evaluate"
26
+
27
+
28
+ class PymooProblemParams(ExternalProblemParams):
29
+ """Parameters to generate Pymoo test problems.
30
+
31
+ See: https://pymoo.org/problems/test_problems.html
32
+ """
33
+
34
+ name: str = Field(description="The name of the model. See https://pymoo.org/problems/test_problems.html")
35
+ n_var: int | None = Field(description="The number of desirable variables (if relevant).", default=None)
36
+ n_obj: int | None = Field(description="The number of desirable objective functions (if relevant).", default=None)
37
+ extra: dict[str, Any] | None = Field(description="Any other extra parameters.", default=None)
38
+
39
+
40
+ def _stable_json(d: dict[str, Any]) -> str:
41
+ return json.dumps(d, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
42
+
43
+
44
+ def _params_key(params: dict) -> str:
45
+ """Canonical cache key for a PymooParams instance.
46
+
47
+ Canonical cache key for a PymooParams instance.
48
+ Includes 'extra' (if present) and omits None fields.
49
+ """
50
+ return _stable_json(params)
51
+
52
+
53
+ @lru_cache(maxsize=256)
54
+ def _get_cached_pymoo_problem(params_key: str):
55
+ """Cache pymoo Problem instances by a stable JSON key.
56
+
57
+ Note: per-process cache (fine for typical single-worker runs).
58
+ """
59
+ params_dict = json.loads(params_key)
60
+
61
+ extra = params_dict.pop("extra", None)
62
+ if isinstance(extra, dict):
63
+ params_dict |= extra
64
+
65
+ return pymoo_get_problem(**{k: v for k, v in params_dict.items() if v is not None})
66
+
67
+
68
+ @lru_cache(maxsize=256)
69
+ def _get_cached_info(params_key: str) -> ExternalProblemInfo:
70
+ """Cache ExternalProblemInfo too, since it derives from the cached pymoo problem."""
71
+ pymoo_problem = _get_cached_pymoo_problem(params_key)
72
+
73
+ p_name = pymoo_problem.name() # why?
74
+ p_n_var = pymoo_problem.n_var
75
+ p_n_obj = pymoo_problem.n_obj
76
+ p_n_ieq_constr = pymoo_problem.n_ieq_constr
77
+ p_n_eq_constr = pymoo_problem.n_eq_constr
78
+ p_xl = pymoo_problem.xl
79
+ p_xu = pymoo_problem.xu
80
+ p_vtype = pymoo_problem.vtype
81
+
82
+ try:
83
+ p_ideal = pymoo_problem.ideal_point()
84
+ except Exception:
85
+ p_ideal = None
86
+ try:
87
+ p_nadir = pymoo_problem.nadir_point()
88
+ except Exception:
89
+ p_nadir = None
90
+
91
+ var_symbols = [f"x_{i}" for i in range(1, p_n_var + 1)]
92
+ obj_symbols = [f"f_{i}" for i in range(1, p_n_obj + 1)]
93
+ constr_symbols = (
94
+ [f"c_{i}" for i in range(1, p_n_ieq_constr + p_n_eq_constr + 1)] if p_n_ieq_constr + p_n_eq_constr > 0 else None
95
+ )
96
+ constr_types = [ConstraintTypeEnum.LTE] * p_n_ieq_constr + [ConstraintTypeEnum.EQ] * p_n_eq_constr
97
+
98
+ var_type_mapping = {float: VariableTypeEnum.real, int: VariableTypeEnum.integer, bool: VariableTypeEnum.binary}
99
+
100
+ return ExternalProblemInfo(
101
+ name=p_name,
102
+ description=f"The {p_name} problem as defined in the Pymoo library.",
103
+ variable_symbols=var_symbols,
104
+ variable_names=dict(zip(var_symbols, var_symbols, strict=True)),
105
+ variable_type=dict.fromkeys(var_symbols, var_type_mapping[p_vtype]), # assumed all same
106
+ variable_lower_bounds=dict(zip(var_symbols, p_xl, strict=True)),
107
+ variable_upper_bounds=dict(zip(var_symbols, p_xu, strict=True)),
108
+ objective_symbols=obj_symbols,
109
+ objective_names=dict(zip(obj_symbols, obj_symbols, strict=True)),
110
+ objective_maximize=dict.fromkeys(obj_symbols, False), # assumed all min
111
+ ideal_point=dict(zip(obj_symbols, p_ideal, strict=True)) if p_ideal is not None else None,
112
+ nadir_point=dict(zip(obj_symbols, p_nadir, strict=True)) if p_nadir is not None else None,
113
+ constraint_symbols=constr_symbols,
114
+ constraint_names=dict(zip(constr_symbols, constr_symbols, strict=True)) if constr_symbols is not None else None,
115
+ constraint_types=dict(zip(constr_symbols, constr_types, strict=True)) if len(constr_types) > 0 else None,
116
+ )
117
+
118
+
119
+ class PymooProvider(Provider):
120
+ """Provider to get info on and evaluate Pymoo test problems.
121
+
122
+ Note:
123
+ the methods `info` and `evaluate` use caching so that the Pymoo problem is generated only once
124
+ in case of multiple consecutive evaluations of the same problem.
125
+ """
126
+
127
+ def info(self, params: PymooProblemParams | dict[str, Any]) -> ExternalProblemInfo:
128
+ """Get info on a Pymoo test problem.
129
+
130
+ Args:
131
+ params (PymooProblemParams | dict[str, Any]): parameters to generate the problem.
132
+
133
+ Returns:
134
+ ExternalProblemInfo: info on the problem generated based on the provided parameters.
135
+ """
136
+ return _get_cached_info(_params_key(params if isinstance(params, dict) else params.model_dump()))
137
+
138
+ def evaluate(
139
+ self, xs: dict[str, VariableType | list[VariableType]], params: PymooProblemParams | dict[str, Any]
140
+ ) -> dict[str, float]:
141
+ """Evaluate a Pymoo test problem.
142
+
143
+ Args:
144
+ xs (dict[str, VariableType]): a set of variables to be evaluated.
145
+ Expected format is {variable symbol: [values]}.
146
+ params (ProviderParams | dict[str, Any]): parameters that generate the problem Pymoo
147
+ problem to be evaluated.
148
+
149
+ Returns:
150
+ dict[str, float]: a dict with keys corresponding to evaluated fields of the problem, e.g.,
151
+ objective functions, constraints, etc., and values consisting of lists.
152
+
153
+ Note:
154
+ When multiple values are provided for the variables, it is assumed that
155
+ the external problem is evaluated pointwise and that the returned values
156
+ correspond to the input values in the same order.
157
+ """
158
+ params_key = _params_key(params if isinstance(params, dict) else params.model_dump())
159
+ problem = _get_cached_pymoo_problem(params_key)
160
+ info = _get_cached_info(params_key)
161
+
162
+ out = {"F": None, "G": None, "H": None}
163
+
164
+ xs_arr = np.atleast_2d(np.column_stack([xs[k] for k in info.variable_symbols]))
165
+ problem._evaluate(np.asarray(xs_arr, dtype=float), out)
166
+
167
+ # parse output
168
+ obj_results = dict(zip(info.objective_symbols, np.atleast_2d(out["F"].T).tolist(), strict=True))
169
+ _constrs = (np.atleast_2d(out["G"].T).tolist() if out["G"] is not None else []) + (
170
+ np.atleast_2d(out["H"].T).tolist() if out["H"] is not None else []
171
+ )
172
+ constr_results = dict(zip(info.constraint_symbols, _constrs, strict=True)) if len(_constrs) > 0 else {}
173
+
174
+ return obj_results | constr_results
175
+
176
+
177
+ def create_pymoo_problem(params: PymooProblemParams) -> Problem:
178
+ """Create a Pymoo test problem based on given parameters.
179
+
180
+ Args:
181
+ params (PymooProblemParams): the parameters to generate the Pymoo test problem.
182
+
183
+ Returns:
184
+ Problem: an instance of a Pymoo test problem generated based on the given parameters.
185
+
186
+ Note:
187
+ Any `Problem` generated with this function should be considered to be black-box, i.e.,
188
+ the exact mathematical forms are not available. However, if the user is knowledgeable
189
+ about the properties of the problem, they can still use, for example, some gradient-based
190
+ solvers available in DESDEO to try and solve the problem, if the problem is differentiable.
191
+
192
+ Ideal and nadir point values will be available, if they are available in Pymoo for a given problem.
193
+
194
+ For info on the possible problems to generate, see https://pymoo.org/problems/test_problems.html
195
+ """
196
+ provider = PymooProvider()
197
+ info = provider.info(params)
198
+
199
+ simulator_url = Url(url=_pymoo_locator_evaluate)
200
+
201
+ variables: list[Variable] = []
202
+ for sym in info.variable_symbols:
203
+ v_name = info.variable_names[sym] if info.variable_names is not None else sym
204
+ v_type = info.variable_type[sym] if info.variable_type is not None else VariableTypeEnum.real
205
+ lb = info.variable_lower_bounds[sym] if info.variable_lower_bounds is not None else None
206
+ ub = info.variable_upper_bounds[sym] if info.variable_upper_bounds is not None else None
207
+
208
+ variables.append(
209
+ Variable(
210
+ name=v_name,
211
+ symbol=sym,
212
+ lowerbound=lb,
213
+ upperbound=ub,
214
+ variable_type=v_type,
215
+ )
216
+ )
217
+
218
+ objectives: list[Objective] = []
219
+ for sym in info.objective_symbols:
220
+ o_name = info.objective_names[sym] if info.objective_names is not None else sym
221
+ maximize = info.objective_maximize[sym] if info.objective_maximize is not None else False
222
+
223
+ objectives.append(
224
+ Objective(
225
+ name=o_name,
226
+ symbol=sym,
227
+ simulator_path=simulator_url,
228
+ objective_type="simulator",
229
+ maximize=maximize,
230
+ )
231
+ )
232
+
233
+ # Constraints (optional)
234
+ constraints: list[Constraint] = []
235
+ if info.constraint_symbols is not None and len(info.constraint_symbols) > 0:
236
+ for sym in info.constraint_symbols:
237
+ c_name = info.constraint_names[sym] if info.constraint_names is not None else sym
238
+ c_type = info.constraint_types[sym] if info.constraint_types is not None else ConstraintTypeEnum.LTE
239
+
240
+ constraints.append(
241
+ Constraint(
242
+ name=c_name,
243
+ symbol=sym,
244
+ simulator_path=simulator_url,
245
+ cons_type=c_type,
246
+ )
247
+ )
248
+
249
+ simulator = Simulator(
250
+ name="pymoo_sim",
251
+ symbol="pymoo_sim",
252
+ url=simulator_url,
253
+ parameter_options=params.model_dump(exclude_none=True),
254
+ )
255
+
256
+ problem = Problem(
257
+ name=info.name,
258
+ description=info.description,
259
+ variables=variables,
260
+ objectives=objectives,
261
+ constraints=constraints if len(constraints) > 0 else None,
262
+ simulators=[simulator],
263
+ )
264
+
265
+ # add ideal and nadir (might be None)
266
+ return problem.update_ideal_and_nadir(info.ideal_point, info.nadir_point)