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,1282 @@
|
|
|
1
|
+
"""Evolutionary operators for recombination.
|
|
2
|
+
|
|
3
|
+
Various evolutionary operators for recombination
|
|
4
|
+
in multiobjective optimization are defined here.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import copy
|
|
8
|
+
from abc import abstractmethod
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
import polars as pl
|
|
13
|
+
|
|
14
|
+
from desdeo.problem import Problem, VariableDomainTypeEnum
|
|
15
|
+
from desdeo.tools.message import (
|
|
16
|
+
CrossoverMessageTopics,
|
|
17
|
+
FloatMessage,
|
|
18
|
+
Message,
|
|
19
|
+
PolarsDataFrameMessage,
|
|
20
|
+
)
|
|
21
|
+
from desdeo.tools.patterns import Publisher, Subscriber
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BaseCrossover(Subscriber):
|
|
25
|
+
"""A base class for crossover operators."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, problem: Problem, verbosity: int, publisher: Publisher):
|
|
28
|
+
"""Initialize a crossover operator."""
|
|
29
|
+
super().__init__(verbosity=verbosity, publisher=publisher)
|
|
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
|
+
|
|
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, *, population: pl.DataFrame, to_mate: list[int] | None = None) -> pl.DataFrame:
|
|
40
|
+
"""Perform the crossover operation.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
population (pl.DataFrame): the population to perform the crossover with. The DataFrame
|
|
44
|
+
contains the decision vectors, the target vectors, and the constraint vectors.
|
|
45
|
+
to_mate (list[int] | None): the indices of the population members that should
|
|
46
|
+
participate in the crossover. If `None`, the whole population is subject
|
|
47
|
+
to the crossover.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
pl.DataFrame: the offspring resulting from the crossover.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class SimulatedBinaryCrossover(BaseCrossover):
|
|
55
|
+
"""A class for creating a simulated binary crossover operator.
|
|
56
|
+
|
|
57
|
+
Reference:
|
|
58
|
+
Kalyanmoy Deb and Ram Bhushan Agrawal. 1995. Simulated binary crossover for continuous search space.
|
|
59
|
+
Complex Systems 9, 2 (1995), 115-148.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def provided_topics(self) -> dict[int, Sequence[CrossoverMessageTopics]]:
|
|
64
|
+
"""The message topics provided by the crossover operator."""
|
|
65
|
+
return {
|
|
66
|
+
0: [],
|
|
67
|
+
1: [CrossoverMessageTopics.XOVER_PROBABILITY, CrossoverMessageTopics.XOVER_DISTRIBUTION],
|
|
68
|
+
2: [
|
|
69
|
+
CrossoverMessageTopics.XOVER_PROBABILITY,
|
|
70
|
+
CrossoverMessageTopics.XOVER_DISTRIBUTION,
|
|
71
|
+
CrossoverMessageTopics.PARENTS,
|
|
72
|
+
CrossoverMessageTopics.OFFSPRINGS,
|
|
73
|
+
],
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def interested_topics(self):
|
|
78
|
+
"""The message topics the crossover operator is interested in."""
|
|
79
|
+
return []
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
*,
|
|
84
|
+
problem: Problem,
|
|
85
|
+
seed: int,
|
|
86
|
+
verbosity: int,
|
|
87
|
+
publisher: Publisher,
|
|
88
|
+
xover_probability: float = 1.0,
|
|
89
|
+
xover_distribution: float = 30,
|
|
90
|
+
):
|
|
91
|
+
"""Initialize a simulated binary crossover operator.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
problem (Problem): the problem object.
|
|
95
|
+
seed (int): the seed for the random number generator.
|
|
96
|
+
verbosity (int): the verbosity level of the component. The keys in `provided_topics` tell what
|
|
97
|
+
topics are provided by the operator at each verbosity level. Recommended to be set to 1.
|
|
98
|
+
publisher (Publisher): the publisher to which the operator will publish messages.
|
|
99
|
+
xover_probability (float, optional): the crossover probability
|
|
100
|
+
parameter. Ranges between 0 and 1.0. Defaults to 1.0.
|
|
101
|
+
xover_distribution (float, optional): the crossover distribution
|
|
102
|
+
parameter. Must be positive. Defaults to 30.
|
|
103
|
+
"""
|
|
104
|
+
# Subscribes to no topics, so no need to stroe/pass the topics to the super class.
|
|
105
|
+
super().__init__(problem, verbosity=verbosity, publisher=publisher)
|
|
106
|
+
self.problem = problem
|
|
107
|
+
|
|
108
|
+
if not 0 <= xover_probability <= 1:
|
|
109
|
+
raise ValueError("Crossover probability must be between 0 and 1.")
|
|
110
|
+
if xover_distribution <= 0:
|
|
111
|
+
raise ValueError("Crossover distribution must be positive.")
|
|
112
|
+
self.xover_probability = xover_probability
|
|
113
|
+
self.xover_distribution = xover_distribution
|
|
114
|
+
self.parent_population: pl.DataFrame
|
|
115
|
+
self.offspring_population: pl.DataFrame
|
|
116
|
+
self.rng = np.random.default_rng(seed)
|
|
117
|
+
self.seed = seed
|
|
118
|
+
|
|
119
|
+
def do(
|
|
120
|
+
self,
|
|
121
|
+
*,
|
|
122
|
+
population: pl.DataFrame,
|
|
123
|
+
to_mate: list[int] | None = None,
|
|
124
|
+
) -> pl.DataFrame:
|
|
125
|
+
"""Perform the simulated binary crossover operation.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
population (pl.DataFrame): the population to perform the crossover with. The DataFrame
|
|
129
|
+
contains the decision vectors, the target vectors, and the constraint vectors.
|
|
130
|
+
to_mate (list[int] | None): the indices of the population members that should
|
|
131
|
+
participate in the crossover. If `None`, the whole population is subject
|
|
132
|
+
to the crossover.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
pl.DataFrame: the offspring resulting from the crossover.
|
|
136
|
+
"""
|
|
137
|
+
self.parent_population = population
|
|
138
|
+
pop_size = self.parent_population.shape[0]
|
|
139
|
+
num_var = len(self.variable_symbols)
|
|
140
|
+
|
|
141
|
+
parent_decvars = self.parent_population[self.variable_symbols].to_numpy()
|
|
142
|
+
|
|
143
|
+
if to_mate is None:
|
|
144
|
+
shuffled_ids = list(range(pop_size))
|
|
145
|
+
self.rng.shuffle(shuffled_ids)
|
|
146
|
+
else:
|
|
147
|
+
shuffled_ids = to_mate
|
|
148
|
+
mating_pop = parent_decvars[shuffled_ids]
|
|
149
|
+
mate_size = len(shuffled_ids)
|
|
150
|
+
|
|
151
|
+
if len(shuffled_ids) % 2 == 1:
|
|
152
|
+
mating_pop = np.vstack((mating_pop, mating_pop[0]))
|
|
153
|
+
mate_size += 1
|
|
154
|
+
|
|
155
|
+
offspring = np.zeros_like(mating_pop)
|
|
156
|
+
|
|
157
|
+
HALF = 0.5 # NOQA: N806
|
|
158
|
+
# TODO(@light-weaver): Extract into a numba jitted function.
|
|
159
|
+
for i in range(0, mate_size, 2):
|
|
160
|
+
beta = np.zeros(num_var)
|
|
161
|
+
miu = self.rng.random(num_var)
|
|
162
|
+
beta[miu <= HALF] = (2 * miu[miu <= HALF]) ** (1 / (self.xover_distribution + 1))
|
|
163
|
+
beta[miu > HALF] = (2 - 2 * miu[miu > HALF]) ** (-1 / (self.xover_distribution + 1))
|
|
164
|
+
beta = beta * ((-1) ** self.rng.integers(low=0, high=2, size=num_var))
|
|
165
|
+
beta[self.rng.random(num_var) > self.xover_probability] = 1
|
|
166
|
+
avg = (mating_pop[i] + mating_pop[i + 1]) / 2
|
|
167
|
+
diff = (mating_pop[i] - mating_pop[i + 1]) / 2
|
|
168
|
+
offspring[i] = avg + beta * diff
|
|
169
|
+
offspring[i + 1] = avg - beta * diff
|
|
170
|
+
|
|
171
|
+
self.offspring_population = pl.from_numpy(offspring, schema=self.variable_symbols)
|
|
172
|
+
self.notify()
|
|
173
|
+
|
|
174
|
+
return self.offspring_population
|
|
175
|
+
|
|
176
|
+
def update(self, *_, **__):
|
|
177
|
+
"""Do nothing. This is just the basic SBX operator."""
|
|
178
|
+
|
|
179
|
+
def state(self) -> Sequence[Message]:
|
|
180
|
+
"""Return the state of the crossover operator."""
|
|
181
|
+
if self.parent_population is None or self.offspring_population is None:
|
|
182
|
+
return []
|
|
183
|
+
if self.verbosity == 0:
|
|
184
|
+
return []
|
|
185
|
+
if self.verbosity == 1:
|
|
186
|
+
return [
|
|
187
|
+
FloatMessage(
|
|
188
|
+
topic=CrossoverMessageTopics.XOVER_PROBABILITY,
|
|
189
|
+
source="SimulatedBinaryCrossover",
|
|
190
|
+
value=self.xover_probability,
|
|
191
|
+
),
|
|
192
|
+
FloatMessage(
|
|
193
|
+
topic=CrossoverMessageTopics.XOVER_DISTRIBUTION,
|
|
194
|
+
source="SimulatedBinaryCrossover",
|
|
195
|
+
value=self.xover_distribution,
|
|
196
|
+
),
|
|
197
|
+
]
|
|
198
|
+
# verbosity == 2 or higher
|
|
199
|
+
return [
|
|
200
|
+
FloatMessage(
|
|
201
|
+
topic=CrossoverMessageTopics.XOVER_PROBABILITY,
|
|
202
|
+
source="SimulatedBinaryCrossover",
|
|
203
|
+
value=self.xover_probability,
|
|
204
|
+
),
|
|
205
|
+
FloatMessage(
|
|
206
|
+
topic=CrossoverMessageTopics.XOVER_DISTRIBUTION,
|
|
207
|
+
source="SimulatedBinaryCrossover",
|
|
208
|
+
value=self.xover_distribution,
|
|
209
|
+
),
|
|
210
|
+
PolarsDataFrameMessage(
|
|
211
|
+
topic=CrossoverMessageTopics.PARENTS,
|
|
212
|
+
source="SimulatedBinaryCrossover",
|
|
213
|
+
value=self.parent_population,
|
|
214
|
+
),
|
|
215
|
+
PolarsDataFrameMessage(
|
|
216
|
+
topic=CrossoverMessageTopics.OFFSPRINGS,
|
|
217
|
+
source="SimulatedBinaryCrossover",
|
|
218
|
+
value=self.offspring_population,
|
|
219
|
+
),
|
|
220
|
+
]
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class SinglePointBinaryCrossover(BaseCrossover):
|
|
224
|
+
"""A class that defines the single point binary crossover operation."""
|
|
225
|
+
|
|
226
|
+
def __init__(self, *, problem: Problem, seed: int, verbosity: int, publisher: Publisher):
|
|
227
|
+
"""Initialize the single point binary crossover operator.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
problem (Problem): the problem object.
|
|
231
|
+
seed (int): the seed used in the random number generator for choosing the crossover point.
|
|
232
|
+
verbosity (int): the verbosity level of the component. The keys in `provided_topics` tell what
|
|
233
|
+
topics are provided by the operator at each verbosity level.
|
|
234
|
+
publisher (Publisher): the publisher to which the operator will publish messages.
|
|
235
|
+
"""
|
|
236
|
+
super().__init__(problem, verbosity=verbosity, publisher=publisher)
|
|
237
|
+
self.seed = seed
|
|
238
|
+
|
|
239
|
+
self.parent_population: pl.DataFrame
|
|
240
|
+
self.offspring_population: pl.DataFrame
|
|
241
|
+
self.rng = np.random.default_rng(seed)
|
|
242
|
+
self.seed = seed
|
|
243
|
+
|
|
244
|
+
@property
|
|
245
|
+
def provided_topics(self) -> dict[int, Sequence[CrossoverMessageTopics]]:
|
|
246
|
+
"""The message topics provided by the single point binary crossover operator."""
|
|
247
|
+
return {
|
|
248
|
+
0: [],
|
|
249
|
+
1: [],
|
|
250
|
+
2: [
|
|
251
|
+
CrossoverMessageTopics.PARENTS,
|
|
252
|
+
CrossoverMessageTopics.OFFSPRINGS,
|
|
253
|
+
],
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
@property
|
|
257
|
+
def interested_topics(self):
|
|
258
|
+
"""The message topics the single point binary crossover operator is interested in."""
|
|
259
|
+
return []
|
|
260
|
+
|
|
261
|
+
def do(
|
|
262
|
+
self,
|
|
263
|
+
*,
|
|
264
|
+
population: pl.DataFrame,
|
|
265
|
+
to_mate: list[int] | None = None,
|
|
266
|
+
) -> pl.DataFrame:
|
|
267
|
+
"""Perform single point binary crossover.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
population (pl.DataFrame): the population to perform the crossover with.
|
|
271
|
+
to_mate (list[int] | None, optional): indices. Defaults to None.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
pl.DataFrame: the offspring from the crossover.
|
|
275
|
+
"""
|
|
276
|
+
self.parent_population = population
|
|
277
|
+
pop_size = self.parent_population.shape[0]
|
|
278
|
+
num_var = len(self.variable_symbols)
|
|
279
|
+
|
|
280
|
+
parent_decision_vars = self.parent_population[self.variable_symbols].to_numpy().astype(np.bool)
|
|
281
|
+
|
|
282
|
+
if to_mate is None:
|
|
283
|
+
shuffled_ids = list(range(pop_size))
|
|
284
|
+
self.rng.shuffle(shuffled_ids)
|
|
285
|
+
else:
|
|
286
|
+
shuffled_ids = copy.copy(to_mate)
|
|
287
|
+
|
|
288
|
+
mating_pop = parent_decision_vars[shuffled_ids]
|
|
289
|
+
mating_pop_size = len(shuffled_ids)
|
|
290
|
+
original_mating_pop_size = mating_pop_size
|
|
291
|
+
|
|
292
|
+
if mating_pop_size % 2 != 0:
|
|
293
|
+
# if the number of member to mate is of uneven size, copy the first member to the tail
|
|
294
|
+
mating_pop = np.vstack((mating_pop, mating_pop[0]))
|
|
295
|
+
mating_pop_size += 1
|
|
296
|
+
shuffled_ids.append(shuffled_ids[0])
|
|
297
|
+
|
|
298
|
+
# split the population into parents, one with members with even numbered indices, the
|
|
299
|
+
# other with uneven numbered indices
|
|
300
|
+
parents1 = mating_pop[[shuffled_ids[i] for i in range(0, mating_pop_size, 2)]]
|
|
301
|
+
parents2 = mating_pop[[shuffled_ids[i] for i in range(1, mating_pop_size, 2)]]
|
|
302
|
+
|
|
303
|
+
cross_over_points = self.rng.integers(1, num_var - 1, mating_pop_size // 2)
|
|
304
|
+
|
|
305
|
+
# create a mask where, on each row, the element is 1 before the crossover point,
|
|
306
|
+
# and zero after it
|
|
307
|
+
cross_over_mask = np.zeros_like(parents1, dtype=np.bool)
|
|
308
|
+
cross_over_mask[np.arange(cross_over_mask.shape[1]) < cross_over_points[:, None]] = 1
|
|
309
|
+
|
|
310
|
+
# pick genes from the first parents before the crossover point
|
|
311
|
+
# pick genes from the second parents after, and including, the crossover point
|
|
312
|
+
offspring1_first = cross_over_mask & parents1
|
|
313
|
+
offspring1_second = (~cross_over_mask) & parents2
|
|
314
|
+
|
|
315
|
+
# combine into a first half of the whole offspring population
|
|
316
|
+
offspring1 = offspring1_first | offspring1_second
|
|
317
|
+
|
|
318
|
+
# pick genes from the first parents after, and including, the crossover point
|
|
319
|
+
# pick genes from the second parents before the crossover point
|
|
320
|
+
offspring2_first = (~cross_over_mask) & parents1
|
|
321
|
+
offspring2_second = cross_over_mask & parents2
|
|
322
|
+
|
|
323
|
+
# combine into the second half of the whole offspring population
|
|
324
|
+
offspring2 = offspring2_first | offspring2_second
|
|
325
|
+
|
|
326
|
+
# combine the two offspring populations into one, drop the last member if the number of
|
|
327
|
+
# indices (to_mate) is uneven
|
|
328
|
+
self.offspring_population = pl.from_numpy(
|
|
329
|
+
np.vstack((offspring1, offspring2))[
|
|
330
|
+
: (original_mating_pop_size if original_mating_pop_size % 2 == 0 else -1)
|
|
331
|
+
],
|
|
332
|
+
schema=self.variable_symbols,
|
|
333
|
+
).select(pl.all().cast(pl.Float64))
|
|
334
|
+
self.notify()
|
|
335
|
+
|
|
336
|
+
return self.offspring_population
|
|
337
|
+
|
|
338
|
+
def update(self, *_, **__):
|
|
339
|
+
"""Do nothing. This is just the basic single point binary crossover operator."""
|
|
340
|
+
|
|
341
|
+
def state(self) -> Sequence[Message]:
|
|
342
|
+
"""Return the state of the single ponit binary crossover operator."""
|
|
343
|
+
if self.parent_population is None or self.offspring_population is None:
|
|
344
|
+
return []
|
|
345
|
+
if self.verbosity == 0:
|
|
346
|
+
return []
|
|
347
|
+
if self.verbosity == 1:
|
|
348
|
+
return []
|
|
349
|
+
# verbosity == 2 or higher
|
|
350
|
+
return [
|
|
351
|
+
PolarsDataFrameMessage(
|
|
352
|
+
topic=CrossoverMessageTopics.PARENTS,
|
|
353
|
+
source="SimulatedBinaryCrossover",
|
|
354
|
+
value=self.parent_population,
|
|
355
|
+
),
|
|
356
|
+
PolarsDataFrameMessage(
|
|
357
|
+
topic=CrossoverMessageTopics.OFFSPRINGS,
|
|
358
|
+
source="SimulatedBinaryCrossover",
|
|
359
|
+
value=self.offspring_population,
|
|
360
|
+
),
|
|
361
|
+
]
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
class UniformIntegerCrossover(BaseCrossover):
|
|
365
|
+
"""A class that defines the uniform integer crossover operation."""
|
|
366
|
+
|
|
367
|
+
def __init__(self, *, problem: Problem, seed: int, verbosity: int, publisher: Publisher):
|
|
368
|
+
"""Initialize the uniform integer crossover operator.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
problem (Problem): the problem object.
|
|
372
|
+
seed (int): the seed used in the random number generator for choosing the crossover point.
|
|
373
|
+
verbosity (int): the verbosity level of the component. The keys in `provided_topics` tell what
|
|
374
|
+
topics are provided by the operator at each verbosity level. Recommended to be set to 1.
|
|
375
|
+
publisher (Publisher): the publisher to which the operator will publish messages.
|
|
376
|
+
"""
|
|
377
|
+
super().__init__(problem, verbosity=verbosity, publisher=publisher)
|
|
378
|
+
self.seed = seed
|
|
379
|
+
|
|
380
|
+
self.parent_population: pl.DataFrame
|
|
381
|
+
self.offspring_population: pl.DataFrame
|
|
382
|
+
self.rng = np.random.default_rng(seed)
|
|
383
|
+
self.seed = seed
|
|
384
|
+
|
|
385
|
+
@property
|
|
386
|
+
def provided_topics(self) -> dict[int, Sequence[CrossoverMessageTopics]]:
|
|
387
|
+
"""The message topics provided by the single point binary crossover operator."""
|
|
388
|
+
return {
|
|
389
|
+
0: [],
|
|
390
|
+
1: [],
|
|
391
|
+
2: [
|
|
392
|
+
CrossoverMessageTopics.PARENTS,
|
|
393
|
+
CrossoverMessageTopics.OFFSPRINGS,
|
|
394
|
+
],
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
@property
|
|
398
|
+
def interested_topics(self):
|
|
399
|
+
"""The message topics the single point binary crossover operator is interested in."""
|
|
400
|
+
return []
|
|
401
|
+
|
|
402
|
+
def do(
|
|
403
|
+
self,
|
|
404
|
+
*,
|
|
405
|
+
population: pl.DataFrame,
|
|
406
|
+
to_mate: list[int] | None = None,
|
|
407
|
+
) -> pl.DataFrame:
|
|
408
|
+
"""Perform single point binary crossover.
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
population (pl.DataFrame): the population to perform the crossover with.
|
|
412
|
+
to_mate (list[int] | None, optional): indices. Defaults to None.
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
pl.DataFrame: the offspring from the crossover.
|
|
416
|
+
"""
|
|
417
|
+
self.parent_population = population
|
|
418
|
+
pop_size = self.parent_population.shape[0]
|
|
419
|
+
num_var = len(self.variable_symbols)
|
|
420
|
+
|
|
421
|
+
parent_decision_vars = self.parent_population[self.variable_symbols].to_numpy().astype(int)
|
|
422
|
+
|
|
423
|
+
if to_mate is None:
|
|
424
|
+
shuffled_ids = list(range(pop_size))
|
|
425
|
+
self.rng.shuffle(shuffled_ids)
|
|
426
|
+
else:
|
|
427
|
+
shuffled_ids = copy.copy(to_mate)
|
|
428
|
+
|
|
429
|
+
mating_pop = parent_decision_vars[shuffled_ids]
|
|
430
|
+
mating_pop_size = len(shuffled_ids)
|
|
431
|
+
original_mating_pop_size = mating_pop_size
|
|
432
|
+
|
|
433
|
+
if mating_pop_size % 2 != 0:
|
|
434
|
+
# if the number of member to mate is of uneven size, copy the first member to the tail
|
|
435
|
+
mating_pop = np.vstack((mating_pop, mating_pop[0]))
|
|
436
|
+
mating_pop_size += 1
|
|
437
|
+
shuffled_ids.append(shuffled_ids[0])
|
|
438
|
+
|
|
439
|
+
# split the population into parents, one with members with even numbered indices, the
|
|
440
|
+
# other with uneven numbered indices
|
|
441
|
+
parents1 = mating_pop[[shuffled_ids[i] for i in range(0, mating_pop_size, 2)]]
|
|
442
|
+
parents2 = mating_pop[[shuffled_ids[i] for i in range(1, mating_pop_size, 2)]]
|
|
443
|
+
|
|
444
|
+
mask = self.rng.choice([True, False], size=num_var)
|
|
445
|
+
|
|
446
|
+
offspring1 = np.where(mask, parents1, parents2) # True, pick from parent1, False, pick from parent2
|
|
447
|
+
offspring2 = np.where(mask, parents2, parents1) # True, pick from parent2, False, pick from parent1
|
|
448
|
+
|
|
449
|
+
# combine the two offspring populations into one, drop the last member if the number of
|
|
450
|
+
# indices (to_mate) is uneven
|
|
451
|
+
self.offspring_population = pl.from_numpy(
|
|
452
|
+
np.vstack((offspring1, offspring2))[
|
|
453
|
+
: (original_mating_pop_size if original_mating_pop_size % 2 == 0 else -1)
|
|
454
|
+
],
|
|
455
|
+
schema=self.variable_symbols,
|
|
456
|
+
).select(pl.all().cast(pl.Float64))
|
|
457
|
+
|
|
458
|
+
self.notify()
|
|
459
|
+
|
|
460
|
+
return self.offspring_population
|
|
461
|
+
|
|
462
|
+
def update(self, *_, **__):
|
|
463
|
+
"""Do nothing. This is just the basic single point binary crossover operator."""
|
|
464
|
+
|
|
465
|
+
def state(self) -> Sequence[Message]:
|
|
466
|
+
"""Return the state of the single ponit binary crossover operator."""
|
|
467
|
+
if self.parent_population is None or self.offspring_population is None:
|
|
468
|
+
return []
|
|
469
|
+
if self.verbosity == 0:
|
|
470
|
+
return []
|
|
471
|
+
if self.verbosity == 1:
|
|
472
|
+
return []
|
|
473
|
+
# verbosity == 2 or higher
|
|
474
|
+
return [
|
|
475
|
+
PolarsDataFrameMessage(
|
|
476
|
+
topic=CrossoverMessageTopics.PARENTS,
|
|
477
|
+
source="SimulatedBinaryCrossover",
|
|
478
|
+
value=self.parent_population,
|
|
479
|
+
),
|
|
480
|
+
PolarsDataFrameMessage(
|
|
481
|
+
topic=CrossoverMessageTopics.OFFSPRINGS,
|
|
482
|
+
source="SimulatedBinaryCrossover",
|
|
483
|
+
value=self.offspring_population,
|
|
484
|
+
),
|
|
485
|
+
]
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
class UniformMixedIntegerCrossover(BaseCrossover):
|
|
489
|
+
"""A class that defines the uniform mixed-integer crossover operation.
|
|
490
|
+
|
|
491
|
+
TODO: This is virtually identical to `UniformIntegerCrossover`. The only
|
|
492
|
+
difference is that the `parent_decision_vars` in `do` are not casted to
|
|
493
|
+
`int`. This is not an ideal way to implement crossover for mixed-integer
|
|
494
|
+
stuff...
|
|
495
|
+
"""
|
|
496
|
+
|
|
497
|
+
def __init__(self, *, problem: Problem, seed: int, verbosity: int, publisher: Publisher):
|
|
498
|
+
"""Initialize the uniform integer crossover operator.
|
|
499
|
+
|
|
500
|
+
Args:
|
|
501
|
+
problem (Problem): the problem object.
|
|
502
|
+
seed (int): the seed used in the random number generator for choosing the crossover point.
|
|
503
|
+
verbosity (int): the verbosity level of the component. The keys in `provided_topics` tell what
|
|
504
|
+
topics are provided by the operator at each verbosity level. Recommended to be set to 1.
|
|
505
|
+
publisher (Publisher): the publisher to which the operator will publish messages.
|
|
506
|
+
"""
|
|
507
|
+
super().__init__(problem, verbosity=verbosity, publisher=publisher)
|
|
508
|
+
self.seed = seed
|
|
509
|
+
|
|
510
|
+
self.parent_population: pl.DataFrame
|
|
511
|
+
self.offspring_population: pl.DataFrame
|
|
512
|
+
self.rng = np.random.default_rng(seed)
|
|
513
|
+
self.seed = seed
|
|
514
|
+
|
|
515
|
+
@property
|
|
516
|
+
def provided_topics(self) -> dict[int, Sequence[CrossoverMessageTopics]]:
|
|
517
|
+
"""The message topics provided by the single point binary crossover operator."""
|
|
518
|
+
return {
|
|
519
|
+
0: [],
|
|
520
|
+
1: [],
|
|
521
|
+
2: [
|
|
522
|
+
CrossoverMessageTopics.PARENTS,
|
|
523
|
+
CrossoverMessageTopics.OFFSPRINGS,
|
|
524
|
+
],
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
@property
|
|
528
|
+
def interested_topics(self):
|
|
529
|
+
"""The message topics the single point binary crossover operator is interested in."""
|
|
530
|
+
return []
|
|
531
|
+
|
|
532
|
+
def do(
|
|
533
|
+
self,
|
|
534
|
+
*,
|
|
535
|
+
population: pl.DataFrame,
|
|
536
|
+
to_mate: list[int] | None = None,
|
|
537
|
+
) -> pl.DataFrame:
|
|
538
|
+
"""Perform single point binary crossover.
|
|
539
|
+
|
|
540
|
+
Args:
|
|
541
|
+
population (pl.DataFrame): the population to perform the crossover with.
|
|
542
|
+
to_mate (list[int] | None, optional): indices. Defaults to None.
|
|
543
|
+
|
|
544
|
+
Returns:
|
|
545
|
+
pl.DataFrame: the offspring from the crossover.
|
|
546
|
+
"""
|
|
547
|
+
self.parent_population = population
|
|
548
|
+
pop_size = self.parent_population.shape[0]
|
|
549
|
+
num_var = len(self.variable_symbols)
|
|
550
|
+
|
|
551
|
+
parent_decision_vars = self.parent_population[self.variable_symbols].to_numpy().astype(float)
|
|
552
|
+
|
|
553
|
+
if to_mate is None:
|
|
554
|
+
shuffled_ids = list(range(pop_size))
|
|
555
|
+
self.rng.shuffle(shuffled_ids)
|
|
556
|
+
else:
|
|
557
|
+
shuffled_ids = copy.copy(to_mate)
|
|
558
|
+
|
|
559
|
+
mating_pop = parent_decision_vars[shuffled_ids]
|
|
560
|
+
mating_pop_size = len(shuffled_ids)
|
|
561
|
+
original_mating_pop_size = mating_pop_size
|
|
562
|
+
|
|
563
|
+
if mating_pop_size % 2 != 0:
|
|
564
|
+
# if the number of member to mate is of uneven size, copy the first member to the tail
|
|
565
|
+
mating_pop = np.vstack((mating_pop, mating_pop[0]))
|
|
566
|
+
mating_pop_size += 1
|
|
567
|
+
shuffled_ids.append(shuffled_ids[0])
|
|
568
|
+
|
|
569
|
+
# split the population into parents, one with members with even numbered indices, the
|
|
570
|
+
# other with uneven numbered indices
|
|
571
|
+
parents1 = mating_pop[[shuffled_ids[i] for i in range(0, mating_pop_size, 2)]]
|
|
572
|
+
parents2 = mating_pop[[shuffled_ids[i] for i in range(1, mating_pop_size, 2)]]
|
|
573
|
+
|
|
574
|
+
mask = self.rng.choice([True, False], size=num_var)
|
|
575
|
+
|
|
576
|
+
offspring1 = np.where(mask, parents1, parents2) # True, pick from parent1, False, pick from parent2
|
|
577
|
+
offspring2 = np.where(mask, parents2, parents1) # True, pick from parent2, False, pick from parent1
|
|
578
|
+
|
|
579
|
+
# combine the two offspring populations into one, drop the last member if the number of
|
|
580
|
+
# indices (to_mate) is uneven
|
|
581
|
+
self.offspring_population = pl.from_numpy(
|
|
582
|
+
np.vstack((offspring1, offspring2))[
|
|
583
|
+
: (original_mating_pop_size if original_mating_pop_size % 2 == 0 else -1)
|
|
584
|
+
],
|
|
585
|
+
schema=self.variable_symbols,
|
|
586
|
+
).select(pl.all().cast(pl.Float64))
|
|
587
|
+
|
|
588
|
+
self.notify()
|
|
589
|
+
|
|
590
|
+
return self.offspring_population
|
|
591
|
+
|
|
592
|
+
def update(self, *_, **__):
|
|
593
|
+
"""Do nothing. This is just the basic single point binary crossover operator."""
|
|
594
|
+
|
|
595
|
+
def state(self) -> Sequence[Message]:
|
|
596
|
+
"""Return the state of the single point binary crossover operator."""
|
|
597
|
+
if self.parent_population is None or self.offspring_population is None:
|
|
598
|
+
return []
|
|
599
|
+
if self.verbosity == 0:
|
|
600
|
+
return []
|
|
601
|
+
if self.verbosity == 1:
|
|
602
|
+
return []
|
|
603
|
+
# verbosity == 2 or higher
|
|
604
|
+
return [
|
|
605
|
+
PolarsDataFrameMessage(
|
|
606
|
+
topic=CrossoverMessageTopics.PARENTS,
|
|
607
|
+
source="SimulatedBinaryCrossover",
|
|
608
|
+
value=self.parent_population,
|
|
609
|
+
),
|
|
610
|
+
PolarsDataFrameMessage(
|
|
611
|
+
topic=CrossoverMessageTopics.OFFSPRINGS,
|
|
612
|
+
source="SimulatedBinaryCrossover",
|
|
613
|
+
value=self.offspring_population,
|
|
614
|
+
),
|
|
615
|
+
]
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
class BlendAlphaCrossover(BaseCrossover):
|
|
619
|
+
"""Blend-alpha (BLX-alpha) crossover for continuous problems."""
|
|
620
|
+
|
|
621
|
+
@property
|
|
622
|
+
def provided_topics(self) -> dict[int, Sequence[CrossoverMessageTopics]]:
|
|
623
|
+
"""The message topics provided by the blend alpha crossover operator."""
|
|
624
|
+
return {
|
|
625
|
+
0: [],
|
|
626
|
+
1: [
|
|
627
|
+
CrossoverMessageTopics.XOVER_PROBABILITY,
|
|
628
|
+
CrossoverMessageTopics.ALPHA,
|
|
629
|
+
],
|
|
630
|
+
2: [
|
|
631
|
+
CrossoverMessageTopics.XOVER_PROBABILITY,
|
|
632
|
+
CrossoverMessageTopics.ALPHA,
|
|
633
|
+
CrossoverMessageTopics.PARENTS,
|
|
634
|
+
CrossoverMessageTopics.OFFSPRINGS,
|
|
635
|
+
],
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
@property
|
|
639
|
+
def interested_topics(self):
|
|
640
|
+
"""The message topics provided by the blend alpha crossover operator."""
|
|
641
|
+
return []
|
|
642
|
+
|
|
643
|
+
def __init__(
|
|
644
|
+
self,
|
|
645
|
+
*,
|
|
646
|
+
problem: Problem,
|
|
647
|
+
verbosity: int,
|
|
648
|
+
publisher: Publisher,
|
|
649
|
+
seed: int,
|
|
650
|
+
alpha: float = 0.5,
|
|
651
|
+
xover_probability: float = 1.0,
|
|
652
|
+
):
|
|
653
|
+
"""Initialize the blend alpha crossover operator.
|
|
654
|
+
|
|
655
|
+
Args:
|
|
656
|
+
problem (Problem): the problem object.
|
|
657
|
+
verbosity (int): the verbosity level of the component. The keys in `provided_topics` tell what
|
|
658
|
+
topics are provided by the operator at each verbosity level. Recommended to be set to 1.
|
|
659
|
+
publisher (Publisher): the publisher to which the operator will publish messages.
|
|
660
|
+
seed (int): the seed used in the random number generator for choosing the crossover point.
|
|
661
|
+
alpha (float, optional): non-negative blending factor 'alpha' that controls the extent to which
|
|
662
|
+
offspring may be sampled outside the interval defined by each pair of parent
|
|
663
|
+
genes. alpha = 0 restricts children strictly within the
|
|
664
|
+
parents range, larger alpha allows some outliers. Defaults to 0.5.
|
|
665
|
+
xover_probability (float, optional): the crossover probability parameter.
|
|
666
|
+
Ranges between 0 and 1.0. Defaults to 1.0.
|
|
667
|
+
"""
|
|
668
|
+
super().__init__(problem=problem, verbosity=verbosity, publisher=publisher)
|
|
669
|
+
|
|
670
|
+
if problem.variable_domain is not VariableDomainTypeEnum.continuous:
|
|
671
|
+
raise ValueError("BlendAlphaCrossover only works on continuous problems.")
|
|
672
|
+
|
|
673
|
+
if not 0 <= xover_probability <= 1:
|
|
674
|
+
raise ValueError("Crossover probability must be in [0,1].")
|
|
675
|
+
if alpha < 0:
|
|
676
|
+
raise ValueError("Alpha must be non-negative.")
|
|
677
|
+
|
|
678
|
+
self.alpha = alpha
|
|
679
|
+
self.xover_probability = xover_probability
|
|
680
|
+
self.seed = seed
|
|
681
|
+
self.rng = np.random.default_rng(self.seed)
|
|
682
|
+
|
|
683
|
+
self.parent_population: pl.DataFrame | None = None
|
|
684
|
+
self.offspring_population: pl.DataFrame | None = None
|
|
685
|
+
|
|
686
|
+
def do(
|
|
687
|
+
self,
|
|
688
|
+
*,
|
|
689
|
+
population: pl.DataFrame,
|
|
690
|
+
to_mate: list[int] | None = None,
|
|
691
|
+
) -> pl.DataFrame:
|
|
692
|
+
"""Perform BLX-alpha crossover.
|
|
693
|
+
|
|
694
|
+
Args:
|
|
695
|
+
population (pl.DataFrame): the population to perform the crossover with. The DataFrame
|
|
696
|
+
contains the decision vectors, the target vectors, and the constraint vectors.
|
|
697
|
+
to_mate (list[int] | None): the indices of the population members that should
|
|
698
|
+
participate in the crossover. If `None`, the whole population is subject
|
|
699
|
+
to the crossover.
|
|
700
|
+
|
|
701
|
+
Returns:
|
|
702
|
+
pl.DataFrame: the offspring resulting from the crossover.
|
|
703
|
+
"""
|
|
704
|
+
self.parent_population = population
|
|
705
|
+
pop_size = population.shape[0]
|
|
706
|
+
num_var = len(self.variable_symbols)
|
|
707
|
+
|
|
708
|
+
parent_decision_vars = population[self.variable_symbols].to_numpy()
|
|
709
|
+
if to_mate is None:
|
|
710
|
+
shuffled_ids = list(range(pop_size))
|
|
711
|
+
self.rng.shuffle(shuffled_ids)
|
|
712
|
+
else:
|
|
713
|
+
shuffled_ids = copy.copy(to_mate)
|
|
714
|
+
|
|
715
|
+
mating_pop_size = len(shuffled_ids)
|
|
716
|
+
original_pop_size = mating_pop_size
|
|
717
|
+
if mating_pop_size % 2 == 1:
|
|
718
|
+
shuffled_ids.append(shuffled_ids[0])
|
|
719
|
+
mating_pop_size += 1
|
|
720
|
+
|
|
721
|
+
mating_pop = parent_decision_vars[shuffled_ids]
|
|
722
|
+
|
|
723
|
+
parents1 = mating_pop[0::2, :]
|
|
724
|
+
parents2 = mating_pop[1::2, :]
|
|
725
|
+
|
|
726
|
+
c_min = np.array(self.lower_bounds)
|
|
727
|
+
c_max = np.array(self.upper_bounds)
|
|
728
|
+
span = c_max - c_min
|
|
729
|
+
|
|
730
|
+
lower = c_min - self.alpha * span
|
|
731
|
+
upper = c_max + self.alpha * span
|
|
732
|
+
|
|
733
|
+
uniform_1 = self.rng.random((mating_pop_size // 2, num_var))
|
|
734
|
+
uniform_2 = self.rng.random((mating_pop_size // 2, num_var))
|
|
735
|
+
|
|
736
|
+
offspring1 = lower + uniform_1 * (upper - lower)
|
|
737
|
+
offspring2 = lower + uniform_2 * (upper - lower)
|
|
738
|
+
|
|
739
|
+
mask = self.rng.random(mating_pop_size // 2) > self.xover_probability
|
|
740
|
+
offspring1[mask, :] = parents1[mask, :]
|
|
741
|
+
offspring2[mask, :] = parents2[mask, :]
|
|
742
|
+
|
|
743
|
+
offspring = np.vstack((offspring1, offspring2))
|
|
744
|
+
if original_pop_size % 2 == 1:
|
|
745
|
+
offspring = offspring[:-1, :]
|
|
746
|
+
|
|
747
|
+
self.offspring_population = pl.from_numpy(offspring, schema=self.variable_symbols).select(
|
|
748
|
+
pl.all().cast(pl.Float64)
|
|
749
|
+
)
|
|
750
|
+
self.notify()
|
|
751
|
+
return self.offspring_population
|
|
752
|
+
|
|
753
|
+
def update(self, *_, **__):
|
|
754
|
+
"""Do nothing."""
|
|
755
|
+
|
|
756
|
+
def state(self) -> Sequence[Message]:
|
|
757
|
+
"""Return the state of the blend-alpha crossover operator."""
|
|
758
|
+
if self.parent_population is None:
|
|
759
|
+
return []
|
|
760
|
+
msgs: list[Message] = []
|
|
761
|
+
if self.verbosity >= 1:
|
|
762
|
+
msgs.append(
|
|
763
|
+
FloatMessage(
|
|
764
|
+
topic=CrossoverMessageTopics.XOVER_PROBABILITY,
|
|
765
|
+
source=self.__class__.__name__,
|
|
766
|
+
value=self.xover_probability,
|
|
767
|
+
)
|
|
768
|
+
)
|
|
769
|
+
msgs.append(
|
|
770
|
+
FloatMessage(
|
|
771
|
+
topic=CrossoverMessageTopics.ALPHA,
|
|
772
|
+
source=self.__class__.__name__,
|
|
773
|
+
value=self.alpha,
|
|
774
|
+
)
|
|
775
|
+
)
|
|
776
|
+
if self.verbosity >= 2: # noqa: PLR2004
|
|
777
|
+
msgs.extend(
|
|
778
|
+
[
|
|
779
|
+
PolarsDataFrameMessage(
|
|
780
|
+
topic=CrossoverMessageTopics.PARENTS,
|
|
781
|
+
source=self.__class__.__name__,
|
|
782
|
+
value=self.parent_population,
|
|
783
|
+
),
|
|
784
|
+
PolarsDataFrameMessage(
|
|
785
|
+
topic=CrossoverMessageTopics.OFFSPRINGS,
|
|
786
|
+
source=self.__class__.__name__,
|
|
787
|
+
value=self.offspring_population,
|
|
788
|
+
),
|
|
789
|
+
]
|
|
790
|
+
)
|
|
791
|
+
return msgs
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
class SingleArithmeticCrossover(BaseCrossover):
|
|
795
|
+
"""Single Arithmetic Crossover for continuous problems."""
|
|
796
|
+
|
|
797
|
+
@property
|
|
798
|
+
def provided_topics(self) -> dict[int, Sequence[CrossoverMessageTopics]]:
|
|
799
|
+
"""The message topics provided by the single arithmetic crossover operator."""
|
|
800
|
+
return {
|
|
801
|
+
0: [], # No topics for 0
|
|
802
|
+
1: [
|
|
803
|
+
CrossoverMessageTopics.XOVER_PROBABILITY, # Probability of crossover
|
|
804
|
+
],
|
|
805
|
+
2: [
|
|
806
|
+
CrossoverMessageTopics.XOVER_PROBABILITY, # Crossover probability
|
|
807
|
+
CrossoverMessageTopics.PARENTS, # Parents involved in crossover
|
|
808
|
+
CrossoverMessageTopics.OFFSPRINGS, # Offsprings created from crossover
|
|
809
|
+
],
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
@property
|
|
813
|
+
def interested_topics(self):
|
|
814
|
+
"""The message topics that the single arithmetic crossover operator is interested in."""
|
|
815
|
+
return []
|
|
816
|
+
|
|
817
|
+
def __init__(
|
|
818
|
+
self,
|
|
819
|
+
problem: Problem,
|
|
820
|
+
verbosity: int,
|
|
821
|
+
publisher: Publisher,
|
|
822
|
+
seed: int,
|
|
823
|
+
xover_probability: float = 1.0,
|
|
824
|
+
):
|
|
825
|
+
"""Initialize the single arithmetic crossover operator.
|
|
826
|
+
|
|
827
|
+
Args:
|
|
828
|
+
problem (Problem): the problem object.
|
|
829
|
+
verbosity (int): the verbosity level of the component. The keys in `provided_topics` tell what
|
|
830
|
+
topics are provided by the operator at each verbosity level. Recommended to be set to 1.
|
|
831
|
+
publisher (Publisher): the publisher to which the operator will publish messages.
|
|
832
|
+
xover_probability (float): probability of performing crossover.
|
|
833
|
+
seed (int): random seed for reproducibility.
|
|
834
|
+
"""
|
|
835
|
+
super().__init__(problem=problem, verbosity=verbosity, publisher=publisher)
|
|
836
|
+
|
|
837
|
+
if not 0 <= xover_probability <= 1:
|
|
838
|
+
raise ValueError("Crossover probability must be in [0, 1].")
|
|
839
|
+
|
|
840
|
+
self.xover_probability = xover_probability
|
|
841
|
+
self.seed = seed
|
|
842
|
+
self.parent_population: pl.DataFrame | None = None
|
|
843
|
+
self.offspring_population: pl.DataFrame | None = None
|
|
844
|
+
self.rng = np.random.default_rng(self.seed)
|
|
845
|
+
|
|
846
|
+
def do(self, *, population: pl.DataFrame, to_mate: list[int] | None = None) -> pl.DataFrame:
|
|
847
|
+
"""Perform Single Arithmetic Crossover.
|
|
848
|
+
|
|
849
|
+
Args:
|
|
850
|
+
population (pl.DataFrame): the population to perform the crossover with. The DataFrame
|
|
851
|
+
contains the decision vectors, the target vectors, and the constraint vectors.
|
|
852
|
+
to_mate (list[int] | None): the indices of the population members that should
|
|
853
|
+
participate in the crossover. If `None`, the whole population is subject
|
|
854
|
+
to the crossover.
|
|
855
|
+
|
|
856
|
+
Returns:
|
|
857
|
+
pl.DataFrame: the offspring resulting from the crossover.
|
|
858
|
+
"""
|
|
859
|
+
self.parent_population = population
|
|
860
|
+
pop_size = population.shape[0]
|
|
861
|
+
num_vars = len(self.variable_symbols)
|
|
862
|
+
|
|
863
|
+
parents = population[self.variable_symbols].to_numpy()
|
|
864
|
+
|
|
865
|
+
if to_mate is None:
|
|
866
|
+
mating_indices = list(range(pop_size))
|
|
867
|
+
self.rng.shuffle(mating_indices)
|
|
868
|
+
else:
|
|
869
|
+
mating_indices = copy.copy(to_mate)
|
|
870
|
+
|
|
871
|
+
mating_pop_size = len(mating_indices)
|
|
872
|
+
original_pop_size = mating_pop_size
|
|
873
|
+
|
|
874
|
+
if mating_pop_size % 2 == 1:
|
|
875
|
+
mating_indices.append(mating_indices[0])
|
|
876
|
+
mating_pop_size += 1
|
|
877
|
+
|
|
878
|
+
mating_pool = parents[mating_indices, :]
|
|
879
|
+
|
|
880
|
+
parents1 = mating_pool[0::2, :]
|
|
881
|
+
parents2 = mating_pool[1::2, :]
|
|
882
|
+
|
|
883
|
+
mask = self.rng.random(mating_pop_size // 2) <= self.xover_probability
|
|
884
|
+
gene_pos = self.rng.integers(0, num_vars, size=mating_pop_size // 2)
|
|
885
|
+
|
|
886
|
+
# Initialize offspring as exact copies
|
|
887
|
+
offspring1 = parents1.copy()
|
|
888
|
+
offspring2 = parents2.copy()
|
|
889
|
+
|
|
890
|
+
# Apply crossover only for selected pairs
|
|
891
|
+
row_idx = np.arange(len(mask))[mask]
|
|
892
|
+
col_idx = gene_pos[mask]
|
|
893
|
+
|
|
894
|
+
avg = 0.5 * (parents1[row_idx, col_idx] + parents2[row_idx, col_idx])
|
|
895
|
+
|
|
896
|
+
# Use advanced indexing to set arithmetic crossover gene
|
|
897
|
+
offspring1[row_idx, col_idx] = avg
|
|
898
|
+
offspring2[row_idx, col_idx] = avg
|
|
899
|
+
|
|
900
|
+
for i, k in zip(row_idx, col_idx, strict=True):
|
|
901
|
+
offspring1[i, k + 1 :] = parents2[i, k + 1 :]
|
|
902
|
+
offspring2[i, k + 1 :] = parents1[i, k + 1 :]
|
|
903
|
+
offspring1[i, :k] = parents1[i, :k]
|
|
904
|
+
offspring2[i, :k] = parents2[i, :k]
|
|
905
|
+
|
|
906
|
+
offspring = np.vstack((offspring1, offspring2))
|
|
907
|
+
if original_pop_size % 2 == 1:
|
|
908
|
+
offspring = offspring[:-1, :]
|
|
909
|
+
|
|
910
|
+
self.offspring_population = pl.from_numpy(offspring, schema=self.variable_symbols).select(
|
|
911
|
+
pl.all().cast(pl.Float64)
|
|
912
|
+
)
|
|
913
|
+
self.notify()
|
|
914
|
+
return self.offspring_population
|
|
915
|
+
|
|
916
|
+
def update(self, *_, **__):
|
|
917
|
+
"""Do nothing."""
|
|
918
|
+
|
|
919
|
+
def state(self) -> Sequence[Message]:
|
|
920
|
+
"""Return the state of the single arithmetic crossover operator."""
|
|
921
|
+
if self.parent_population is None:
|
|
922
|
+
return []
|
|
923
|
+
|
|
924
|
+
msgs: list[Message] = []
|
|
925
|
+
|
|
926
|
+
# Messages for crossover probability
|
|
927
|
+
if self.verbosity >= 1:
|
|
928
|
+
msgs.append(
|
|
929
|
+
FloatMessage(
|
|
930
|
+
topic=CrossoverMessageTopics.XOVER_PROBABILITY,
|
|
931
|
+
source=self.__class__.__name__,
|
|
932
|
+
value=self.xover_probability,
|
|
933
|
+
)
|
|
934
|
+
)
|
|
935
|
+
|
|
936
|
+
# Messages for parents and offspring
|
|
937
|
+
if self.verbosity >= 2: # More detailed info
|
|
938
|
+
msgs.extend(
|
|
939
|
+
[
|
|
940
|
+
PolarsDataFrameMessage(
|
|
941
|
+
topic=CrossoverMessageTopics.PARENTS,
|
|
942
|
+
source=self.__class__.__name__,
|
|
943
|
+
value=self.parent_population,
|
|
944
|
+
),
|
|
945
|
+
PolarsDataFrameMessage(
|
|
946
|
+
topic=CrossoverMessageTopics.OFFSPRINGS,
|
|
947
|
+
source=self.__class__.__name__,
|
|
948
|
+
value=self.offspring_population,
|
|
949
|
+
),
|
|
950
|
+
]
|
|
951
|
+
)
|
|
952
|
+
|
|
953
|
+
return msgs
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
class LocalCrossover(BaseCrossover):
|
|
957
|
+
"""Local Crossover for continuous problems."""
|
|
958
|
+
|
|
959
|
+
@property
|
|
960
|
+
def provided_topics(self) -> dict[int, Sequence[CrossoverMessageTopics]]:
|
|
961
|
+
"""The message topics provided by the local crossover operator."""
|
|
962
|
+
return {
|
|
963
|
+
0: [],
|
|
964
|
+
1: [
|
|
965
|
+
CrossoverMessageTopics.XOVER_PROBABILITY,
|
|
966
|
+
],
|
|
967
|
+
2: [
|
|
968
|
+
CrossoverMessageTopics.XOVER_PROBABILITY,
|
|
969
|
+
CrossoverMessageTopics.PARENTS,
|
|
970
|
+
CrossoverMessageTopics.OFFSPRINGS,
|
|
971
|
+
],
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
@property
|
|
975
|
+
def interested_topics(self):
|
|
976
|
+
"""The message topics that the local crossover operator is interested in."""
|
|
977
|
+
return []
|
|
978
|
+
|
|
979
|
+
def __init__(
|
|
980
|
+
self,
|
|
981
|
+
problem: Problem,
|
|
982
|
+
verbosity: int,
|
|
983
|
+
publisher: Publisher,
|
|
984
|
+
seed: int,
|
|
985
|
+
xover_probability: float = 1.0,
|
|
986
|
+
):
|
|
987
|
+
"""Initialize the local crossover operator.
|
|
988
|
+
|
|
989
|
+
Args:
|
|
990
|
+
problem (Problem): the problem object.
|
|
991
|
+
verbosity (int): the verbosity level of the component. The keys in `provided_topics` tell what
|
|
992
|
+
topics are provided by the operator at each verbosity level. Recommended to be set to 1.
|
|
993
|
+
publisher (Publisher): the publisher to which the operator will publish messages.
|
|
994
|
+
xover_probability (float): probability of performing crossover.
|
|
995
|
+
seed (int): random seed for reproducibility.
|
|
996
|
+
"""
|
|
997
|
+
super().__init__(problem=problem, verbosity=verbosity, publisher=publisher)
|
|
998
|
+
|
|
999
|
+
if not 0 <= xover_probability <= 1:
|
|
1000
|
+
raise ValueError("Crossover probability must be in [0, 1].")
|
|
1001
|
+
|
|
1002
|
+
self.xover_probability = xover_probability
|
|
1003
|
+
self.seed = seed
|
|
1004
|
+
self.rng = np.random.default_rng(self.seed)
|
|
1005
|
+
self.parent_population: pl.DataFrame | None = None
|
|
1006
|
+
self.offspring_population: pl.DataFrame | None = None
|
|
1007
|
+
|
|
1008
|
+
def do(self, *, population: pl.DataFrame, to_mate: list[int] | None = None) -> pl.DataFrame:
|
|
1009
|
+
"""Perform Local Crossover.
|
|
1010
|
+
|
|
1011
|
+
Args:
|
|
1012
|
+
population (pl.DataFrame): the population to perform the crossover with. The DataFrame
|
|
1013
|
+
contains the decision vectors, the target vectors, and the constraint vectors.
|
|
1014
|
+
to_mate (list[int] | None): the indices of the population members that should
|
|
1015
|
+
participate in the crossover. If `None`, the whole population is subject
|
|
1016
|
+
to the crossover.
|
|
1017
|
+
|
|
1018
|
+
Returns:
|
|
1019
|
+
pl.DataFrame: the offspring resulting from the crossover.
|
|
1020
|
+
"""
|
|
1021
|
+
self.parent_population = population
|
|
1022
|
+
pop_size = population.shape[0]
|
|
1023
|
+
num_var = len(self.variable_symbols)
|
|
1024
|
+
|
|
1025
|
+
parent_decision_vars = population[self.variable_symbols].to_numpy()
|
|
1026
|
+
|
|
1027
|
+
if to_mate is None:
|
|
1028
|
+
shuffled_ids = list(range(pop_size))
|
|
1029
|
+
self.rng.shuffle(shuffled_ids)
|
|
1030
|
+
else:
|
|
1031
|
+
shuffled_ids = to_mate.copy()
|
|
1032
|
+
|
|
1033
|
+
mating_pop_size = len(shuffled_ids)
|
|
1034
|
+
if mating_pop_size % 2 == 1:
|
|
1035
|
+
shuffled_ids.append(shuffled_ids[0])
|
|
1036
|
+
mating_pop_size += 1
|
|
1037
|
+
|
|
1038
|
+
mating_pop = parent_decision_vars[shuffled_ids]
|
|
1039
|
+
parents1 = mating_pop[0::2]
|
|
1040
|
+
parents2 = mating_pop[1::2]
|
|
1041
|
+
|
|
1042
|
+
offspring = np.empty((mating_pop_size, num_var))
|
|
1043
|
+
|
|
1044
|
+
for i in range(mating_pop_size // 2):
|
|
1045
|
+
if self.rng.random() < self.xover_probability:
|
|
1046
|
+
alpha = self.rng.random(num_var)
|
|
1047
|
+
|
|
1048
|
+
offspring[2 * i] = alpha * parents1[i] + (1 - alpha) * parents2[i]
|
|
1049
|
+
offspring[2 * i + 1] = (1 - alpha) * parents1[i] + alpha * parents2[i]
|
|
1050
|
+
else:
|
|
1051
|
+
offspring[2 * i] = parents1[i]
|
|
1052
|
+
offspring[2 * i + 1] = parents2[i]
|
|
1053
|
+
|
|
1054
|
+
self.offspring_population = pl.from_numpy(offspring, schema=self.variable_symbols).select(
|
|
1055
|
+
pl.all().cast(pl.Float64)
|
|
1056
|
+
)
|
|
1057
|
+
|
|
1058
|
+
self.notify()
|
|
1059
|
+
return self.offspring_population
|
|
1060
|
+
|
|
1061
|
+
def update(self, *_, **__):
|
|
1062
|
+
"""Do nothing."""
|
|
1063
|
+
|
|
1064
|
+
def state(self) -> Sequence[Message]:
|
|
1065
|
+
"""Return the state of the local crossover operator."""
|
|
1066
|
+
if self.parent_population is None:
|
|
1067
|
+
return []
|
|
1068
|
+
|
|
1069
|
+
msgs: list[Message] = []
|
|
1070
|
+
|
|
1071
|
+
if self.verbosity >= 1:
|
|
1072
|
+
msgs.append(
|
|
1073
|
+
FloatMessage(
|
|
1074
|
+
topic=CrossoverMessageTopics.XOVER_PROBABILITY,
|
|
1075
|
+
source=self.__class__.__name__,
|
|
1076
|
+
value=self.xover_probability,
|
|
1077
|
+
)
|
|
1078
|
+
)
|
|
1079
|
+
if self.verbosity >= 2:
|
|
1080
|
+
msgs.extend(
|
|
1081
|
+
[
|
|
1082
|
+
PolarsDataFrameMessage(
|
|
1083
|
+
topic=CrossoverMessageTopics.PARENTS,
|
|
1084
|
+
source=self.__class__.__name__,
|
|
1085
|
+
value=self.parent_population,
|
|
1086
|
+
),
|
|
1087
|
+
PolarsDataFrameMessage(
|
|
1088
|
+
topic=CrossoverMessageTopics.OFFSPRINGS,
|
|
1089
|
+
source=self.__class__.__name__,
|
|
1090
|
+
value=self.offspring_population,
|
|
1091
|
+
),
|
|
1092
|
+
]
|
|
1093
|
+
)
|
|
1094
|
+
return msgs
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
class BoundedExponentialCrossover(BaseCrossover):
|
|
1098
|
+
"""Bounded‐exponential (BEX) crossover for continuous problems."""
|
|
1099
|
+
|
|
1100
|
+
@property
|
|
1101
|
+
def provided_topics(self) -> dict[int, Sequence[CrossoverMessageTopics]]:
|
|
1102
|
+
"""The message topics provided by the bounded exponential crossover operator."""
|
|
1103
|
+
return {
|
|
1104
|
+
0: [],
|
|
1105
|
+
1: [
|
|
1106
|
+
CrossoverMessageTopics.XOVER_PROBABILITY,
|
|
1107
|
+
CrossoverMessageTopics.LAMBDA,
|
|
1108
|
+
],
|
|
1109
|
+
2: [
|
|
1110
|
+
CrossoverMessageTopics.XOVER_PROBABILITY,
|
|
1111
|
+
CrossoverMessageTopics.LAMBDA,
|
|
1112
|
+
CrossoverMessageTopics.PARENTS,
|
|
1113
|
+
CrossoverMessageTopics.OFFSPRINGS,
|
|
1114
|
+
],
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
@property
|
|
1118
|
+
def interested_topics(self):
|
|
1119
|
+
"""The message topics provided by the bounded exponential crossover operator."""
|
|
1120
|
+
return []
|
|
1121
|
+
|
|
1122
|
+
def __init__(
|
|
1123
|
+
self,
|
|
1124
|
+
*,
|
|
1125
|
+
problem: Problem,
|
|
1126
|
+
verbosity: int,
|
|
1127
|
+
publisher: Publisher,
|
|
1128
|
+
seed: int,
|
|
1129
|
+
lambda_: float = 1.0,
|
|
1130
|
+
xover_probability: float = 1.0,
|
|
1131
|
+
):
|
|
1132
|
+
"""Initialize the bounded‐exponential crossover operator.
|
|
1133
|
+
|
|
1134
|
+
Args:
|
|
1135
|
+
problem (Problem): the problem object.
|
|
1136
|
+
verbosity (int): the verbosity level of the component. The keys in `provided_topics` tell what
|
|
1137
|
+
topics are provided by the operator at each verbosity level. Recommended to be set to 1.
|
|
1138
|
+
publisher (Publisher): the publisher to which the operator will publish messages.
|
|
1139
|
+
seed (int): random seed for the internal generator.
|
|
1140
|
+
lambda_ (float, optional): positive scale λ for the exponential distribution.
|
|
1141
|
+
Defaults to 1.0.
|
|
1142
|
+
xover_probability (float, optional): probability of applying crossover
|
|
1143
|
+
to each pair. Defaults to 1.0.
|
|
1144
|
+
"""
|
|
1145
|
+
super().__init__(problem=problem, verbosity=verbosity, publisher=publisher)
|
|
1146
|
+
|
|
1147
|
+
if problem.variable_domain is not VariableDomainTypeEnum.continuous:
|
|
1148
|
+
raise ValueError("BoundedExponentialCrossover only works on continuous problems.")
|
|
1149
|
+
if lambda_ <= 0:
|
|
1150
|
+
raise ValueError("lambda_ must be positive.")
|
|
1151
|
+
if not 0 <= xover_probability <= 1:
|
|
1152
|
+
raise ValueError("xover_probability must be in [0,1].")
|
|
1153
|
+
|
|
1154
|
+
self.lambda_ = lambda_
|
|
1155
|
+
self.xover_probability = xover_probability
|
|
1156
|
+
self.seed = seed
|
|
1157
|
+
self.rng = np.random.default_rng(self.seed)
|
|
1158
|
+
|
|
1159
|
+
self.parent_population: pl.DataFrame | None = None
|
|
1160
|
+
self.offspring_population: pl.DataFrame | None = None
|
|
1161
|
+
|
|
1162
|
+
def do(
|
|
1163
|
+
self,
|
|
1164
|
+
*,
|
|
1165
|
+
population: pl.DataFrame,
|
|
1166
|
+
to_mate: list[int] | None = None,
|
|
1167
|
+
) -> pl.DataFrame:
|
|
1168
|
+
"""Perform bounded‐exponential crossover.
|
|
1169
|
+
|
|
1170
|
+
Args:
|
|
1171
|
+
population (pl.DataFrame): the population to perform the crossover with. The DataFrame
|
|
1172
|
+
contains the decision vectors, the target vectors, and the constraint vectors.
|
|
1173
|
+
to_mate (list[int] | None): the indices of the population members that should
|
|
1174
|
+
participate in the crossover. If `None`, the whole population is subject
|
|
1175
|
+
to the crossover.
|
|
1176
|
+
|
|
1177
|
+
Returns:
|
|
1178
|
+
pl.DataFrame: the offspring resulting from the crossover.
|
|
1179
|
+
"""
|
|
1180
|
+
self.parent_population = population
|
|
1181
|
+
pop_size = population.shape[0]
|
|
1182
|
+
num_var = len(self.variable_symbols)
|
|
1183
|
+
|
|
1184
|
+
parent_decision_vars = population[self.variable_symbols].to_numpy()
|
|
1185
|
+
if to_mate is None:
|
|
1186
|
+
shuffled_ids = list(range(pop_size))
|
|
1187
|
+
self.rng.shuffle(shuffled_ids)
|
|
1188
|
+
else:
|
|
1189
|
+
shuffled_ids = copy.copy(to_mate)
|
|
1190
|
+
|
|
1191
|
+
mating_pop_size = len(shuffled_ids)
|
|
1192
|
+
original_pop_size = mating_pop_size
|
|
1193
|
+
if mating_pop_size % 2 == 1:
|
|
1194
|
+
shuffled_ids.append(shuffled_ids[0])
|
|
1195
|
+
mating_pop_size += 1
|
|
1196
|
+
|
|
1197
|
+
mating_pop = parent_decision_vars[shuffled_ids]
|
|
1198
|
+
|
|
1199
|
+
parents1 = mating_pop[0::2, :]
|
|
1200
|
+
parents2 = mating_pop[1::2, :]
|
|
1201
|
+
|
|
1202
|
+
x_lower = np.array(self.lower_bounds)
|
|
1203
|
+
x_upper = np.array(self.upper_bounds)
|
|
1204
|
+
span = parents2 - parents1 # y_i - x_1
|
|
1205
|
+
|
|
1206
|
+
u_i = self.rng.random((mating_pop_size // 2, num_var)) # random integers
|
|
1207
|
+
r_i = self.rng.random((mating_pop_size // 2, num_var))
|
|
1208
|
+
|
|
1209
|
+
exp_lower_1 = np.exp((x_lower - parents1) / (self.lambda_ * span))
|
|
1210
|
+
exp_upper_1 = np.exp((parents1 - x_upper) / (self.lambda_ * span))
|
|
1211
|
+
|
|
1212
|
+
exp_lower_2 = np.exp((x_lower - parents2) / (self.lambda_ * span))
|
|
1213
|
+
exp_upper_2 = np.exp((parents2 - x_upper) / (self.lambda_ * span))
|
|
1214
|
+
|
|
1215
|
+
beta_1 = np.where(
|
|
1216
|
+
r_i <= 0.5,
|
|
1217
|
+
self.lambda_ * np.log(exp_lower_1 + u_i * (1 - exp_lower_1)),
|
|
1218
|
+
-self.lambda_ * np.log(1 - u_i * (1 - exp_upper_1)),
|
|
1219
|
+
)
|
|
1220
|
+
|
|
1221
|
+
beta_2 = np.where(
|
|
1222
|
+
r_i <= 0.5,
|
|
1223
|
+
self.lambda_ * np.log(exp_lower_2 + u_i * (1 - exp_lower_2)),
|
|
1224
|
+
-self.lambda_ * np.log(1 - u_i * (1 - exp_upper_2)),
|
|
1225
|
+
)
|
|
1226
|
+
|
|
1227
|
+
offspring1 = parents1 + beta_1 * span
|
|
1228
|
+
offspring2 = parents2 + beta_2 * span
|
|
1229
|
+
|
|
1230
|
+
mask = self.rng.random(mating_pop_size // 2) > self.xover_probability
|
|
1231
|
+
offspring1[mask, :] = parents1[mask, :]
|
|
1232
|
+
offspring2[mask, :] = parents2[mask, :]
|
|
1233
|
+
|
|
1234
|
+
children = np.vstack((offspring1, offspring2))
|
|
1235
|
+
if original_pop_size % 2 == 1:
|
|
1236
|
+
children = children[:-1, :]
|
|
1237
|
+
|
|
1238
|
+
self.offspring_population = pl.from_numpy(children, schema=self.variable_symbols).select(
|
|
1239
|
+
pl.all().cast(pl.Float64)
|
|
1240
|
+
)
|
|
1241
|
+
self.notify()
|
|
1242
|
+
return self.offspring_population
|
|
1243
|
+
|
|
1244
|
+
def update(self, *_, **__):
|
|
1245
|
+
"""Do nothing."""
|
|
1246
|
+
|
|
1247
|
+
def state(self) -> Sequence[Message]:
|
|
1248
|
+
"""Return the state of the crossover operator."""
|
|
1249
|
+
if getattr(self, "parent_population", None) is None:
|
|
1250
|
+
return []
|
|
1251
|
+
msgs: list[Message] = []
|
|
1252
|
+
if self.verbosity >= 1:
|
|
1253
|
+
msgs.append(
|
|
1254
|
+
FloatMessage(
|
|
1255
|
+
topic=CrossoverMessageTopics.XOVER_PROBABILITY,
|
|
1256
|
+
source=self.__class__.__name__,
|
|
1257
|
+
value=self.xover_probability,
|
|
1258
|
+
)
|
|
1259
|
+
)
|
|
1260
|
+
msgs.append(
|
|
1261
|
+
FloatMessage(
|
|
1262
|
+
topic=CrossoverMessageTopics.LAMBDA,
|
|
1263
|
+
source=self.__class__.__name__,
|
|
1264
|
+
value=self.lambda_,
|
|
1265
|
+
)
|
|
1266
|
+
)
|
|
1267
|
+
if self.verbosity >= 2:
|
|
1268
|
+
msgs.extend(
|
|
1269
|
+
[
|
|
1270
|
+
PolarsDataFrameMessage(
|
|
1271
|
+
topic=CrossoverMessageTopics.PARENTS,
|
|
1272
|
+
source=self.__class__.__name__,
|
|
1273
|
+
value=self.parent_population,
|
|
1274
|
+
),
|
|
1275
|
+
PolarsDataFrameMessage(
|
|
1276
|
+
topic=CrossoverMessageTopics.OFFSPRINGS,
|
|
1277
|
+
source=self.__class__.__name__,
|
|
1278
|
+
value=self.offspring_population,
|
|
1279
|
+
),
|
|
1280
|
+
]
|
|
1281
|
+
)
|
|
1282
|
+
return msgs
|