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