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,265 @@
1
+ """Defines the messaging protocol used by the various EMO operators."""
2
+
3
+ from enum import Enum
4
+ from typing import Any, Literal
5
+
6
+ import numpy as np
7
+ from polars import DataFrame
8
+ from pydantic import BaseModel, ConfigDict, Field, field_serializer
9
+
10
+
11
+ class CrossoverMessageTopics(Enum):
12
+ """Topics for messages related to crossover operators."""
13
+
14
+ TEST = "TEST"
15
+ """ A message topic used only for testing the crossover operators. """
16
+ XOVER_PROBABILITY = "XOVER_PROBABILITY"
17
+ """ The current crossover probability. """
18
+ XOVER_DISTRIBUTION = "XOVER_DISTRIBUTION"
19
+ """ The current crossover distribution index. Primary used in the SBX crossover. """
20
+ PARENTS = "PARENTS"
21
+ """ The parents selected for crossover. """
22
+ OFFSPRINGS = "OFFSPRINGS"
23
+ """ The offsprings generated from the crossover. """
24
+ ALPHA = "ALPHA"
25
+ """ Alpha parameter used in crossover. """
26
+ LAMBDA = "LAMBDA"
27
+ """ Lambda parameter used in crossover. Primarily used in the bounded exponential xover. """
28
+
29
+
30
+ class MutationMessageTopics(Enum):
31
+ """Topics for messages related to mutation operators."""
32
+
33
+ TEST = "TEST"
34
+ """ A message topic used only for testing the mutation operators. """
35
+ MUTATION_PROBABILITY = "MUTATION_PROBABILITY"
36
+ """ The current mutation probability. """
37
+ MUTATION_DISTRIBUTION = "MUTATION_DISTRIBUTION"
38
+ """ The current mutation distribution index. Primary used in the polynomial mutation. """
39
+ OFFSPRING_ORIGINAL = "OFFSPRING_ORIGINAL"
40
+ """ The original offsprings before mutation. """
41
+ OFFSPRINGS = "OFFSPRINGS"
42
+ """ The offsprings after mutation. """
43
+ PARENTS = "PARENTS"
44
+ """ The parents of the offsprings. """
45
+
46
+
47
+ class EvaluatorMessageTopics(Enum):
48
+ """Topics for messages related to evaluator operators."""
49
+
50
+ TEST = "TEST"
51
+ """ A message topic used only for testing the evaluator operators. """
52
+ POPULATION = "POPULATION"
53
+ """ The population to evaluate. """
54
+ OUTPUTS = "OUTPUTS"
55
+ """ The outputs of the population. Contains objectives, targets, constraints. """
56
+ OBJECTIVES = "OBJECTIVES"
57
+ """ The true objective values of the population. """
58
+ TARGETS = "TARGETS"
59
+ """ The targets, i.e., objective values seen by the evolutionary operators."""
60
+ CONSTRAINTS = "CONSTRAINTS"
61
+ """ The constraints of the population. """
62
+ VERBOSE_OUTPUTS = "VERBOSE_OUTPUTS"
63
+ """ Same as POPULATION + OUTPUTS."""
64
+ NEW_EVALUATIONS = "NEW_EVALUATIONS"
65
+ """ The number of new evaluations. """
66
+
67
+
68
+ class GeneratorMessageTopics(Enum):
69
+ """Topics for messages related to population generator operators."""
70
+
71
+ TEST = "TEST"
72
+ """ A message topic used only for testing the evaluator operators. """
73
+ POPULATION = "POPULATION"
74
+ """ The population to evaluate. """
75
+ OUTPUTS = "OUTPUTS"
76
+ """ The outputs of the population generation. Contains objectives, targets, and constraints. """
77
+ OBJECTIVES = "OBJECTIVES"
78
+ """ The true objective values of the population. """
79
+ TARGETS = "TARGETS"
80
+ """ The targets, i.e., objective values seen by the evolutionary operators."""
81
+ CONSTRAINTS = "CONSTRAINTS"
82
+ """ The constraints of the population. """
83
+ VERBOSE_OUTPUTS = "VERBOSE_OUTPUTS"
84
+ """ Same as POPULATION + OUTPUTS. """
85
+ NEW_EVALUATIONS = "NEW_EVALUATIONS"
86
+ """ The number of new evaluations. """
87
+
88
+
89
+ class SelectorMessageTopics(Enum):
90
+ """Topics for messages related to selector operators."""
91
+
92
+ TEST = "TEST"
93
+ """ A message topic used only for testing the selector operators. """
94
+ STATE = "STATE"
95
+ """ The state of the parameters of the selector. """
96
+ INDIVIDUALS = "INDIVIDUALS"
97
+ """ The individuals to select from. """
98
+ OUTPUTS = "OUTPUTS"
99
+ """ The outputs of the individuals. """
100
+ CONSTRAINTS = "CONSTRAINTS"
101
+ """ The constraints of the individuals. """
102
+ SELECTED_INDIVIDUALS = "SELECTED_INDIVIDUALS"
103
+ """ The individuals selected by the selector. """
104
+ SELECTED_OUTPUTS = "SELECTED_OUTPUTS"
105
+ """ The targets of the selected individuals. """
106
+ SELECTED_FITNESS = "SELECTED_FITNESS"
107
+ """ The fitness of the selected individuals. This is the fitness calculated by the selector, not the objectives."""
108
+ SELECTED_VERBOSE_OUTPUTS = "SELECTED_VERBOSE_OUTPUTS"
109
+ """ Same as SELECTED_OUTPUTS + SELECTED_INDIVIDUALS"""
110
+ REFERENCE_VECTORS = "REFERENCE_VECTORS"
111
+ """ The reference vectors used in the selection in decomposition-based EMO algorithms. """
112
+
113
+
114
+ class TerminatorMessageTopics(Enum):
115
+ """Topics for messages related to terminator operators."""
116
+
117
+ TEST = "TEST"
118
+ """ A message topic used only for testing the terminator operators. """
119
+ STATE = "STATE"
120
+ """ The state of the parameters of the terminator. """
121
+ TERMINATION = "TERMINATION"
122
+ """ The value of the termination condition. """
123
+ GENERATION = "GENERATION"
124
+ """ The current generation number. """
125
+ EVALUATION = "EVALUATION"
126
+ """ The current number of evaluations. """
127
+ MAX_GENERATIONS = "MAX_GENERATIONS"
128
+ """ The maximum number of generations. """
129
+ MAX_EVALUATIONS = "MAX_EVALUATIONS"
130
+ """ The maximum number of evaluations. """
131
+
132
+
133
+ class ReferenceVectorMessageTopics(Enum):
134
+ """Topics for messages related to the reference vectors."""
135
+
136
+ TEST = "TEST"
137
+
138
+
139
+ MessageTopics = (
140
+ CrossoverMessageTopics
141
+ | MutationMessageTopics
142
+ | EvaluatorMessageTopics
143
+ | GeneratorMessageTopics
144
+ | SelectorMessageTopics
145
+ | TerminatorMessageTopics
146
+ | ReferenceVectorMessageTopics
147
+ | Literal[
148
+ "ALL"
149
+ ] # Used to indicate that all topics are of interest to a subscriber.
150
+ )
151
+
152
+
153
+ class BaseMessage(BaseModel):
154
+ """A message containing an integer value."""
155
+
156
+ topic: MessageTopics = Field(..., description="The topic of the message.")
157
+ """ The topic of the message. """
158
+ source: str = Field(..., description="The source of the message.")
159
+ """ The source of the message. """
160
+
161
+
162
+ class IntMessage(BaseMessage):
163
+ """A message containing an integer value."""
164
+
165
+ value: int = Field(..., description="The integer value of the message.")
166
+ """ The integer value of the message. """
167
+
168
+
169
+ class FloatMessage(BaseMessage):
170
+ """A message containing a float value."""
171
+
172
+ value: float = Field(..., description="The float value of the message.")
173
+ """ The float value of the message. """
174
+
175
+
176
+ class StringMessage(BaseMessage):
177
+ """A message containing a string value."""
178
+
179
+ value: str = Field(..., description="The string value of the message.")
180
+ """ The string value of the message. """
181
+
182
+
183
+ class BoolMessage(BaseMessage):
184
+ """A message containing a boolean value."""
185
+
186
+ value: bool = Field(..., description="The boolean value of the message.")
187
+ """ The boolean value of the message. """
188
+
189
+
190
+ class DictMessage(BaseMessage):
191
+ """A message containing a dictionary value."""
192
+
193
+ value: dict[str, Any] = Field(
194
+ ..., description="The dictionary value of the message."
195
+ )
196
+ """ The dictionary value of the message. """
197
+
198
+
199
+ class Array2DMessage(BaseMessage):
200
+ """A message containing a 2D array value, such as a population or a set of objectives."""
201
+
202
+ value: list[list[float]] = Field(..., description="The array value of the message.")
203
+ """ The array value of the message. """
204
+
205
+
206
+ class PolarsDataFrameMessage(BaseMessage):
207
+ """A message containing a 2D array value, such as a population or a set of objectives."""
208
+
209
+ value: DataFrame = Field(..., description="The array value of the message.")
210
+ """ The array value of the message. """
211
+
212
+ model_config = ConfigDict(arbitrary_types_allowed=True)
213
+
214
+ @field_serializer("value")
215
+ def _serialize_value(self, value: DataFrame) -> dict[str, list[int | float]]:
216
+ return value.to_dict(as_series=False)
217
+
218
+
219
+ class NumpyArrayMessage(BaseMessage):
220
+ """A message containing a numpy array value."""
221
+
222
+ value: np.ndarray = Field(..., description="The numpy array value of the message.")
223
+ """ The numpy array value of the message. """
224
+
225
+ model_config = ConfigDict(arbitrary_types_allowed=True)
226
+
227
+ @field_serializer("value")
228
+ def _serialize_value(self, value: np.ndarray) -> list[list[float]]:
229
+ return value.tolist()
230
+
231
+
232
+ class GenericMessage(BaseMessage):
233
+ """A message containing a generic value."""
234
+
235
+ value: Any = Field(..., description="The generic value of the message.")
236
+ """ The generic value of the message. """
237
+
238
+
239
+ Message = (
240
+ IntMessage
241
+ | FloatMessage
242
+ | DictMessage
243
+ | Array2DMessage
244
+ | GenericMessage
245
+ | StringMessage
246
+ | BoolMessage
247
+ | PolarsDataFrameMessage
248
+ | NumpyArrayMessage
249
+ )
250
+
251
+ AllowedMessagesAtVerbosity: dict[int, tuple[type[Message], ...]] = {
252
+ 0: (),
253
+ 1: (IntMessage, FloatMessage, StringMessage, BoolMessage),
254
+ 2: (
255
+ IntMessage,
256
+ FloatMessage,
257
+ StringMessage,
258
+ BoolMessage,
259
+ DictMessage,
260
+ Array2DMessage,
261
+ GenericMessage,
262
+ PolarsDataFrameMessage,
263
+ NumpyArrayMessage,
264
+ ),
265
+ }
@@ -0,0 +1,199 @@
1
+ """Solver interfaces to the optimization routines found in nevergrad.
2
+
3
+ For more info, see https://facebookresearch.github.io/nevergrad/index.html
4
+ """
5
+
6
+ from concurrent.futures import ThreadPoolExecutor
7
+ from typing import Literal
8
+
9
+ import nevergrad as ng
10
+ from pydantic import BaseModel, Field
11
+
12
+ from desdeo.problem import Problem, SympyEvaluator
13
+ from desdeo.tools.generics import BaseSolver, SolverResults
14
+
15
+ available_nevergrad_optimizers = [
16
+ "NGOpt",
17
+ "TwoPointsDE",
18
+ "PortfolioDiscreteOnePlusOne",
19
+ "OnePlusOne",
20
+ "CMA",
21
+ "TBPSA",
22
+ "PSO",
23
+ "ScrHammersleySearchPlusMiddlePoint",
24
+ "RandomSearch",
25
+ ]
26
+
27
+
28
+ class NevergradGenericOptions(BaseModel):
29
+ """Defines options to be passed to nevergrad's optimization routines."""
30
+
31
+ budget: int = Field(description="The maximum number of allowed function evaluations.", default=100)
32
+ """The maximum number of allowed function evaluations. Defaults to 100."""
33
+
34
+ num_workers: int = Field(description="The maximum number of allowed parallel evaluations.", default=1)
35
+ """The maximum number of allowed parallel evaluations. This is currently
36
+ used to define the batch size when evaluating problems. Defaults to 1."""
37
+
38
+ optimizer: Literal[*available_nevergrad_optimizers] = Field(
39
+ description=(
40
+ "The optimizer to be used. Must be one of `NGOpt`, `TwoPointDE`, `PortfolioDiscreteOnePlusOne`, "
41
+ "`OnePlusOne`, `CMA`, `TBPSA`, `PSO`, `ScrHammersleySearchPlusMiddlePoint`, or `RandomSearch`. "
42
+ "Defaults to `NGOpt`."
43
+ ),
44
+ default="NGOpt",
45
+ )
46
+ """The optimizer to be used. Must be one of `NGOpt`, `TwoPointsDE`, `PortfolioDiscreteOnePlusOne`,
47
+ `OnePlusOne`, `CMA`, `TBPSA`, `PSO`, `ScrHammersleySearchPlusMiddlePoint`, or `RandomSearch`.
48
+ Defaults to `NGOpt`."""
49
+
50
+
51
+ _default_nevergrad_generic_options = NevergradGenericOptions()
52
+ """The set of default options for nevergrad's NgOpt optimizer."""
53
+
54
+
55
+ def parse_ng_results(results: dict, problem: Problem, evaluator: SympyEvaluator) -> SolverResults:
56
+ """Parses the optimization results returned by nevergrad solvers.
57
+
58
+ Args:
59
+ results (dict): the results. A dict with at least the keys
60
+ `recommendation`, which points to a parametrization returned by
61
+ nevergrad solvers, `message` with information about the optimization,
62
+ and `success` indicating whther a recommendation was found successfully
63
+ or not.
64
+ problem (Problem): the problem the results belong to.
65
+ evaluator (GenericEvaluator): the evaluator used to evaluate the problem.
66
+
67
+ Returns:
68
+ SolverResults: a pydantic dataclass withthe relevant optimization results.
69
+ """
70
+ optimal_variables = results["recommendation"].value
71
+ success = results["success"]
72
+ msg = results["message"]
73
+
74
+ results = evaluator.evaluate(optimal_variables)
75
+
76
+ optimal_objectives = {obj.symbol: results[obj.symbol] for obj in problem.objectives}
77
+
78
+ constraint_values = (
79
+ {con.symbol: results[con.symbol] for con in problem.constraints} if problem.constraints is not None else None
80
+ )
81
+ extra_func_values = (
82
+ {extra.symbol: results[extra.symbol] for extra in problem.extra_funcs}
83
+ if problem.extra_funcs is not None
84
+ else None
85
+ )
86
+ scalarization_values = (
87
+ {scal.symbol: results[scal.symbol] for scal in problem.scalarization_funcs}
88
+ if problem.scalarization_funcs is not None
89
+ else None
90
+ )
91
+
92
+ return SolverResults(
93
+ optimal_variables=optimal_variables,
94
+ optimal_objectives=optimal_objectives,
95
+ constraint_values=constraint_values,
96
+ extra_func_values=extra_func_values,
97
+ scalarization_values=scalarization_values,
98
+ success=success,
99
+ message=msg,
100
+ )
101
+
102
+
103
+ class NevergradGenericSolver(BaseSolver):
104
+ """Creates a solver that utilizes optimizations routines found in the nevergrad library."""
105
+
106
+ def __init__(self, problem: Problem, options: NevergradGenericOptions | None = _default_nevergrad_generic_options):
107
+ """Creates a solver that utilizes optimizations routines found in the nevergrad library.
108
+
109
+ These solvers are best utilized for black-box, gradient free optimization with
110
+ computationally expensive function calls. Utilizing multiple workers is recommended
111
+ (see `NevergradGenericOptions`) when function calls are heavily I/O bound.
112
+
113
+ See https://facebookresearch.github.io/nevergrad/getting_started.html for further information
114
+ on nevergrad and its solvers.
115
+
116
+ References:
117
+ Rapin, J., & Teytaud, O. (2018). Nevergrad - A gradient-free
118
+ optimization platform. GitHub.
119
+ https://GitHub.com/FacebookResearch/Nevergrad
120
+
121
+ Args:
122
+ problem (Problem): the problem to be solved.
123
+ options (NgOptOptions | None): options to be passes to the solver.
124
+ If none, `_default_ng_ngopt_options` are used. Defaults to None.
125
+
126
+ Returns:
127
+ Callable[[str], SolverResults]: returns a callable function that takes
128
+ as its argument one of the symbols defined for a function expression in
129
+ problem.
130
+ """
131
+ self.problem = problem
132
+ self.options = options if options is not None else _default_nevergrad_generic_options
133
+ self.evaluator = SympyEvaluator(problem)
134
+
135
+ def solve(self, target: str) -> SolverResults:
136
+ """Solve the problem for the given target.
137
+
138
+ Args:
139
+ target (str): the symbol of the objective function to be optimized.
140
+
141
+ Returns:
142
+ SolverResults: the results of the optimization.
143
+ """
144
+ parametrization = ng.p.Dict(
145
+ **{
146
+ var.symbol: ng.p.Scalar(
147
+ # sets the initial value of the variables, if None, then the
148
+ # mid-point of the lower and upper bounds is chosen as the
149
+ # initial value.
150
+ init=var.initial_value if var.initial_value is not None else (var.lowerbound + var.upperbound) / 2
151
+ ).set_bounds(var.lowerbound, var.upperbound)
152
+ for var in self.problem.variables
153
+ }
154
+ )
155
+
156
+ optimizer = ng.optimizers.registry[self.options.optimizer](
157
+ parametrization=parametrization, **self.options.model_dump(exclude="optimizer")
158
+ )
159
+
160
+ constraint_symbols = (
161
+ None if self.problem.constraints is None else [con.symbol for con in self.problem.constraints]
162
+ )
163
+
164
+ try:
165
+ if optimizer.num_workers == 1:
166
+ # single thread
167
+ recommendation = optimizer.minimize(
168
+ lambda xs, t=target: self.evaluator.evaluate_target(xs, t),
169
+ constraint_violation=[
170
+ lambda xs, t=con_t: self.evaluator.evaluate_target(xs, t) for con_t in constraint_symbols
171
+ ]
172
+ if constraint_symbols is not None
173
+ else None,
174
+ )
175
+
176
+ elif optimizer.num_workers > 1:
177
+ # multiple processors
178
+ with ThreadPoolExecutor(max_workers=optimizer.num_workers) as executor:
179
+ recommendation = optimizer.minimize(
180
+ lambda xs, t=target: self.evaluator.evaluate_target(xs, t),
181
+ constraint_violation=[
182
+ lambda xs, t=con_t: self.evaluator.evaluate_target(xs, t) for con_t in constraint_symbols
183
+ ]
184
+ if constraint_symbols is not None
185
+ else None,
186
+ executor=executor,
187
+ batch_mode=False,
188
+ )
189
+
190
+ msg = f"Recommendation found by {self.options.optimizer}."
191
+ success = True
192
+
193
+ except Exception as e:
194
+ msg = f"{self.options.optimizer} failed. Possible reason: {e}"
195
+ success = False
196
+
197
+ result = {"recommendation": recommendation, "message": msg, "success": success}
198
+
199
+ return parse_ng_results(result, self.problem, self.evaluator)
@@ -0,0 +1,134 @@
1
+ """This module contains functions for non-dominated sorting of solutions."""
2
+
3
+ import numpy as np
4
+ from numba import njit # type: ignore
5
+
6
+
7
+ @njit()
8
+ def dominates(x: np.ndarray, y: np.ndarray) -> bool:
9
+ """Returns true if x dominates y.
10
+
11
+ Args:
12
+ x (np.ndarray): First solution. Should be a 1-D array of numerics.
13
+ y (np.ndarray): Second solution. Should be the same shape as x.
14
+
15
+ Returns:
16
+ bool: True if x dominates y, false otherwise.
17
+ """
18
+ dom = False
19
+ for i in range(len(x)):
20
+ if x[i] > y[i]:
21
+ return False
22
+ elif x[i] < y[i]:
23
+ dom = True
24
+ return dom
25
+
26
+
27
+ @njit()
28
+ def non_dominated(data: np.ndarray) -> np.ndarray:
29
+ """Finds the non-dominated front from a population of solutions.
30
+
31
+ Args:
32
+ data (np.ndarray): 2-D array of solutions, with each row being a single solution.
33
+
34
+ Returns:
35
+ np.ndarray: Boolean array of same length as number of solutions (rows). The value is
36
+ true if corresponding solution is non-dominated. False otherwise
37
+ """
38
+ num_solutions = len(data)
39
+ index = np.zeros(num_solutions, dtype=np.bool_)
40
+ index[0] = True
41
+ for i in range(1, num_solutions):
42
+ index[i] = True
43
+ for j in range(i):
44
+ if not index[j]:
45
+ continue
46
+ if dominates(data[i], data[j]):
47
+ index[j] = False
48
+ elif dominates(data[j], data[i]):
49
+ index[i] = False
50
+ break
51
+ return index
52
+
53
+
54
+ @njit()
55
+ def fast_non_dominated_sort(data: np.ndarray) -> np.ndarray:
56
+ """Conduct fast non-dominated sorting on a population of solutions.
57
+
58
+ Args:
59
+ data (np.ndarray): 2-D array of solutions, with each row being a single solution.
60
+
61
+ Returns:
62
+ np.ndarray: n x f boolean array. n is the number of solutions, f is the number of fronts.
63
+ The value of an array element is true if the corresponding solution id (column) belongs in
64
+ the corresponding front (row).
65
+ """
66
+ num_solutions = len(data)
67
+ indices = np.arange(num_solutions)
68
+ taken = np.zeros(num_solutions, dtype=np.bool_)
69
+ fronts = np.zeros((num_solutions, num_solutions), dtype=np.bool_)
70
+
71
+ for i in indices:
72
+ current_front = non_dominated(data[~taken])
73
+
74
+ current_front_all = np.zeros(num_solutions, dtype=np.bool_)
75
+ current_front_all[~taken] = current_front
76
+ fronts[i] = current_front_all
77
+
78
+ taken = taken + fronts[i]
79
+ if taken.all():
80
+ # if all the solutions have been sorted, stop
81
+ break
82
+
83
+ return fronts[: i + 1]
84
+
85
+
86
+ def fast_non_dominated_sort_indices(data: np.ndarray) -> list[np.ndarray]:
87
+ """Conduct fast non-dominated sorting on a population of solutions.
88
+
89
+ This function returns identical results as `fast_non_dominated_sort`, but in a different format.
90
+ This function returns an array of solution indices for each front, packed in a list.
91
+
92
+ Args:
93
+ data (np.ndarray): 2-D array of solutions, with each row being a single solution.
94
+
95
+ Returns:
96
+ list[np.ndarray]: A list with f elements where f is the number of fronts in the data,
97
+ arranged in ascending order. Each element is a numpy array of the indices of solutions
98
+ belonging to the corresponding front.
99
+ """
100
+ fronts = fast_non_dominated_sort(data)
101
+ return [np.where(fronts[i])[0] for i in range(len(fronts))]
102
+
103
+
104
+ @njit()
105
+ def non_dominated_merge(set1: np.ndarray, set2: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
106
+ """Merge two sets of non-dominated solutions.
107
+
108
+ This is a slightly more efficient way to merge two sets of solutions such that the resulting
109
+ set only contains non-dominated solutions from the two sets. This function assumes that the
110
+ two sets already only contain non-dominated solutions. I.e., each solution in each set is non-dominated
111
+ with respect to all other solutions in the same set. However, the solutions in the two sets may not be
112
+ non-dominated with respect to each other.
113
+
114
+ Args:
115
+ set1 (np.ndarray): 2-D array of solutions, with each row being a single solution.
116
+ set2 (np.ndarray): 2-D array of solutions, with each row being a single solution.
117
+
118
+ Returns:
119
+ tuple[np.ndarray, np.ndarray]: A tuple of two mask arrays. The first mask array is for set1 and the
120
+ second mask array is for set2. The value of an element in the mask array is True if the corresponding
121
+ solution is non-dominated in the merged set. False otherwise.
122
+ """
123
+ # Masks to keep track of which solutions are non-dominated. Default is all True.
124
+ set1_mask = np.ones(len(set1), dtype=np.bool_)
125
+ set2_mask = np.ones(len(set2), dtype=np.bool_)
126
+
127
+ for i in range(len(set1)):
128
+ for j in range(len(set2)):
129
+ if dominates(set1[i], set2[j]):
130
+ set2_mask[j] = False
131
+ elif dominates(set2[j], set1[i]):
132
+ set1_mask[i] = False
133
+
134
+ return set1_mask, set2_mask