desdeo 1.2__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 (182) hide show
  1. desdeo/__init__.py +8 -8
  2. desdeo/adm/ADMAfsar.py +551 -0
  3. desdeo/adm/ADMChen.py +414 -0
  4. desdeo/adm/BaseADM.py +119 -0
  5. desdeo/adm/__init__.py +11 -0
  6. desdeo/api/README.md +73 -0
  7. desdeo/api/__init__.py +15 -0
  8. desdeo/api/app.py +50 -0
  9. desdeo/api/config.py +90 -0
  10. desdeo/api/config.toml +64 -0
  11. desdeo/api/db.py +27 -0
  12. desdeo/api/db_init.py +85 -0
  13. desdeo/api/db_models.py +164 -0
  14. desdeo/api/malaga_db_init.py +27 -0
  15. desdeo/api/models/__init__.py +266 -0
  16. desdeo/api/models/archive.py +23 -0
  17. desdeo/api/models/emo.py +128 -0
  18. desdeo/api/models/enautilus.py +69 -0
  19. desdeo/api/models/gdm/gdm_aggregate.py +139 -0
  20. desdeo/api/models/gdm/gdm_base.py +69 -0
  21. desdeo/api/models/gdm/gdm_score_bands.py +114 -0
  22. desdeo/api/models/gdm/gnimbus.py +138 -0
  23. desdeo/api/models/generic.py +104 -0
  24. desdeo/api/models/generic_states.py +401 -0
  25. desdeo/api/models/nimbus.py +158 -0
  26. desdeo/api/models/preference.py +128 -0
  27. desdeo/api/models/problem.py +717 -0
  28. desdeo/api/models/reference_point_method.py +18 -0
  29. desdeo/api/models/session.py +49 -0
  30. desdeo/api/models/state.py +463 -0
  31. desdeo/api/models/user.py +52 -0
  32. desdeo/api/models/utopia.py +25 -0
  33. desdeo/api/routers/_EMO.backup +309 -0
  34. desdeo/api/routers/_NAUTILUS.py +245 -0
  35. desdeo/api/routers/_NAUTILUS_navigator.py +233 -0
  36. desdeo/api/routers/_NIMBUS.py +765 -0
  37. desdeo/api/routers/__init__.py +5 -0
  38. desdeo/api/routers/emo.py +497 -0
  39. desdeo/api/routers/enautilus.py +237 -0
  40. desdeo/api/routers/gdm/gdm_aggregate.py +234 -0
  41. desdeo/api/routers/gdm/gdm_base.py +420 -0
  42. desdeo/api/routers/gdm/gdm_score_bands/gdm_score_bands_manager.py +398 -0
  43. desdeo/api/routers/gdm/gdm_score_bands/gdm_score_bands_routers.py +377 -0
  44. desdeo/api/routers/gdm/gnimbus/gnimbus_manager.py +698 -0
  45. desdeo/api/routers/gdm/gnimbus/gnimbus_routers.py +591 -0
  46. desdeo/api/routers/generic.py +233 -0
  47. desdeo/api/routers/nimbus.py +705 -0
  48. desdeo/api/routers/problem.py +307 -0
  49. desdeo/api/routers/reference_point_method.py +93 -0
  50. desdeo/api/routers/session.py +100 -0
  51. desdeo/api/routers/test.py +16 -0
  52. desdeo/api/routers/user_authentication.py +520 -0
  53. desdeo/api/routers/utils.py +187 -0
  54. desdeo/api/routers/utopia.py +230 -0
  55. desdeo/api/schema.py +100 -0
  56. desdeo/api/tests/__init__.py +0 -0
  57. desdeo/api/tests/conftest.py +151 -0
  58. desdeo/api/tests/test_enautilus.py +330 -0
  59. desdeo/api/tests/test_models.py +1179 -0
  60. desdeo/api/tests/test_routes.py +1075 -0
  61. desdeo/api/utils/_database.py +263 -0
  62. desdeo/api/utils/_logger.py +29 -0
  63. desdeo/api/utils/database.py +36 -0
  64. desdeo/api/utils/emo_database.py +40 -0
  65. desdeo/core.py +34 -0
  66. desdeo/emo/__init__.py +159 -0
  67. desdeo/emo/hooks/archivers.py +188 -0
  68. desdeo/emo/methods/EAs.py +541 -0
  69. desdeo/emo/methods/__init__.py +0 -0
  70. desdeo/emo/methods/bases.py +12 -0
  71. desdeo/emo/methods/templates.py +111 -0
  72. desdeo/emo/operators/__init__.py +1 -0
  73. desdeo/emo/operators/crossover.py +1282 -0
  74. desdeo/emo/operators/evaluator.py +114 -0
  75. desdeo/emo/operators/generator.py +459 -0
  76. desdeo/emo/operators/mutation.py +1224 -0
  77. desdeo/emo/operators/scalar_selection.py +202 -0
  78. desdeo/emo/operators/selection.py +1778 -0
  79. desdeo/emo/operators/termination.py +286 -0
  80. desdeo/emo/options/__init__.py +108 -0
  81. desdeo/emo/options/algorithms.py +435 -0
  82. desdeo/emo/options/crossover.py +164 -0
  83. desdeo/emo/options/generator.py +131 -0
  84. desdeo/emo/options/mutation.py +260 -0
  85. desdeo/emo/options/repair.py +61 -0
  86. desdeo/emo/options/scalar_selection.py +66 -0
  87. desdeo/emo/options/selection.py +127 -0
  88. desdeo/emo/options/templates.py +383 -0
  89. desdeo/emo/options/termination.py +143 -0
  90. desdeo/explanations/__init__.py +6 -0
  91. desdeo/explanations/explainer.py +100 -0
  92. desdeo/explanations/utils.py +90 -0
  93. desdeo/gdm/__init__.py +22 -0
  94. desdeo/gdm/gdmtools.py +45 -0
  95. desdeo/gdm/score_bands.py +114 -0
  96. desdeo/gdm/voting_rules.py +50 -0
  97. desdeo/mcdm/__init__.py +41 -0
  98. desdeo/mcdm/enautilus.py +338 -0
  99. desdeo/mcdm/gnimbus.py +484 -0
  100. desdeo/mcdm/nautili.py +345 -0
  101. desdeo/mcdm/nautilus.py +477 -0
  102. desdeo/mcdm/nautilus_navigator.py +656 -0
  103. desdeo/mcdm/nimbus.py +417 -0
  104. desdeo/mcdm/pareto_navigator.py +269 -0
  105. desdeo/mcdm/reference_point_method.py +186 -0
  106. desdeo/problem/__init__.py +83 -0
  107. desdeo/problem/evaluator.py +561 -0
  108. desdeo/problem/external/__init__.py +18 -0
  109. desdeo/problem/external/core.py +356 -0
  110. desdeo/problem/external/pymoo_provider.py +266 -0
  111. desdeo/problem/external/runtime.py +44 -0
  112. desdeo/problem/gurobipy_evaluator.py +562 -0
  113. desdeo/problem/infix_parser.py +341 -0
  114. desdeo/problem/json_parser.py +944 -0
  115. desdeo/problem/pyomo_evaluator.py +487 -0
  116. desdeo/problem/schema.py +1829 -0
  117. desdeo/problem/simulator_evaluator.py +348 -0
  118. desdeo/problem/sympy_evaluator.py +244 -0
  119. desdeo/problem/testproblems/__init__.py +88 -0
  120. desdeo/problem/testproblems/benchmarks_server.py +120 -0
  121. desdeo/problem/testproblems/binh_and_korn_problem.py +88 -0
  122. desdeo/problem/testproblems/cake_problem.py +185 -0
  123. desdeo/problem/testproblems/dmitry_forest_problem_discrete.py +71 -0
  124. desdeo/problem/testproblems/dtlz2_problem.py +102 -0
  125. desdeo/problem/testproblems/forest_problem.py +283 -0
  126. desdeo/problem/testproblems/knapsack_problem.py +163 -0
  127. desdeo/problem/testproblems/mcwb_problem.py +831 -0
  128. desdeo/problem/testproblems/mixed_variable_dimenrions_problem.py +83 -0
  129. desdeo/problem/testproblems/momip_problem.py +172 -0
  130. desdeo/problem/testproblems/multi_valued_constraints.py +119 -0
  131. desdeo/problem/testproblems/nimbus_problem.py +143 -0
  132. desdeo/problem/testproblems/pareto_navigator_problem.py +89 -0
  133. desdeo/problem/testproblems/re_problem.py +492 -0
  134. desdeo/problem/testproblems/river_pollution_problems.py +440 -0
  135. desdeo/problem/testproblems/rocket_injector_design_problem.py +140 -0
  136. desdeo/problem/testproblems/simple_problem.py +351 -0
  137. desdeo/problem/testproblems/simulator_problem.py +92 -0
  138. desdeo/problem/testproblems/single_objective.py +289 -0
  139. desdeo/problem/testproblems/spanish_sustainability_problem.py +945 -0
  140. desdeo/problem/testproblems/zdt_problem.py +274 -0
  141. desdeo/problem/utils.py +245 -0
  142. desdeo/tools/GenerateReferencePoints.py +181 -0
  143. desdeo/tools/__init__.py +120 -0
  144. desdeo/tools/desc_gen.py +22 -0
  145. desdeo/tools/generics.py +165 -0
  146. desdeo/tools/group_scalarization.py +3090 -0
  147. desdeo/tools/gurobipy_solver_interfaces.py +258 -0
  148. desdeo/tools/indicators_binary.py +117 -0
  149. desdeo/tools/indicators_unary.py +362 -0
  150. desdeo/tools/interaction_schema.py +38 -0
  151. desdeo/tools/intersection.py +54 -0
  152. desdeo/tools/iterative_pareto_representer.py +99 -0
  153. desdeo/tools/message.py +265 -0
  154. desdeo/tools/ng_solver_interfaces.py +199 -0
  155. desdeo/tools/non_dominated_sorting.py +134 -0
  156. desdeo/tools/patterns.py +283 -0
  157. desdeo/tools/proximal_solver.py +99 -0
  158. desdeo/tools/pyomo_solver_interfaces.py +477 -0
  159. desdeo/tools/reference_vectors.py +229 -0
  160. desdeo/tools/scalarization.py +2065 -0
  161. desdeo/tools/scipy_solver_interfaces.py +454 -0
  162. desdeo/tools/score_bands.py +627 -0
  163. desdeo/tools/utils.py +388 -0
  164. desdeo/tools/visualizations.py +67 -0
  165. desdeo/utopia_stuff/__init__.py +0 -0
  166. desdeo/utopia_stuff/data/1.json +15 -0
  167. desdeo/utopia_stuff/data/2.json +13 -0
  168. desdeo/utopia_stuff/data/3.json +15 -0
  169. desdeo/utopia_stuff/data/4.json +17 -0
  170. desdeo/utopia_stuff/data/5.json +15 -0
  171. desdeo/utopia_stuff/from_json.py +40 -0
  172. desdeo/utopia_stuff/reinit_user.py +38 -0
  173. desdeo/utopia_stuff/utopia_db_init.py +212 -0
  174. desdeo/utopia_stuff/utopia_problem.py +403 -0
  175. desdeo/utopia_stuff/utopia_problem_old.py +415 -0
  176. desdeo/utopia_stuff/utopia_reference_solutions.py +79 -0
  177. desdeo-2.1.0.dist-info/METADATA +186 -0
  178. desdeo-2.1.0.dist-info/RECORD +180 -0
  179. {desdeo-1.2.dist-info → desdeo-2.1.0.dist-info}/WHEEL +1 -1
  180. desdeo-2.1.0.dist-info/licenses/LICENSE +21 -0
  181. desdeo-1.2.dist-info/METADATA +0 -16
  182. desdeo-1.2.dist-info/RECORD +0 -4
@@ -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)
@@ -0,0 +1,44 @@
1
+ """Runtime which owns a general ProviderResolver singleton and exposes functions to manage it."""
2
+
3
+ from .core import Provider, ProviderRegistry, ProviderResolver
4
+
5
+ _registry = ProviderRegistry()
6
+ _resolver = ProviderResolver(_registry)
7
+
8
+ # if uri's of other type than 'desdeo://...' are to be supported, update this list
9
+ supported_schemes = ["desdeo"]
10
+
11
+
12
+ def get_registry() -> ProviderRegistry:
13
+ """Get the runtime registry."""
14
+ return _registry
15
+
16
+
17
+ def get_resolver() -> ProviderResolver:
18
+ """Get the runtime provider resolver."""
19
+ return _resolver
20
+
21
+
22
+ def register_provider(name: str, provider: Provider, *, overwrite: bool = False, clear_cache: bool = True) -> None:
23
+ """Register a provider to the current runtime resolver.
24
+
25
+ Args:
26
+ name (str): name of the provider.
27
+ provider (Provider): the instance of the provider.
28
+ overwrite (bool, optional): should an existing provider with the same
29
+ name be overwritten, if it already exits? Defaults to False.
30
+ clear_cache (bool, optional): should the resolver's cache be cleared? Defaults to True.
31
+
32
+ Raises:
33
+ KeyError: if overwrite is 'False' and a provider with the given `name`
34
+ already exists in the register of the resolver.
35
+ """
36
+ reg = get_registry()
37
+
38
+ if reg.has(name) and not overwrite:
39
+ raise KeyError(f"Provider {name!r} already registered")
40
+
41
+ reg.register(name, provider)
42
+
43
+ if clear_cache:
44
+ get_resolver().clear_caches()