desdeo 1.2__py3-none-any.whl → 2.0.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 (122) hide show
  1. desdeo/__init__.py +8 -8
  2. desdeo/api/README.md +73 -0
  3. desdeo/api/__init__.py +15 -0
  4. desdeo/api/app.py +40 -0
  5. desdeo/api/config.py +69 -0
  6. desdeo/api/config.toml +53 -0
  7. desdeo/api/db.py +25 -0
  8. desdeo/api/db_init.py +79 -0
  9. desdeo/api/db_models.py +164 -0
  10. desdeo/api/malaga_db_init.py +27 -0
  11. desdeo/api/models/__init__.py +66 -0
  12. desdeo/api/models/archive.py +34 -0
  13. desdeo/api/models/preference.py +90 -0
  14. desdeo/api/models/problem.py +507 -0
  15. desdeo/api/models/reference_point_method.py +18 -0
  16. desdeo/api/models/session.py +46 -0
  17. desdeo/api/models/state.py +96 -0
  18. desdeo/api/models/user.py +51 -0
  19. desdeo/api/routers/_NAUTILUS.py +245 -0
  20. desdeo/api/routers/_NAUTILUS_navigator.py +233 -0
  21. desdeo/api/routers/_NIMBUS.py +762 -0
  22. desdeo/api/routers/__init__.py +5 -0
  23. desdeo/api/routers/problem.py +110 -0
  24. desdeo/api/routers/reference_point_method.py +117 -0
  25. desdeo/api/routers/session.py +76 -0
  26. desdeo/api/routers/test.py +16 -0
  27. desdeo/api/routers/user_authentication.py +366 -0
  28. desdeo/api/schema.py +94 -0
  29. desdeo/api/tests/__init__.py +0 -0
  30. desdeo/api/tests/conftest.py +59 -0
  31. desdeo/api/tests/test_models.py +701 -0
  32. desdeo/api/tests/test_routes.py +216 -0
  33. desdeo/api/utils/database.py +274 -0
  34. desdeo/api/utils/logger.py +29 -0
  35. desdeo/core.py +27 -0
  36. desdeo/emo/__init__.py +29 -0
  37. desdeo/emo/hooks/archivers.py +172 -0
  38. desdeo/emo/methods/EAs.py +418 -0
  39. desdeo/emo/methods/__init__.py +0 -0
  40. desdeo/emo/methods/bases.py +59 -0
  41. desdeo/emo/operators/__init__.py +1 -0
  42. desdeo/emo/operators/crossover.py +780 -0
  43. desdeo/emo/operators/evaluator.py +118 -0
  44. desdeo/emo/operators/generator.py +356 -0
  45. desdeo/emo/operators/mutation.py +1053 -0
  46. desdeo/emo/operators/selection.py +1036 -0
  47. desdeo/emo/operators/termination.py +178 -0
  48. desdeo/explanations/__init__.py +6 -0
  49. desdeo/explanations/explainer.py +100 -0
  50. desdeo/explanations/utils.py +90 -0
  51. desdeo/mcdm/__init__.py +19 -0
  52. desdeo/mcdm/nautili.py +345 -0
  53. desdeo/mcdm/nautilus.py +477 -0
  54. desdeo/mcdm/nautilus_navigator.py +655 -0
  55. desdeo/mcdm/nimbus.py +417 -0
  56. desdeo/mcdm/pareto_navigator.py +269 -0
  57. desdeo/mcdm/reference_point_method.py +116 -0
  58. desdeo/problem/__init__.py +79 -0
  59. desdeo/problem/evaluator.py +561 -0
  60. desdeo/problem/gurobipy_evaluator.py +562 -0
  61. desdeo/problem/infix_parser.py +341 -0
  62. desdeo/problem/json_parser.py +944 -0
  63. desdeo/problem/pyomo_evaluator.py +468 -0
  64. desdeo/problem/schema.py +1808 -0
  65. desdeo/problem/simulator_evaluator.py +298 -0
  66. desdeo/problem/sympy_evaluator.py +244 -0
  67. desdeo/problem/testproblems/__init__.py +73 -0
  68. desdeo/problem/testproblems/binh_and_korn_problem.py +88 -0
  69. desdeo/problem/testproblems/dtlz2_problem.py +102 -0
  70. desdeo/problem/testproblems/forest_problem.py +275 -0
  71. desdeo/problem/testproblems/knapsack_problem.py +163 -0
  72. desdeo/problem/testproblems/mcwb_problem.py +831 -0
  73. desdeo/problem/testproblems/mixed_variable_dimenrions_problem.py +83 -0
  74. desdeo/problem/testproblems/momip_problem.py +172 -0
  75. desdeo/problem/testproblems/nimbus_problem.py +143 -0
  76. desdeo/problem/testproblems/pareto_navigator_problem.py +89 -0
  77. desdeo/problem/testproblems/re_problem.py +492 -0
  78. desdeo/problem/testproblems/river_pollution_problem.py +434 -0
  79. desdeo/problem/testproblems/rocket_injector_design_problem.py +140 -0
  80. desdeo/problem/testproblems/simple_problem.py +351 -0
  81. desdeo/problem/testproblems/simulator_problem.py +92 -0
  82. desdeo/problem/testproblems/spanish_sustainability_problem.py +945 -0
  83. desdeo/problem/testproblems/zdt_problem.py +271 -0
  84. desdeo/problem/utils.py +245 -0
  85. desdeo/tools/GenerateReferencePoints.py +181 -0
  86. desdeo/tools/__init__.py +102 -0
  87. desdeo/tools/generics.py +145 -0
  88. desdeo/tools/gurobipy_solver_interfaces.py +258 -0
  89. desdeo/tools/indicators_binary.py +11 -0
  90. desdeo/tools/indicators_unary.py +375 -0
  91. desdeo/tools/interaction_schema.py +38 -0
  92. desdeo/tools/intersection.py +54 -0
  93. desdeo/tools/iterative_pareto_representer.py +99 -0
  94. desdeo/tools/message.py +234 -0
  95. desdeo/tools/ng_solver_interfaces.py +199 -0
  96. desdeo/tools/non_dominated_sorting.py +133 -0
  97. desdeo/tools/patterns.py +281 -0
  98. desdeo/tools/proximal_solver.py +99 -0
  99. desdeo/tools/pyomo_solver_interfaces.py +464 -0
  100. desdeo/tools/reference_vectors.py +462 -0
  101. desdeo/tools/scalarization.py +3138 -0
  102. desdeo/tools/scipy_solver_interfaces.py +454 -0
  103. desdeo/tools/score_bands.py +464 -0
  104. desdeo/tools/utils.py +320 -0
  105. desdeo/utopia_stuff/__init__.py +0 -0
  106. desdeo/utopia_stuff/data/1.json +15 -0
  107. desdeo/utopia_stuff/data/2.json +13 -0
  108. desdeo/utopia_stuff/data/3.json +15 -0
  109. desdeo/utopia_stuff/data/4.json +17 -0
  110. desdeo/utopia_stuff/data/5.json +15 -0
  111. desdeo/utopia_stuff/from_json.py +40 -0
  112. desdeo/utopia_stuff/reinit_user.py +38 -0
  113. desdeo/utopia_stuff/utopia_db_init.py +212 -0
  114. desdeo/utopia_stuff/utopia_problem.py +403 -0
  115. desdeo/utopia_stuff/utopia_problem_old.py +415 -0
  116. desdeo/utopia_stuff/utopia_reference_solutions.py +79 -0
  117. desdeo-2.0.0.dist-info/LICENSE +21 -0
  118. desdeo-2.0.0.dist-info/METADATA +168 -0
  119. desdeo-2.0.0.dist-info/RECORD +120 -0
  120. {desdeo-1.2.dist-info → desdeo-2.0.0.dist-info}/WHEEL +1 -1
  121. desdeo-1.2.dist-info/METADATA +0 -16
  122. desdeo-1.2.dist-info/RECORD +0 -4
@@ -0,0 +1,1053 @@
1
+ """Evolutionary operators for mutation.
2
+
3
+ Various evolutionary operators for mutation in multiobjective optimization are defined here.
4
+ """
5
+
6
+ import copy
7
+ from abc import abstractmethod
8
+ from collections.abc import Sequence
9
+
10
+ import numpy as np
11
+ import polars as pl
12
+
13
+ from desdeo.problem import Problem, VariableDomainTypeEnum, VariableTypeEnum
14
+ from desdeo.tools.message import (
15
+ FloatMessage,
16
+ Message,
17
+ MutationMessageTopics,
18
+ PolarsDataFrameMessage,
19
+ )
20
+ from desdeo.tools.patterns import Subscriber
21
+
22
+
23
+ class BaseMutation(Subscriber):
24
+ """A base class for mutation operators."""
25
+
26
+ @abstractmethod
27
+ def __init__(self, problem: Problem, **kwargs):
28
+ """Initialize a mu operator."""
29
+ super().__init__(**kwargs)
30
+ self.problem = problem
31
+ self.variable_symbols = [var.symbol for var in problem.get_flattened_variables()]
32
+ self.lower_bounds = [var.lowerbound for var in problem.get_flattened_variables()]
33
+ self.upper_bounds = [var.upperbound for var in problem.get_flattened_variables()]
34
+ self.variable_types = [var.variable_type for var in problem.get_flattened_variables()]
35
+ self.variable_combination: VariableDomainTypeEnum = problem.variable_domain
36
+
37
+ @abstractmethod
38
+ def do(self, offsprings: pl.DataFrame, parents: pl.DataFrame) -> pl.DataFrame:
39
+ """Perform the mutation operation.
40
+
41
+ Args:
42
+ offsprings (pl.DataFrame): the offspring population to mutate.
43
+ parents (pl.DataFrame): the parent population from which the offspring
44
+ was generated (via crossover).
45
+
46
+ Returns:
47
+ pl.DataFrame: the offspring resulting from the mutation.
48
+ """
49
+
50
+
51
+ class BoundedPolynomialMutation(BaseMutation):
52
+ """Implements the bounded polynomial mutation operator.
53
+
54
+ Reference:
55
+ Deb, K., & Goyal, M. (1996). A combined genetic adaptive search (GeneAS) for
56
+ engineering design. Computer Science and informatics, 26(4), 30-45, 1996.
57
+ """
58
+
59
+ @property
60
+ def provided_topics(self) -> dict[int, Sequence[MutationMessageTopics]]:
61
+ """The message topics provided by the mutation operator."""
62
+ return {
63
+ 0: [],
64
+ 1: [
65
+ MutationMessageTopics.MUTATION_PROBABILITY,
66
+ MutationMessageTopics.MUTATION_DISTRIBUTION,
67
+ ],
68
+ 2: [
69
+ MutationMessageTopics.MUTATION_PROBABILITY,
70
+ MutationMessageTopics.MUTATION_DISTRIBUTION,
71
+ MutationMessageTopics.OFFSPRING_ORIGINAL,
72
+ MutationMessageTopics.OFFSPRINGS,
73
+ ],
74
+ }
75
+
76
+ @property
77
+ def interested_topics(self):
78
+ """The message topics that the mutation operator is interested in."""
79
+ return []
80
+
81
+ def __init__(
82
+ self,
83
+ *,
84
+ problem: Problem,
85
+ seed: int,
86
+ mutation_probability: float | None = None,
87
+ distribution_index: float = 20,
88
+ **kwargs,
89
+ ):
90
+ """Initialize a bounded polynomial mutation operator.
91
+
92
+ Args:
93
+ problem (Problem): The problem object.
94
+ seed (int): The seed for the random number generator.
95
+ mutation_probability (float | None, optional): The probability of mutation. Defaults to None.
96
+ distribution_index (float, optional): The distributaion index for polynomial mutation. Defaults to 20.
97
+ kwargs: Additional keyword arguments. These are passed to the Subscriber class. At the very least, the
98
+ publisher must be passed. See the Subscriber class for more information.
99
+ """
100
+ super().__init__(problem, **kwargs)
101
+ if self.variable_combination != VariableDomainTypeEnum.continuous:
102
+ raise ValueError("This mutation operator only works with continuous variables.")
103
+ if mutation_probability is None:
104
+ self.mutation_probability = 1 / len(self.lower_bounds)
105
+ else:
106
+ self.mutation_probability = mutation_probability
107
+ self.distribution_index = distribution_index
108
+ self.rng = np.random.default_rng(seed)
109
+ self.seed = seed
110
+ self.offspring_original: pl.DataFrame
111
+ self.parents: pl.DataFrame
112
+ self.offspring: pl.DataFrame
113
+
114
+ def do(self, offsprings: pl.DataFrame, parents: pl.DataFrame) -> pl.DataFrame:
115
+ """Perform the mutation operation.
116
+
117
+ Args:
118
+ offsprings (pl.DataFrame): the offspring population to mutate.
119
+ parents (pl.DataFrame): the parent population from which the offspring
120
+ was generated (via crossover).
121
+
122
+ Returns:
123
+ pl.DataFrame: the offspring resulting from the mutation.
124
+ """
125
+ # TODO(@light-weaver): Extract to a numba jitted function
126
+ self.offspring_original = offsprings
127
+ self.parents = parents # Not used, but kept for consistency
128
+ offspring = offsprings.to_numpy()
129
+ min_val = np.ones_like(offspring) * self.lower_bounds
130
+ max_val = np.ones_like(offspring) * self.upper_bounds
131
+ k = self.rng.random(size=offspring.shape)
132
+ miu = self.rng.random(size=offspring.shape)
133
+ temp = np.logical_and((k <= self.mutation_probability), (miu < 0.5))
134
+ offspring_scaled = (offspring - min_val) / (max_val - min_val)
135
+ offspring[temp] = offspring[temp] + (
136
+ (max_val[temp] - min_val[temp])
137
+ * (
138
+ (2 * miu[temp] + (1 - 2 * miu[temp]) * (1 - offspring_scaled[temp]) ** (self.distribution_index + 1))
139
+ ** (1 / (self.distribution_index + 1))
140
+ - 1
141
+ )
142
+ )
143
+ temp = np.logical_and((k <= self.mutation_probability), (miu >= 0.5))
144
+ offspring[temp] = offspring[temp] + (
145
+ (max_val[temp] - min_val[temp])
146
+ * (
147
+ 1
148
+ - (
149
+ 2 * (1 - miu[temp])
150
+ + 2 * (miu[temp] - 0.5) * offspring_scaled[temp] ** (self.distribution_index + 1)
151
+ )
152
+ ** (1 / (self.distribution_index + 1))
153
+ )
154
+ )
155
+ offspring[offspring > max_val] = max_val[offspring > max_val]
156
+ offspring[offspring < min_val] = min_val[offspring < min_val]
157
+ self.offspring = pl.from_numpy(offspring, schema=self.variable_symbols)
158
+ self.notify()
159
+ return self.offspring
160
+
161
+ def update(self, *_, **__):
162
+ """Do nothing. This is just the basic polynomial mutation operator."""
163
+
164
+ def state(self) -> Sequence[Message]:
165
+ """Return the state of the mutation operator."""
166
+ if self.offspring_original is None or self.offspring is None:
167
+ return []
168
+ if self.verbosity == 0:
169
+ return []
170
+ if self.verbosity == 1:
171
+ return [
172
+ FloatMessage(
173
+ topic=MutationMessageTopics.MUTATION_PROBABILITY,
174
+ source=self.__class__.__name__,
175
+ value=self.mutation_probability,
176
+ ),
177
+ FloatMessage(
178
+ topic=MutationMessageTopics.MUTATION_DISTRIBUTION,
179
+ source=self.__class__.__name__,
180
+ value=self.distribution_index,
181
+ ),
182
+ ]
183
+ # verbosity == 2
184
+ return [
185
+ PolarsDataFrameMessage(
186
+ topic=MutationMessageTopics.OFFSPRING_ORIGINAL,
187
+ source=self.__class__.__name__,
188
+ value=self.offspring_original,
189
+ ),
190
+ PolarsDataFrameMessage(
191
+ topic=MutationMessageTopics.PARENTS,
192
+ source=self.__class__.__name__,
193
+ value=self.parents,
194
+ ),
195
+ PolarsDataFrameMessage(
196
+ topic=MutationMessageTopics.OFFSPRINGS,
197
+ source=self.__class__.__name__,
198
+ value=self.offspring,
199
+ ),
200
+ FloatMessage(
201
+ topic=MutationMessageTopics.MUTATION_PROBABILITY,
202
+ source=self.__class__.__name__,
203
+ value=self.mutation_probability,
204
+ ),
205
+ FloatMessage(
206
+ topic=MutationMessageTopics.MUTATION_DISTRIBUTION,
207
+ source=self.__class__.__name__,
208
+ value=self.distribution_index,
209
+ ),
210
+ ]
211
+
212
+
213
+ class BinaryFlipMutation(BaseMutation):
214
+ """Implements the bit flip mutation operator for binary variables.
215
+
216
+ The binary flip mutation will mutate each binary decision variable,
217
+ by flipping it (0 to 1, 1 to 0) with a provided probability.
218
+ """
219
+
220
+ @property
221
+ def provided_topics(self) -> dict[int, Sequence[MutationMessageTopics]]:
222
+ """The message topics provided by the mutation operator."""
223
+ return {
224
+ 0: [],
225
+ 1: [
226
+ MutationMessageTopics.MUTATION_PROBABILITY,
227
+ ],
228
+ 2: [
229
+ MutationMessageTopics.MUTATION_PROBABILITY,
230
+ MutationMessageTopics.OFFSPRING_ORIGINAL,
231
+ MutationMessageTopics.OFFSPRINGS,
232
+ ],
233
+ }
234
+
235
+ @property
236
+ def interested_topics(self):
237
+ """The message topics that the mutation operator is interested in."""
238
+ return []
239
+
240
+ def __init__(
241
+ self,
242
+ *,
243
+ problem: Problem,
244
+ seed: int,
245
+ mutation_probability: float | None = None,
246
+ **kwargs,
247
+ ):
248
+ """Initialize a binary flip mutation operator.
249
+
250
+ Args:
251
+ problem (Problem): The problem object.
252
+ seed (int): The seed for the random number generator.
253
+ mutation_probability (float | None, optional): The probability of mutation. If None,
254
+ the probability will be set to be 1/n, where n is the number of decision variables
255
+ in the problem. Defaults to None.
256
+ kwargs: Additional keyword arguments. These are passed to the Subscriber class. At the very least, the
257
+ publisher must be passed. See the Subscriber class for more information.
258
+ """
259
+ super().__init__(problem, **kwargs)
260
+
261
+ if self.variable_combination != VariableDomainTypeEnum.binary:
262
+ raise ValueError("This mutation operator only works with binary variables.")
263
+ if mutation_probability is None:
264
+ self.mutation_probability = 1 / len(self.variable_symbols)
265
+ else:
266
+ self.mutation_probability = mutation_probability
267
+
268
+ self.rng = np.random.default_rng(seed)
269
+ self.seed = seed
270
+ self.offspring_original: pl.DataFrame
271
+ self.parents: pl.DataFrame
272
+ self.offspring: pl.DataFrame
273
+
274
+ def do(self, offsprings: pl.DataFrame, parents: pl.DataFrame) -> pl.DataFrame:
275
+ """Perform the binary flip mutation operation.
276
+
277
+ Args:
278
+ offsprings (pl.DataFrame): the offspring population to mutate.
279
+ parents (pl.DataFrame): the parent population from which the offspring
280
+ was generated (via crossover). Not used in the mutation operator.
281
+
282
+ Returns:
283
+ pl.DataFrame: the offspring resulting from the mutation.
284
+ """
285
+ self.offspring_original = copy.copy(offsprings)
286
+ self.parents = parents # Not used, but kept for consistency
287
+ offspring = offsprings.to_numpy().astype(dtype=np.bool)
288
+
289
+ # create a boolean mask based on the mutation probability
290
+ flip_mask = self.rng.random(offspring.shape) < self.mutation_probability
291
+
292
+ # using XOR (^), flip the bits in the offspring when the mask is True
293
+ # otherwise leave the bit's value as it is
294
+ offspring = offspring ^ flip_mask
295
+
296
+ self.offspring = pl.from_numpy(offspring, schema=self.variable_symbols).select(pl.all()).cast(pl.Float64)
297
+ self.notify()
298
+
299
+ return self.offspring
300
+
301
+ def update(self, *_, **__):
302
+ """Do nothing."""
303
+
304
+ def state(self) -> Sequence[Message]:
305
+ """Return the state of the mutation operator."""
306
+ if self.offspring_original is None or self.offspring is None:
307
+ return []
308
+ if self.verbosity == 0:
309
+ return []
310
+ if self.verbosity == 1:
311
+ return [
312
+ FloatMessage(
313
+ topic=MutationMessageTopics.MUTATION_PROBABILITY,
314
+ source=self.__class__.__name__,
315
+ value=self.mutation_probability,
316
+ ),
317
+ ]
318
+ # verbosity == 2
319
+ return [
320
+ PolarsDataFrameMessage(
321
+ topic=MutationMessageTopics.OFFSPRING_ORIGINAL,
322
+ source=self.__class__.__name__,
323
+ value=self.offspring_original,
324
+ ),
325
+ PolarsDataFrameMessage(
326
+ topic=MutationMessageTopics.PARENTS,
327
+ source=self.__class__.__name__,
328
+ value=self.parents,
329
+ ),
330
+ PolarsDataFrameMessage(
331
+ topic=MutationMessageTopics.OFFSPRINGS,
332
+ source=self.__class__.__name__,
333
+ value=self.offspring,
334
+ ),
335
+ FloatMessage(
336
+ topic=MutationMessageTopics.MUTATION_PROBABILITY,
337
+ source=self.__class__.__name__,
338
+ value=self.mutation_probability,
339
+ ),
340
+ ]
341
+
342
+
343
+ class IntegerRandomMutation(BaseMutation):
344
+ """Implements a random mutation operator for integer variables.
345
+
346
+ The mutation will mutate each binary integer variable,
347
+ by changing its value to a random value bounded by the
348
+ variable's bounds with a provided probability.
349
+ """
350
+
351
+ @property
352
+ def provided_topics(self) -> dict[int, Sequence[MutationMessageTopics]]:
353
+ """The message topics provided by the mutation operator."""
354
+ return {
355
+ 0: [],
356
+ 1: [
357
+ MutationMessageTopics.MUTATION_PROBABILITY,
358
+ ],
359
+ 2: [
360
+ MutationMessageTopics.MUTATION_PROBABILITY,
361
+ MutationMessageTopics.OFFSPRING_ORIGINAL,
362
+ MutationMessageTopics.OFFSPRINGS,
363
+ ],
364
+ }
365
+
366
+ @property
367
+ def interested_topics(self):
368
+ """The message topics that the mutation operator is interested in."""
369
+ return []
370
+
371
+ def __init__(
372
+ self,
373
+ *,
374
+ problem: Problem,
375
+ seed: int,
376
+ mutation_probability: float | None = None,
377
+ **kwargs,
378
+ ):
379
+ """Initialize a random integer mutation operator.
380
+
381
+ Args:
382
+ problem (Problem): The problem object.
383
+ seed (int): The seed for the random number generator.
384
+ mutation_probability (float | None, optional): The probability of mutation. If None,
385
+ the probability will be set to be 1/n, where n is the number of decision variables
386
+ in the problem. Defaults to None.
387
+ kwargs: Additional keyword arguments. These are passed to the Subscriber class. At the very least, the
388
+ publisher must be passed. See the Subscriber class for more information.
389
+ """
390
+ super().__init__(problem, **kwargs)
391
+
392
+ if self.variable_combination != VariableDomainTypeEnum.integer:
393
+ raise ValueError("This mutation operator only works with integer variables.")
394
+ if mutation_probability is None:
395
+ self.mutation_probability = 1 / len(self.variable_symbols)
396
+ else:
397
+ self.mutation_probability = mutation_probability
398
+
399
+ self.rng = np.random.default_rng(seed)
400
+ self.seed = seed
401
+ self.offspring_original: pl.DataFrame
402
+ self.parents: pl.DataFrame
403
+ self.offspring: pl.DataFrame
404
+
405
+ def do(self, offsprings: pl.DataFrame, parents: pl.DataFrame) -> pl.DataFrame:
406
+ """Perform the random integer mutation operation.
407
+
408
+ Args:
409
+ offsprings (pl.DataFrame): the offspring population to mutate.
410
+ parents (pl.DataFrame): the parent population from which the offspring
411
+ was generated (via crossover). Not used in the mutation operator.
412
+
413
+ Returns:
414
+ pl.DataFrame: the offspring resulting from the mutation.
415
+ """
416
+ self.offspring_original = copy.copy(offsprings)
417
+ self.parents = parents # Not used, but kept for consistency
418
+
419
+ population = offsprings.to_numpy().astype(int)
420
+
421
+ # create a boolean mask based on the mutation probability
422
+ mutation_mask = self.rng.random(population.shape) < self.mutation_probability
423
+
424
+ mutated = np.where(
425
+ mutation_mask,
426
+ self.rng.integers(self.lower_bounds, self.upper_bounds, size=population.shape, dtype=int, endpoint=True),
427
+ population,
428
+ )
429
+
430
+ self.offspring = pl.from_numpy(mutated, schema=self.variable_symbols).select(pl.all()).cast(pl.Float64)
431
+ self.notify()
432
+
433
+ return self.offspring
434
+
435
+ def update(self, *_, **__):
436
+ """Do nothing."""
437
+
438
+ def state(self) -> Sequence[Message]:
439
+ """Return the state of the mutation operator."""
440
+ if self.offspring_original is None or self.offspring is None:
441
+ return []
442
+ if self.verbosity == 0:
443
+ return []
444
+ if self.verbosity == 1:
445
+ return [
446
+ FloatMessage(
447
+ topic=MutationMessageTopics.MUTATION_PROBABILITY,
448
+ source=self.__class__.__name__,
449
+ value=self.mutation_probability,
450
+ ),
451
+ ]
452
+ # verbosity == 2
453
+ return [
454
+ PolarsDataFrameMessage(
455
+ topic=MutationMessageTopics.OFFSPRING_ORIGINAL,
456
+ source=self.__class__.__name__,
457
+ value=self.offspring_original,
458
+ ),
459
+ PolarsDataFrameMessage(
460
+ topic=MutationMessageTopics.PARENTS,
461
+ source=self.__class__.__name__,
462
+ value=self.parents,
463
+ ),
464
+ PolarsDataFrameMessage(
465
+ topic=MutationMessageTopics.OFFSPRINGS,
466
+ source=self.__class__.__name__,
467
+ value=self.offspring,
468
+ ),
469
+ FloatMessage(
470
+ topic=MutationMessageTopics.MUTATION_PROBABILITY,
471
+ source=self.__class__.__name__,
472
+ value=self.mutation_probability,
473
+ ),
474
+ ]
475
+
476
+
477
+ class MixedIntegerRandomMutation(BaseMutation):
478
+ """Implements a random mutation operator for mixed-integer variables.
479
+
480
+ The mutation will mutate each mixed-integer variable,
481
+ by changing its value to a random value bounded by the
482
+ variable's bounds with a provided probability.
483
+ """
484
+
485
+ @property
486
+ def provided_topics(self) -> dict[int, Sequence[MutationMessageTopics]]:
487
+ """The message topics provided by the mutation operator."""
488
+ return {
489
+ 0: [],
490
+ 1: [
491
+ MutationMessageTopics.MUTATION_PROBABILITY,
492
+ ],
493
+ 2: [
494
+ MutationMessageTopics.MUTATION_PROBABILITY,
495
+ MutationMessageTopics.OFFSPRING_ORIGINAL,
496
+ MutationMessageTopics.OFFSPRINGS,
497
+ ],
498
+ }
499
+
500
+ @property
501
+ def interested_topics(self):
502
+ """The message topics that the mutation operator is interested in."""
503
+ return []
504
+
505
+ def __init__(
506
+ self,
507
+ *,
508
+ problem: Problem,
509
+ seed: int,
510
+ mutation_probability: float | None = None,
511
+ **kwargs,
512
+ ):
513
+ """Initialize a random mixed_integer mutation operator.
514
+
515
+ Args:
516
+ problem (Problem): The problem object.
517
+ seed (int): The seed for the random number generator.
518
+ mutation_probability (float | None, optional): The probability of mutation. If None,
519
+ the probability will be set to be 1/n, where n is the number of decision variables
520
+ in the problem. Defaults to None.
521
+ kwargs: Additional keyword arguments. These are passed to the Subscriber class. At the very least, the
522
+ publisher must be passed. See the Subscriber class for more information.
523
+ """
524
+ super().__init__(problem, **kwargs)
525
+
526
+ if mutation_probability is None:
527
+ self.mutation_probability = 1 / len(self.variable_symbols)
528
+ else:
529
+ self.mutation_probability = mutation_probability
530
+
531
+ self.rng = np.random.default_rng(seed)
532
+ self.seed = seed
533
+ self.offspring_original: pl.DataFrame
534
+ self.parents: pl.DataFrame
535
+ self.offspring: pl.DataFrame
536
+
537
+ def do(self, offsprings: pl.DataFrame, parents: pl.DataFrame) -> pl.DataFrame:
538
+ """Perform the random integer mutation operation.
539
+
540
+ Args:
541
+ offsprings (pl.DataFrame): the offspring population to mutate.
542
+ parents (pl.DataFrame): the parent population from which the offspring
543
+ was generated (via crossover). Not used in the mutation operator.
544
+
545
+ Returns:
546
+ pl.DataFrame: the offspring resulting from the mutation.
547
+ """
548
+ self.offspring_original = copy.copy(offsprings)
549
+ self.parents = parents # Not used, but kept for consistency
550
+
551
+ population = offsprings.to_numpy().astype(float)
552
+
553
+ # create a boolean mask based on the mutation probability
554
+ mutation_mask = self.rng.random(population.shape) < self.mutation_probability
555
+
556
+ mutation_pool = np.array(
557
+ [
558
+ self.rng.integers(
559
+ low=var.lowerbound, high=var.upperbound, size=population.shape[0], endpoint=True
560
+ ).astype(dtype=float)
561
+ if var.variable_type in [VariableTypeEnum.binary, VariableTypeEnum.integer]
562
+ else self.rng.uniform(low=var.lowerbound, high=var.upperbound, size=population.shape[0]).astype(
563
+ dtype=float
564
+ )
565
+ for var in self.problem.variables
566
+ ]
567
+ ).T
568
+
569
+ mutated = np.where(
570
+ mutation_mask,
571
+ # self.rng.integers(self.lower_bounds, self.upper_bounds, size=population.shape, dtype=int, endpoint=True),
572
+ mutation_pool,
573
+ population,
574
+ )
575
+
576
+ self.offspring = pl.from_numpy(mutated, schema=self.variable_symbols).select(pl.all()).cast(pl.Float64)
577
+ self.notify()
578
+
579
+ return self.offspring
580
+
581
+ def update(self, *_, **__):
582
+ """Do nothing."""
583
+
584
+ def state(self) -> Sequence[Message]:
585
+ """Return the state of the mutation operator."""
586
+ if self.offspring_original is None or self.offspring is None:
587
+ return []
588
+ if self.verbosity == 0:
589
+ return []
590
+ if self.verbosity == 1:
591
+ return [
592
+ FloatMessage(
593
+ topic=MutationMessageTopics.MUTATION_PROBABILITY,
594
+ source=self.__class__.__name__,
595
+ value=self.mutation_probability,
596
+ ),
597
+ ]
598
+ # verbosity == 2
599
+ return [
600
+ PolarsDataFrameMessage(
601
+ topic=MutationMessageTopics.OFFSPRING_ORIGINAL,
602
+ source=self.__class__.__name__,
603
+ value=self.offspring_original,
604
+ ),
605
+ PolarsDataFrameMessage(
606
+ topic=MutationMessageTopics.PARENTS,
607
+ source=self.__class__.__name__,
608
+ value=self.parents,
609
+ ),
610
+ PolarsDataFrameMessage(
611
+ topic=MutationMessageTopics.OFFSPRINGS,
612
+ source=self.__class__.__name__,
613
+ value=self.offspring,
614
+ ),
615
+ FloatMessage(
616
+ topic=MutationMessageTopics.MUTATION_PROBABILITY,
617
+ source=self.__class__.__name__,
618
+ value=self.mutation_probability,
619
+ ),
620
+ ]
621
+
622
+
623
+ class MPTMutation(BaseMutation):
624
+ """Makinen, Periaux and Toivanen (MTP) mutation.
625
+
626
+ Applies small mutations to mixed-integer variables using a mutation exponent strategy.
627
+ """
628
+
629
+ @property
630
+ def provided_topics(self) -> dict[int, Sequence[MutationMessageTopics]]:
631
+ """The message topics provided by the mutation operator."""
632
+ return {
633
+ 0: [],
634
+ 1: [MutationMessageTopics.MUTATION_PROBABILITY],
635
+ 2: [
636
+ MutationMessageTopics.MUTATION_PROBABILITY,
637
+ MutationMessageTopics.OFFSPRING_ORIGINAL,
638
+ MutationMessageTopics.OFFSPRINGS,
639
+ ],
640
+ }
641
+
642
+ @property
643
+ def interested_topics(self):
644
+ """The message topics that the mutation operator is interested in."""
645
+ return []
646
+
647
+ def __init__(
648
+ self,
649
+ *,
650
+ problem: Problem,
651
+ seed: int,
652
+ mutation_probability: float | None = None,
653
+ mutation_exponent: float = 2.0,
654
+ **kwargs,
655
+ ):
656
+ """Initialize a small mutation operator.
657
+
658
+ Args:
659
+ problem (Problem): Optimization problem.
660
+ seed (int): RNG seed.
661
+ mutation_probability (float | None): Probability of mutation per gene.
662
+ mutation_exponent (float): Controls strength of small mutation (larger means smaller mutations).
663
+ kwargs: Additional keyword arguments. These are passed to the Subscriber class. At the very least, the
664
+ publisher must be passed. See the Subscriber class for more information.
665
+ """
666
+ super().__init__(problem, **kwargs)
667
+ self.rng = np.random.default_rng(seed)
668
+ self.seed = seed
669
+ self.mutation_exponent = mutation_exponent
670
+ self.mutation_probability = (
671
+ 1 / len(self.variable_symbols) if mutation_probability is None else mutation_probability
672
+ )
673
+
674
+ def _mutate_value(self, x, lower_bound, upper_bound):
675
+ """Apply small mutation to a single float value using mutation exponent."""
676
+ t = (x - lower_bound) / (upper_bound - lower_bound)
677
+ rnd = self.rng.uniform(0, 1)
678
+
679
+ if rnd < t:
680
+ tm = t - t * ((t - rnd) / t) ** self.mutation_exponent
681
+ elif rnd > t:
682
+ tm = t + (1 - t) * ((rnd - t) / (1 - t)) ** self.mutation_exponent
683
+ else:
684
+ tm = t
685
+
686
+ return (1 - tm) * lower_bound + tm * upper_bound
687
+
688
+ def do(self, offsprings: pl.DataFrame, parents: pl.DataFrame) -> pl.DataFrame:
689
+ """Perform the MPT mutation operation.
690
+
691
+ Args:
692
+ offsprings (pl.DataFrame): the offspring population to mutate.
693
+ parents (pl.DataFrame): the parent population from which the offspring
694
+ was generated (via crossover). Not used in the mutation operator.
695
+
696
+ Returns:
697
+ pl.DataFrame: the offspring resulting from the mutation.
698
+ """
699
+ self.offspring_original = copy.copy(offsprings)
700
+ self.parents = parents
701
+
702
+ population = offsprings.to_numpy().astype(float)
703
+
704
+ for i in range(population.shape[0]):
705
+ for j, var in enumerate(self.problem.variables):
706
+ if self.rng.random() < self.mutation_probability:
707
+ x = population[i, j]
708
+ lower_bound, upper_bound = var.lowerbound, var.upperbound
709
+ if var.variable_type in [VariableTypeEnum.binary, VariableTypeEnum.integer]:
710
+ # Round after float mutation to keep integer domain
711
+ population[i, j] = round(self._mutate_value(x, lower_bound, upper_bound))
712
+ else:
713
+ population[i, j] = self._mutate_value(x, lower_bound, upper_bound)
714
+
715
+ self.offspring = pl.from_numpy(population, schema=self.variable_symbols).select(pl.all()).cast(pl.Float64)
716
+ self.notify()
717
+ return self.offspring
718
+
719
+ def update(self, *_, **__):
720
+ """Do nothing."""
721
+
722
+ def state(self) -> Sequence[Message]:
723
+ """Return the state of the mutation operator."""
724
+ if self.offspring_original is None or self.offspring is None:
725
+ return []
726
+ if self.verbosity == 0:
727
+ return []
728
+ if self.verbosity == 1:
729
+ return [
730
+ FloatMessage(
731
+ topic=MutationMessageTopics.MUTATION_PROBABILITY,
732
+ source=self.__class__.__name__,
733
+ value=self.mutation_probability,
734
+ ),
735
+ ]
736
+ return [
737
+ PolarsDataFrameMessage(
738
+ topic=MutationMessageTopics.OFFSPRING_ORIGINAL,
739
+ source=self.__class__.__name__,
740
+ value=self.offspring_original,
741
+ ),
742
+ PolarsDataFrameMessage(
743
+ topic=MutationMessageTopics.PARENTS,
744
+ source=self.__class__.__name__,
745
+ value=self.parents,
746
+ ),
747
+ PolarsDataFrameMessage(
748
+ topic=MutationMessageTopics.OFFSPRINGS,
749
+ source=self.__class__.__name__,
750
+ value=self.offspring,
751
+ ),
752
+ FloatMessage(
753
+ topic=MutationMessageTopics.MUTATION_PROBABILITY,
754
+ source=self.__class__.__name__,
755
+ value=self.mutation_probability,
756
+ ),
757
+ ]
758
+
759
+
760
+ class NonUniformMutation(BaseMutation):
761
+ """Non-uniform mutation operator.
762
+
763
+ The mutation strength decays over generations.
764
+ """
765
+
766
+ @property
767
+ def provided_topics(self) -> dict[int, Sequence[MutationMessageTopics]]:
768
+ """The message topics provided by the mutation operator."""
769
+ return {
770
+ 0: [],
771
+ 1: [MutationMessageTopics.MUTATION_PROBABILITY],
772
+ 2: [
773
+ MutationMessageTopics.MUTATION_PROBABILITY,
774
+ MutationMessageTopics.OFFSPRING_ORIGINAL,
775
+ MutationMessageTopics.OFFSPRINGS,
776
+ ],
777
+ }
778
+
779
+ @property
780
+ def interested_topics(self):
781
+ """The message topics that the mutation operator is interested in."""
782
+ return []
783
+
784
+ def __init__(
785
+ self,
786
+ *,
787
+ problem: Problem,
788
+ seed: int,
789
+ mutation_probability: float | None = None,
790
+ b: float = 5.0, # decay parameter
791
+ max_generations: int,
792
+ **kwargs,
793
+ ):
794
+ """Initialize a Non-uniform mutation operator.
795
+
796
+ Args:
797
+ problem (Problem): The optimization problem definition.
798
+ seed (int): Random number generator seed for reproducibility.
799
+ mutation_probability (float | None): Probability of mutating each
800
+ gene. If None, defaults to 1 / number of variables.
801
+ b (float): Non-uniform mutation decay parameter. Higher values cause
802
+ faster reduction in mutation strength over generations.
803
+ max_generations (int): Maximum number of generations in the evolutionary run. Used to scale mutation decay.
804
+ **kwargs: Additional keyword arguments passed to the base mutation class.
805
+ """
806
+ super().__init__(problem, **kwargs)
807
+ self.rng = np.random.default_rng(seed)
808
+ self.seed = seed
809
+ self.b = b
810
+ self.current_generation = 0
811
+ self.max_generations = max_generations
812
+ self.mutation_probability = (
813
+ 1 / len(self.variable_symbols) if mutation_probability is None else mutation_probability
814
+ )
815
+
816
+ def _mutate_value(self, x, lower_bound, upper_bound, mutation_threshold=0.5):
817
+ """Apply non-uniform mutation to a single float value.
818
+
819
+ Args:
820
+ x (float): The current value of the gene to be mutated.
821
+ lower_bound (float): The lower bound of the gene.
822
+ upper_bound (float): The upper bound of the gene.
823
+ mutation_threshold (float): The mutation threshold. Defaults to 0.5.
824
+
825
+ Returns:
826
+ float: The mutated gene value, clipped within the bounds [l, u].
827
+ """
828
+ r = self.rng.uniform(0, 1) # Random number to choose direction
829
+ t = self.current_generation
830
+ max_generations = self.max_generations
831
+ b = self.b
832
+
833
+ u_rand = self.rng.uniform(0, 1) # Random number for mutation strength
834
+ tau = (1 - t / max_generations) ** b
835
+
836
+ if r <= mutation_threshold:
837
+ y = upper_bound - x
838
+ delta = y * (1 - u_rand**tau)
839
+ xm = x + delta
840
+ else:
841
+ y = x - lower_bound
842
+ delta = y * (1 - u_rand**tau)
843
+ xm = x - delta
844
+
845
+ return np.clip(xm, lower_bound, upper_bound)
846
+
847
+ def do(self, offsprings: pl.DataFrame, parents: pl.DataFrame) -> pl.DataFrame:
848
+ """Perform non-uniform mutation.
849
+
850
+ Args:
851
+ offsprings (pl.DataFrame): The current offspring population to
852
+ mutate. Each row corresponds to one individual.
853
+ parents (pl.DataFrame): The parent population (not used in mutation but passed for interface consistency).
854
+
855
+ Returns:
856
+ pl.DataFrame: A new offspring population with mutated values applied. Returned as a Polars DataFrame.
857
+ """
858
+ self.offspring_original = copy.copy(offsprings)
859
+ self.parents = parents
860
+
861
+ population = offsprings.to_numpy().astype(float)
862
+
863
+ for i in range(population.shape[0]):
864
+ for j, var in enumerate(self.problem.variables):
865
+ if self.rng.random() < self.mutation_probability:
866
+ x = population[i, j]
867
+ lower_bound, upper_bound = var.lowerbound, var.upperbound
868
+ if var.variable_type in [VariableTypeEnum.binary, VariableTypeEnum.integer]:
869
+ population[i, j] = round(self._mutate_value(x, lower_bound, upper_bound))
870
+ else:
871
+ population[i, j] = self._mutate_value(x, lower_bound, upper_bound)
872
+
873
+ self.offspring = pl.from_numpy(population, schema=self.variable_symbols).cast(pl.Float64)
874
+ self.notify()
875
+
876
+ return self.offspring
877
+
878
+ def update(self, generation: int, **kwargs):
879
+ """Update current generation (used to reduce mutation strength over time)."""
880
+ self.current_generation = generation
881
+
882
+ def state(self) -> Sequence[Message]:
883
+ """Return state messages."""
884
+ if self.verbosity == 0:
885
+ return []
886
+ return [
887
+ FloatMessage(
888
+ topic=MutationMessageTopics.MUTATION_PROBABILITY,
889
+ source=self.__class__.__name__,
890
+ value=self.mutation_probability,
891
+ ),
892
+ ]
893
+
894
+
895
+ class SelfAdaptiveGaussianMutation(BaseMutation):
896
+ """Self-adaptive Gaussian mutation for real-coded evolutionary algorithms.
897
+
898
+ Evolves both solution vector and mutation step sizes (strategy parameters).
899
+ """
900
+
901
+ @property
902
+ def provided_topics(self) -> dict[int, Sequence[MutationMessageTopics]]:
903
+ """The message topics provided by the mutation operator."""
904
+ return {
905
+ 0: [],
906
+ 1: [
907
+ MutationMessageTopics.MUTATION_PROBABILITY,
908
+ ],
909
+ 2: [
910
+ MutationMessageTopics.MUTATION_PROBABILITY,
911
+ MutationMessageTopics.OFFSPRING_ORIGINAL,
912
+ MutationMessageTopics.OFFSPRINGS,
913
+ ],
914
+ }
915
+
916
+ @property
917
+ def interested_topics(self):
918
+ """The message topics that the mutation operator is interested in."""
919
+ return []
920
+
921
+ def __init__(
922
+ self,
923
+ *,
924
+ problem: Problem,
925
+ seed: int,
926
+ mutation_probability: float | None = None,
927
+ **kwargs,
928
+ ):
929
+ """Initialize the self-adaptive Gaussian mutation operator.
930
+
931
+ Args:
932
+ problem (Problem): The optimization problem definition, including variable bounds and types.
933
+ seed (int): Seed for the random number generator to ensure reproducibility.
934
+ mutation_probability (float | None): Probability of mutating each gene.
935
+ If None, it defaults to 1 divided by the number of variables.
936
+ **kwargs: Additional keyword arguments passed to the base mutation class.
937
+
938
+ Attributes:
939
+ rng (Generator): NumPy random number generator initialized with the given seed.
940
+ seed (int): The seed used for reproducibility.
941
+ num_vars (int): Number of variables in the problem.
942
+ mutation_probability (float): Probability of mutating each gene.
943
+ tau_prime (float): Global learning rate, used in step size adaptation.
944
+ tau (float): Local learning rate, used in step size adaptation.
945
+ """
946
+ super().__init__(problem=problem, **kwargs)
947
+
948
+ self.rng = np.random.default_rng(seed)
949
+ self.seed = seed
950
+ self.num_vars = len(self.variable_symbols)
951
+
952
+ self.mutation_probability = 1 / self.num_vars if mutation_probability is None else mutation_probability
953
+
954
+ self.tau_prime = 1 / np.sqrt(2 * self.num_vars)
955
+ self.tau = 1 / np.sqrt(2 * np.sqrt(self.num_vars))
956
+
957
+ def do(
958
+ self,
959
+ offsprings: pl.DataFrame,
960
+ parents: pl.DataFrame,
961
+ step_sizes: np.ndarray | None = None,
962
+ ) -> tuple[pl.DataFrame, np.ndarray]:
963
+ """Apply self-adaptive Gaussian mutation.
964
+
965
+ Args:
966
+ offsprings (pl.DataFrame): Current offspring population.
967
+ parents (pl.DataFrame): Parent population.
968
+ step_sizes (np.ndarray | None): Step sizes for each gene of each individual.
969
+
970
+ Returns:
971
+ tuple:
972
+ - Mutated offspring population as a Polars DataFrame.
973
+ - Updated step sizes as a NumPy array.
974
+ """
975
+ self.offspring_original = offsprings
976
+ self.parents = parents
977
+
978
+ offspring_array = offsprings.to_numpy().astype(float)
979
+
980
+ if step_sizes is None:
981
+ step_sizes = np.full_like(offspring_array, fill_value=0.1)
982
+
983
+ new_offspring, new_eta = self._mutation(offspring_array, step_sizes)
984
+
985
+ mutated_df = pl.from_numpy(new_offspring, schema=self.variable_symbols).cast(pl.Float64)
986
+ self.offspring = mutated_df
987
+ self.notify()
988
+
989
+ return mutated_df, new_eta
990
+
991
+ def _mutation(self, variables: np.ndarray, eta: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
992
+ """Perform the self-adaptive mutation.
993
+
994
+ Args:
995
+ variables (np.ndarray): Current offspring population as a NumPy array.
996
+ eta (np.ndarray): Current step sizes for mutation.
997
+
998
+ Returns:
999
+ tuple[np.ndarray, np.ndarray]: Mutated population and updated step sizes.
1000
+ """
1001
+ new_variables = variables.copy()
1002
+ new_eta = eta.copy()
1003
+
1004
+ for i in range(variables.shape[0]):
1005
+ common_noise = self.rng.normal()
1006
+ for j in range(variables.shape[1]):
1007
+ if self.rng.random() < self.mutation_probability:
1008
+ rnd_number = self.rng.normal() # random number in the interval [0, 1]
1009
+ new_eta[i, j] *= np.exp(self.tau_prime * common_noise + self.tau * rnd_number)
1010
+ new_variables[i, j] += new_eta[i, j] * rnd_number
1011
+
1012
+ return new_variables, new_eta
1013
+
1014
+ def update(self, *_, **__):
1015
+ """Do nothing."""
1016
+
1017
+ def state(self) -> Sequence[Message]:
1018
+ """Return the state of the mutation operator."""
1019
+ if self.offspring_original is None or self.offspring is None:
1020
+ return []
1021
+ if self.verbosity == 0:
1022
+ return []
1023
+ if self.verbosity == 1:
1024
+ return [
1025
+ FloatMessage(
1026
+ topic=MutationMessageTopics.MUTATION_PROBABILITY,
1027
+ source=self.__class__.__name__,
1028
+ value=self.mutation_probability,
1029
+ ),
1030
+ ]
1031
+ # verbosity == 2
1032
+ return [
1033
+ PolarsDataFrameMessage(
1034
+ topic=MutationMessageTopics.OFFSPRING_ORIGINAL,
1035
+ source=self.__class__.__name__,
1036
+ value=self.offspring_original,
1037
+ ),
1038
+ PolarsDataFrameMessage(
1039
+ topic=MutationMessageTopics.PARENTS,
1040
+ source=self.__class__.__name__,
1041
+ value=self.parents,
1042
+ ),
1043
+ PolarsDataFrameMessage(
1044
+ topic=MutationMessageTopics.OFFSPRINGS,
1045
+ source=self.__class__.__name__,
1046
+ value=self.offspring,
1047
+ ),
1048
+ FloatMessage(
1049
+ topic=MutationMessageTopics.MUTATION_PROBABILITY,
1050
+ source=self.__class__.__name__,
1051
+ value=self.mutation_probability,
1052
+ ),
1053
+ ]