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,114 @@
1
+ """Classes for evaluating the objectives and constraints of the individuals in the population."""
2
+
3
+ import warnings
4
+ from collections.abc import Sequence
5
+
6
+ import polars as pl
7
+
8
+ from desdeo.problem import Problem, SimulatorEvaluator
9
+ from desdeo.tools.message import (
10
+ EvaluatorMessageTopics,
11
+ IntMessage,
12
+ Message,
13
+ PolarsDataFrameMessage,
14
+ )
15
+ from desdeo.tools.patterns import Publisher, Subscriber
16
+
17
+
18
+ class EMOEvaluator(Subscriber):
19
+ """Base class for evaluating the objectives and constraints of the individuals in the population.
20
+
21
+ This class should be inherited by the classes that implement the evaluation of the objectives
22
+ and constraints of the individuals in the population.
23
+
24
+ """
25
+
26
+ @property
27
+ def provided_topics(self) -> dict[int, Sequence[EvaluatorMessageTopics]]:
28
+ """The topics provided by the Evaluator."""
29
+ return {
30
+ 0: [],
31
+ 1: [EvaluatorMessageTopics.NEW_EVALUATIONS],
32
+ 2: [
33
+ EvaluatorMessageTopics.NEW_EVALUATIONS,
34
+ EvaluatorMessageTopics.VERBOSE_OUTPUTS,
35
+ ],
36
+ }
37
+
38
+ @property
39
+ def interested_topics(self):
40
+ """The topics that the Evaluator is interested in."""
41
+ return []
42
+
43
+ def __init__(self, problem: Problem, verbosity: int, publisher: Publisher):
44
+ """Initialize the EMOEvaluator class."""
45
+ super().__init__(
46
+ verbosity=verbosity,
47
+ publisher=publisher,
48
+ )
49
+ self.problem = problem
50
+ # TODO(@light-weaver, @gialmisi): This can be so much more efficient.
51
+ self.evaluator = lambda x: SimulatorEvaluator(problem).evaluate(
52
+ {name.symbol: x[name.symbol].to_list() for name in problem.get_flattened_variables()}, flat=True
53
+ )
54
+ self.variable_symbols = [name.symbol for name in problem.variables]
55
+ self.population: pl.DataFrame
56
+ self.out: pl.DataFrame
57
+ self.new_evals: int = 0
58
+
59
+ def evaluate(self, population: pl.DataFrame) -> pl.DataFrame:
60
+ """Evaluate and return the objectives.
61
+
62
+ Args:
63
+ population (pl.Dataframe): The set of decision variables to evaluate.
64
+
65
+ Returns:
66
+ pl.Dataframe: A dataframe of objective vectors, target vectors, and constraint vectors.
67
+ """
68
+ self.population = population
69
+ out = self.evaluator(population)
70
+ # remove variable_symbols from the output
71
+ self.out = out.drop(self.variable_symbols, strict=False)
72
+ self.new_evals = len(population)
73
+ # merge the objectives and targets
74
+
75
+ self.notify()
76
+ return self.out
77
+
78
+ def state(self) -> Sequence[Message]:
79
+ """The state of the evaluator sent to the Publisher."""
80
+ if self.population is None or self.out is None or self.population is None or self.verbosity == 0:
81
+ return []
82
+ if self.verbosity == 1:
83
+ return [
84
+ IntMessage(
85
+ topic=EvaluatorMessageTopics.NEW_EVALUATIONS,
86
+ value=self.new_evals,
87
+ source=self.__class__.__name__,
88
+ )
89
+ ]
90
+
91
+ if isinstance(self.population, pl.DataFrame):
92
+ message = PolarsDataFrameMessage(
93
+ topic=EvaluatorMessageTopics.VERBOSE_OUTPUTS,
94
+ value=pl.concat([self.population, self.out], how="horizontal"),
95
+ source=self.__class__.__name__,
96
+ )
97
+ else:
98
+ warnings.warn("Population is not a Polars DataFrame. Defaulting to providing OUTPUTS only.", stacklevel=2)
99
+ message = PolarsDataFrameMessage(
100
+ topic=EvaluatorMessageTopics.VERBOSE_OUTPUTS,
101
+ value=self.out,
102
+ source=self.__class__.__name__,
103
+ )
104
+ return [
105
+ IntMessage(
106
+ topic=EvaluatorMessageTopics.NEW_EVALUATIONS,
107
+ value=self.new_evals,
108
+ source=self.__class__.__name__,
109
+ ),
110
+ message,
111
+ ]
112
+
113
+ def update(self, *_, **__):
114
+ """Update the parameters of the evaluator."""
@@ -0,0 +1,459 @@
1
+ """Class for generating initial population for the evolutionary optimization algorithms."""
2
+
3
+ from abc import abstractmethod
4
+ from collections.abc import Sequence
5
+
6
+ import numpy as np
7
+ import polars as pl
8
+ from scipy.stats.qmc import LatinHypercube
9
+
10
+ from desdeo.emo.operators.evaluator import EMOEvaluator
11
+ from desdeo.problem import Problem, VariableTypeEnum
12
+ from desdeo.tools.message import GeneratorMessageTopics, IntMessage, Message, PolarsDataFrameMessage
13
+ from desdeo.tools.patterns import Publisher, Subscriber
14
+
15
+
16
+ class BaseGenerator(Subscriber):
17
+ """Base class for generating initial population for the evolutionary optimization algorithms.
18
+
19
+ This class should be inherited by the classes that implement the initial population generation
20
+ for the evolutionary optimization algorithms.
21
+
22
+ """
23
+
24
+ @property
25
+ def provided_topics(self) -> dict[int, Sequence[GeneratorMessageTopics]]:
26
+ """Return the topics provided by the generator.
27
+
28
+ Returns:
29
+ dict[int, Sequence[GeneratorMessageTopics]]: The topics provided by the generator.
30
+ """
31
+ return {
32
+ 0: [],
33
+ 1: [GeneratorMessageTopics.NEW_EVALUATIONS],
34
+ 2: [
35
+ GeneratorMessageTopics.NEW_EVALUATIONS,
36
+ GeneratorMessageTopics.VERBOSE_OUTPUTS,
37
+ ],
38
+ }
39
+
40
+ @property
41
+ def interested_topics(self):
42
+ """Return the message topics that the generator is interested in."""
43
+ return []
44
+
45
+ def __init__(self, problem: Problem, publisher: Publisher, verbosity: int):
46
+ """Initialize the BaseGenerator class."""
47
+ super().__init__(publisher=publisher, verbosity=verbosity)
48
+ self.problem = problem
49
+ self.variable_symbols = [var.symbol for var in problem.get_flattened_variables()]
50
+ self.bounds = np.array([[var.lowerbound, var.upperbound] for var in problem.get_flattened_variables()])
51
+ self.population: pl.DataFrame = None
52
+ self.out: pl.DataFrame = None
53
+
54
+ @abstractmethod
55
+ def do(self) -> tuple[pl.DataFrame, pl.DataFrame]:
56
+ """Generate the initial population.
57
+
58
+ This method should be implemented by the inherited classes.
59
+
60
+ Returns:
61
+ tuple[pl.DataFrame, pl.DataFrame]: The initial population as the first element,
62
+ the corresponding objectives, the constraint violations, and the targets as the
63
+ second element.
64
+ """
65
+
66
+ def state(self) -> Sequence[Message]:
67
+ """Return the state of the generator.
68
+
69
+ This method should be implemented by the inherited classes.
70
+
71
+ Returns:
72
+ dict: The state of the generator.
73
+ """
74
+ if self.population is None or self.out is None or self.verbosity == 0:
75
+ return []
76
+ if self.verbosity == 1:
77
+ return [
78
+ IntMessage(
79
+ topic=GeneratorMessageTopics.NEW_EVALUATIONS,
80
+ value=self.population.shape[0],
81
+ source=self.__class__.__name__,
82
+ ),
83
+ ]
84
+ # verbosity == 2
85
+ return [
86
+ PolarsDataFrameMessage(
87
+ topic=GeneratorMessageTopics.VERBOSE_OUTPUTS,
88
+ value=pl.concat([self.population, self.out], how="horizontal"),
89
+ source=self.__class__.__name__,
90
+ ),
91
+ IntMessage(
92
+ topic=GeneratorMessageTopics.NEW_EVALUATIONS,
93
+ value=self.population.shape[0],
94
+ source=self.__class__.__name__,
95
+ ),
96
+ ]
97
+
98
+
99
+ class RandomGenerator(BaseGenerator):
100
+ """Class for generating random initial population for the evolutionary optimization algorithms.
101
+
102
+ This class generates the initial population by randomly sampling the points from the variable bounds. The
103
+ distribution of the points is uniform. If the seed is not provided, the seed is set to 0.
104
+ """
105
+
106
+ def __init__(
107
+ self, problem: Problem, evaluator: EMOEvaluator, n_points: int, seed: int, verbosity: int, publisher: Publisher
108
+ ):
109
+ """Initialize the RandomGenerator class.
110
+
111
+ Args:
112
+ problem (Problem): The problem to solve.
113
+ evaluator (BaseEvaluator): The evaluator to evaluate the population.
114
+ n_points (int): The number of points to generate for the initial population.
115
+ seed (int): The seed for the random number generator.
116
+ verbosity (int): The verbosity level of the generator. A verbosity of 2 is needed if you want to maintain
117
+ an external archive. Otherwise, a verbosity of 1 is sufficient.
118
+ publisher (Publisher): The publisher to publish the messages.
119
+ """
120
+ super().__init__(problem, verbosity=verbosity, publisher=publisher)
121
+ self.n_points = n_points
122
+ self.evaluator = evaluator
123
+ self.rng = np.random.default_rng(seed)
124
+ self.seed = seed
125
+
126
+ def do(self) -> tuple[pl.DataFrame, pl.DataFrame]:
127
+ """Generate the initial population.
128
+
129
+ This method should be implemented by the inherited classes.
130
+
131
+ Returns:
132
+ tuple[pl.DataFrame, pl.DataFrame]: The initial population as the first element,
133
+ the corresponding objectives, the constraint violations, and the targets as the second element.
134
+ """
135
+ if self.population is not None and self.out is not None:
136
+ self.notify()
137
+ return self.population, self.out
138
+ self.population = pl.from_numpy(
139
+ self.rng.uniform(low=self.bounds[:, 0], high=self.bounds[:, 1], size=(self.n_points, self.bounds.shape[0])),
140
+ schema=self.variable_symbols,
141
+ )
142
+ self.out = self.evaluator.evaluate(self.population)
143
+ self.notify()
144
+ return self.population, self.out
145
+
146
+ def update(self, message) -> None:
147
+ """Update the generator based on the message."""
148
+
149
+
150
+ class LHSGenerator(BaseGenerator):
151
+ """Class for generating Latin Hypercube Sampling (LHS) initial population for the MOEAs.
152
+
153
+ This class generates the initial population by using the Latin Hypercube Sampling (LHS) method.
154
+ If the seed is not provided, the seed is set to 0.
155
+ """
156
+
157
+ def __init__(
158
+ self, problem: Problem, evaluator: EMOEvaluator, n_points: int, seed: int, verbosity: int, publisher: Publisher
159
+ ):
160
+ """Initialize the LHSGenerator class.
161
+
162
+ Args:
163
+ problem (Problem): The problem to solve.
164
+ evaluator (BaseEvaluator): The evaluator to evaluate the population.
165
+ n_points (int): The number of points to generate for the initial population.
166
+ seed (int): The seed for the random number generator.
167
+ verbosity (int): The verbosity level of the generator. A verbosity of 2 is needed if you want to maintain
168
+ an external archive. Otherwise, a verbosity of 1 is sufficient.
169
+ publisher (Publisher): The publisher to publish the messages.
170
+ """
171
+ super().__init__(problem, verbosity=verbosity, publisher=publisher)
172
+ self.n_points = n_points
173
+ self.evaluator = evaluator
174
+ self.lhsrng = LatinHypercube(d=len(self.variable_symbols), seed=seed)
175
+ self.seed = seed
176
+
177
+ def do(self) -> tuple[pl.DataFrame, pl.DataFrame]:
178
+ """Generate the initial population.
179
+
180
+ This method should be implemented by the inherited classes.
181
+
182
+ Returns:
183
+ tuple[pl.DataFrame, pl.DataFrame]: The initial population as the first element,
184
+ the corresponding objectives, the constraint violations, and the targets as the second element.
185
+ """
186
+ if self.population is not None and self.out is not None:
187
+ self.notify()
188
+ return self.population, self.out
189
+ self.population = pl.from_numpy(
190
+ self.lhsrng.random(n=self.n_points) * (self.bounds[:, 1] - self.bounds[:, 0]) + self.bounds[:, 0],
191
+ schema=self.variable_symbols,
192
+ )
193
+ self.out = self.evaluator.evaluate(self.population)
194
+ self.notify()
195
+ return self.population, self.out
196
+
197
+ def update(self, message) -> None:
198
+ """Update the generator based on the message."""
199
+
200
+
201
+ class RandomBinaryGenerator(BaseGenerator):
202
+ """Class for generating random initial population for problems with binary variables.
203
+
204
+ This class generates an initial population by randomly setting variable values to be either 0 or 1.
205
+ """
206
+
207
+ def __init__(
208
+ self, problem: Problem, evaluator: EMOEvaluator, n_points: int, seed: int, verbosity: int, publisher: Publisher
209
+ ):
210
+ """Initialize the RandomBinaryGenerator class.
211
+
212
+ Args:
213
+ problem (Problem): The problem to solve.
214
+ evaluator (BaseEvaluator): The evaluator to evaluate the population.
215
+ n_points (int): The number of points to generate for the initial population.
216
+ seed (int): The seed for the random number generator.
217
+ verbosity (int): The verbosity level of the generator. A verbosity of 2 is needed if you want to maintain
218
+ an external archive. Otherwise, a verbosity of 1 is sufficient.
219
+ publisher (Publisher): The publisher to publish the messages.
220
+ """
221
+ super().__init__(problem, verbosity=verbosity, publisher=publisher)
222
+ self.n_points = n_points
223
+ self.evaluator = evaluator
224
+ self.rng = np.random.default_rng(seed)
225
+ self.seed = seed
226
+
227
+ def do(self) -> tuple[pl.DataFrame, pl.DataFrame]:
228
+ """Generate the initial population.
229
+
230
+ Returns:
231
+ tuple[pl.DataFrame, pl.DataFrame]: The initial population as the first element,
232
+ the corresponding objectives, the constraint violations, and the targets as the second element.
233
+ """
234
+ if self.population is not None and self.out is not None:
235
+ self.notify()
236
+ return self.population, self.out
237
+
238
+ self.population = pl.from_numpy(
239
+ self.rng.integers(low=0, high=2, size=(self.n_points, self.bounds.shape[0])).astype(dtype=np.float64),
240
+ schema=self.variable_symbols,
241
+ )
242
+
243
+ self.out = self.evaluator.evaluate(self.population)
244
+ self.notify()
245
+ return self.population, self.out
246
+
247
+ def update(self, message) -> None:
248
+ """Update the generator based on the message."""
249
+
250
+
251
+ class RandomIntegerGenerator(BaseGenerator):
252
+ """Class for generating random initial population for problems with integer variables.
253
+
254
+ This class generates an initial population by randomly setting variable values to be integers between the bounds of
255
+ the variables.
256
+ """
257
+
258
+ def __init__(
259
+ self, problem: Problem, evaluator: EMOEvaluator, n_points: int, seed: int, verbosity: int, publisher: Publisher
260
+ ):
261
+ """Initialize the RandomIntegerGenerator class.
262
+
263
+ Args:
264
+ problem (Problem): The problem to solve.
265
+ evaluator (BaseEvaluator): The evaluator to evaluate the population.
266
+ n_points (int): The number of points to generate for the initial population.
267
+ seed (int): The seed for the random number generator.
268
+ verbosity (int): The verbosity level of the generator. A verbosity of 2 is needed if you want to maintain
269
+ an external archive. Otherwise, a verbosity of 1 is sufficient.
270
+ publisher (Publisher): The publisher to publish the messages.
271
+ """
272
+ super().__init__(problem, verbosity=verbosity, publisher=publisher)
273
+ self.n_points = n_points
274
+ self.evaluator = evaluator
275
+ self.rng = np.random.default_rng(seed)
276
+ self.seed = seed
277
+
278
+ def do(self) -> tuple[pl.DataFrame, pl.DataFrame]:
279
+ """Generate the initial population.
280
+
281
+ Returns:
282
+ tuple[pl.DataFrame, pl.DataFrame]: The initial population as the first element,
283
+ the corresponding objectives, the constraint violations, and the targets as the second element.
284
+ """
285
+ if self.population is not None and self.out is not None:
286
+ self.notify()
287
+ return self.population, self.out
288
+
289
+ self.population = pl.from_numpy(
290
+ self.rng.integers(
291
+ low=self.bounds[:, 0],
292
+ high=self.bounds[:, 1],
293
+ size=(self.n_points, self.bounds.shape[0]),
294
+ endpoint=True,
295
+ ).astype(dtype=float),
296
+ schema=self.variable_symbols,
297
+ )
298
+
299
+ self.out = self.evaluator.evaluate(self.population)
300
+ self.notify()
301
+ return self.population, self.out
302
+
303
+ def update(self, message) -> None:
304
+ """Update the generator based on the message."""
305
+
306
+
307
+ class RandomMixedIntegerGenerator(BaseGenerator):
308
+ """Class for generating random initial population for problems with mixed-integer variables.
309
+
310
+ This class generates an initial population by randomly setting variable
311
+ values to be integers or floats between the bounds of the variables.
312
+ """
313
+
314
+ def __init__(
315
+ self, problem: Problem, evaluator: EMOEvaluator, n_points: int, seed: int, verbosity: int, publisher: Publisher
316
+ ):
317
+ """Initialize the RandomMixedIntegerGenerator class.
318
+
319
+ Args:
320
+ problem (Problem): The problem to solve.
321
+ evaluator (BaseEvaluator): The evaluator to evaluate the population.
322
+ n_points (int): The number of points to generate for the initial population.
323
+ seed (int): The seed for the random number generator.
324
+ verbosity (int): The verbosity level of the generator. A verbosity of 2 is needed if you want to maintain
325
+ an external archive. Otherwise, a verbosity of 1 is sufficient.
326
+ publisher (Publisher): The publisher to publish the messages.
327
+ """
328
+ super().__init__(problem, verbosity=verbosity, publisher=publisher)
329
+ self.var_symbol_types = {
330
+ VariableTypeEnum.real: [
331
+ var.symbol for var in problem.variables if var.variable_type == VariableTypeEnum.real
332
+ ],
333
+ VariableTypeEnum.integer: [
334
+ var.symbol
335
+ for var in problem.variables
336
+ if var.variable_type in [VariableTypeEnum.integer, VariableTypeEnum.binary]
337
+ ],
338
+ }
339
+ self.n_points = n_points
340
+ self.evaluator = evaluator
341
+ self.rng = np.random.default_rng(seed)
342
+ self.seed = seed
343
+
344
+ def do(self) -> tuple[pl.DataFrame, pl.DataFrame]:
345
+ """Generate the initial population.
346
+
347
+ Returns:
348
+ tuple[pl.DataFrame, pl.DataFrame]: The initial population as the first element,
349
+ the corresponding objectives, the constraint violations, and the targets as the second element.
350
+ """
351
+ if self.population is not None and self.out is not None:
352
+ self.notify()
353
+ return self.population, self.out
354
+
355
+ tmp = {
356
+ var.symbol: self.rng.integers(
357
+ low=var.lowerbound, high=var.upperbound, size=self.n_points, endpoint=True
358
+ ).astype(dtype=float)
359
+ if var.variable_type in [VariableTypeEnum.binary, VariableTypeEnum.integer]
360
+ else self.rng.uniform(low=var.lowerbound, high=var.upperbound, size=self.n_points).astype(dtype=float)
361
+ for var in self.problem.variables
362
+ }
363
+
364
+ # combine
365
+ # self.population
366
+ self.population = pl.DataFrame(tmp)
367
+
368
+ self.out = self.evaluator.evaluate(self.population)
369
+ self.notify()
370
+ return self.population, self.out
371
+
372
+ def update(self, message) -> None:
373
+ """Update the generator based on the message."""
374
+
375
+
376
+ class ArchiveGenerator(BaseGenerator):
377
+ """Class for getting initial population from an archive."""
378
+
379
+ def __init__(
380
+ self,
381
+ problem: Problem,
382
+ evaluator: EMOEvaluator,
383
+ publisher: Publisher,
384
+ verbosity: int,
385
+ solutions: pl.DataFrame,
386
+ **kwargs, # just to dump seed
387
+ ):
388
+ """Initialize the ArchiveGenerator class.
389
+
390
+ Args:
391
+ problem (Problem): The problem to solve.
392
+ evaluator (BaseEvaluator): The evaluator to evaluate the population. Only used to check that the outputs
393
+ have the correct variables.
394
+ publisher (Publisher): The publisher to publish the messages.
395
+ verbosity (int): The verbosity level of the generator. A verbosity of 2 is needed if you want to maintain
396
+ an external archive. Otherwise, a verbosity of 1 is sufficient.
397
+ solutions (pl.DataFrame): The decision variable vectors to use as the initial population.
398
+ """
399
+ super().__init__(problem, verbosity=verbosity, publisher=publisher)
400
+ if not isinstance(solutions, pl.DataFrame):
401
+ raise ValueError("The solutions must be a polars DataFrame.")
402
+ if solutions.shape[0] == 0:
403
+ raise ValueError("The solutions DataFrame is empty.")
404
+ self.solutions = solutions
405
+ # self.outputs = outputs
406
+ if not set(self.solutions.columns) == set(self.variable_symbols):
407
+ raise ValueError("The solutions DataFrame must have the same columns as the problem variables.")
408
+ # TODO: Check that the outputs have the correct columns
409
+ self.evaluator = evaluator
410
+
411
+ def do(self) -> tuple[pl.DataFrame, pl.DataFrame]:
412
+ """Get the initial population from the archive.
413
+
414
+ Returns:
415
+ tuple[pl.DataFrame, pl.DataFrame]: The initial population as the first element,
416
+ the corresponding objectives, the constraint violations, and the targets as the second element.
417
+ """
418
+ self.outputs = self.evaluator.evaluate(self.solutions)
419
+ self.notify()
420
+ return self.solutions, self.outputs
421
+
422
+ def state(self) -> Sequence[Message]:
423
+ """Return the state of the generator.
424
+
425
+ This method overrides the state method of the BaseGenerator class, because the solutions and outputs are
426
+ already provided and not generated by the generator.
427
+
428
+ Returns:
429
+ dict: The state of the generator.
430
+ """
431
+ # TODO: Should we do it like this? Or just do super().state()?
432
+ # Maybe saying that zero evaluations have been done is misleading?
433
+ # idk
434
+ if self.verbosity == 0:
435
+ return []
436
+ if self.verbosity == 1:
437
+ return [
438
+ IntMessage(
439
+ topic=GeneratorMessageTopics.NEW_EVALUATIONS,
440
+ value=0,
441
+ source=self.__class__.__name__,
442
+ ),
443
+ ]
444
+ # verbosity == 2
445
+ return [
446
+ PolarsDataFrameMessage(
447
+ topic=GeneratorMessageTopics.VERBOSE_OUTPUTS,
448
+ value=pl.concat([self.solutions, self.outputs], how="horizontal"),
449
+ source=self.__class__.__name__,
450
+ ),
451
+ IntMessage(
452
+ topic=GeneratorMessageTopics.NEW_EVALUATIONS,
453
+ value=0,
454
+ source=self.__class__.__name__,
455
+ ),
456
+ ]
457
+
458
+ def update(self, message) -> None:
459
+ """Update the generator based on the message."""