evograd-diff 0.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.
- evograd/__init__.py +67 -0
- evograd/algorithms/__init__.py +138 -0
- evograd/algorithms/cmaes.py +1365 -0
- evograd/algorithms/de.py +895 -0
- evograd/algorithms/ga.py +532 -0
- evograd/algorithms/pso.py +648 -0
- evograd/algorithms/shade.py +1165 -0
- evograd/benchmarks/functions/__init__.py +229 -0
- evograd/benchmarks/functions/base.py +217 -0
- evograd/benchmarks/functions/cec2017/__init__.py +250 -0
- evograd/benchmarks/functions/cec2017/basic.py +413 -0
- evograd/benchmarks/functions/cec2017/composition.py +580 -0
- evograd/benchmarks/functions/cec2017/data.pkl +0 -0
- evograd/benchmarks/functions/cec2017/data.py +350 -0
- evograd/benchmarks/functions/cec2017/hybrid.py +406 -0
- evograd/benchmarks/functions/cec2017/simple.py +326 -0
- evograd/benchmarks/functions/classical.py +649 -0
- evograd/benchmarks/functions/smoothed_funnel.py +476 -0
- evograd/benchmarks/functions/transforms.py +463 -0
- evograd/benchmarks/run_benchmark_functions.py +1208 -0
- evograd/core/__init__.py +73 -0
- evograd/core/algorithm.py +778 -0
- evograd/core/maximize.py +269 -0
- evograd/core/minimize.py +740 -0
- evograd/core/problem.py +444 -0
- evograd/core/result.py +571 -0
- evograd/core/termination.py +602 -0
- evograd/operators/__init__.py +178 -0
- evograd/operators/crossover.py +1117 -0
- evograd/operators/mutation.py +1098 -0
- evograd/operators/relaxations.py +175 -0
- evograd/operators/repair.py +601 -0
- evograd/operators/sampling.py +577 -0
- evograd/operators/selection.py +981 -0
- evograd/operators/survival.py +1000 -0
- evograd/tests/__init__.py +11 -0
- evograd/tests/run_all.py +78 -0
- evograd/tests/test_core.py +528 -0
- evograd/tests/test_ga.py +572 -0
- evograd/tests/test_operators.py +662 -0
- evograd/tests/test_per_individual.py +326 -0
- evograd/tests/test_utils.py +328 -0
- evograd/utils/__init__.py +97 -0
- evograd/utils/callbacks.py +926 -0
- evograd/utils/device.py +502 -0
- evograd/utils/duplicates.py +421 -0
- evograd_diff-0.1.0.dist-info/METADATA +439 -0
- evograd_diff-0.1.0.dist-info/RECORD +50 -0
- evograd_diff-0.1.0.dist-info/WHEEL +4 -0
- evograd_diff-0.1.0.dist-info/licenses/LICENSE +201 -0
evograd/algorithms/de.py
ADDED
|
@@ -0,0 +1,895 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Differential Evolution (DE) implementation for EvoGrad.
|
|
3
|
+
|
|
4
|
+
This module provides a fully differentiable Differential Evolution algorithm
|
|
5
|
+
that supports both classical and gradient-enabled optimisation modes.
|
|
6
|
+
|
|
7
|
+
DE evolves a population through:
|
|
8
|
+
1. Mutation: Create donor vectors using difference of population members
|
|
9
|
+
2. Crossover: Combine target and donor to create trial vectors
|
|
10
|
+
3. Selection: Greedy one-to-one replacement
|
|
11
|
+
|
|
12
|
+
All operators are pluggable via dependency injection (pymoo-style). The
|
|
13
|
+
crossover operator uses the existing BinomialCrossover or ExponentialCrossover
|
|
14
|
+
from the operators module.
|
|
15
|
+
|
|
16
|
+
Variants:
|
|
17
|
+
The variant string (e.g., "DE/rand/1/bin") specifies:
|
|
18
|
+
- Mutation base: rand, best, current-to-best, current-to-rand
|
|
19
|
+
- Number of difference vectors: 1 or 2
|
|
20
|
+
- Crossover type: bin (binomial) or exp (exponential)
|
|
21
|
+
|
|
22
|
+
Modes:
|
|
23
|
+
- adaptive=False, differentiable=False: Classical DE
|
|
24
|
+
- adaptive=True, differentiable=False: Operators are differentiable,
|
|
25
|
+
hyperparameters (F, CR, temperatures) learned via backprop
|
|
26
|
+
- adaptive=False, differentiable=True: Population is differentiable,
|
|
27
|
+
learned via backprop
|
|
28
|
+
- adaptive=True, differentiable=True: Both operators and population
|
|
29
|
+
are differentiable
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
>>> from evograd.algorithms import DE
|
|
33
|
+
>>> from evograd.core import Problem, minimize
|
|
34
|
+
>>>
|
|
35
|
+
>>> problem = Problem(
|
|
36
|
+
... objective=lambda x: (x**2).sum(dim=-1),
|
|
37
|
+
... n_var=30,
|
|
38
|
+
... xl=-100.0,
|
|
39
|
+
... xu=100.0,
|
|
40
|
+
... )
|
|
41
|
+
>>>
|
|
42
|
+
>>> # Classical DE
|
|
43
|
+
>>> de = DE(pop_size=100, variant="DE/rand/1/bin", F=0.5, CR=0.9)
|
|
44
|
+
>>> result = minimize(problem, de, max_evals=10000)
|
|
45
|
+
>>>
|
|
46
|
+
>>> # Adaptive DE with learnable hyperparameters
|
|
47
|
+
>>> de = DE(pop_size=100, variant="DE/best/1/bin", adaptive=True)
|
|
48
|
+
>>> result = minimize(problem, de, max_evals=10000)
|
|
49
|
+
|
|
50
|
+
Reference:
|
|
51
|
+
Storn, R. & Price, K. (1997). Differential Evolution - A Simple and
|
|
52
|
+
Efficient Heuristic for Global Optimization over Continuous Spaces.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
from __future__ import annotations
|
|
56
|
+
|
|
57
|
+
import re
|
|
58
|
+
from dataclasses import dataclass
|
|
59
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
|
|
60
|
+
|
|
61
|
+
import torch
|
|
62
|
+
import torch.nn as nn
|
|
63
|
+
from torch import Tensor
|
|
64
|
+
|
|
65
|
+
from evograd.core.algorithm import Algorithm
|
|
66
|
+
|
|
67
|
+
if TYPE_CHECKING:
|
|
68
|
+
from evograd.core.problem import Problem
|
|
69
|
+
|
|
70
|
+
__all__ = ["DE", "DEVariant", "de_default", "de_rand_1_bin", "de_best_1_bin", "de_current_to_best_1_bin"]
|
|
71
|
+
|
|
72
|
+
# =============================================================================
|
|
73
|
+
# DE Variant Parser
|
|
74
|
+
# =============================================================================
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class DEVariant:
|
|
78
|
+
"""
|
|
79
|
+
Parsed DE variant specification.
|
|
80
|
+
|
|
81
|
+
Attributes:
|
|
82
|
+
mutation: Mutation strategy (rand, best, current-to-best, current-to-rand)
|
|
83
|
+
n_diff: Number of difference vectors (1 or 2)
|
|
84
|
+
crossover: Crossover type (bin, exp, or None for current-to-rand)
|
|
85
|
+
"""
|
|
86
|
+
mutation: str
|
|
87
|
+
n_diff: int
|
|
88
|
+
crossover: Optional[str]
|
|
89
|
+
|
|
90
|
+
# Pattern: DE/mutation/n_diff/crossover
|
|
91
|
+
_PATTERN = re.compile(
|
|
92
|
+
r"^DE/(RAND|BEST|CURRENT-TO-BEST|CURRENT-TO-RAND)/([12])(?:/(BIN|EXP))?$",
|
|
93
|
+
re.IGNORECASE
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def parse(cls, variant: str) -> "DEVariant":
|
|
98
|
+
"""
|
|
99
|
+
Parse a DE variant string.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
variant: Variant string like "DE/rand/1/bin"
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Parsed DEVariant instance.
|
|
106
|
+
|
|
107
|
+
Raises:
|
|
108
|
+
ValueError: If variant string is invalid.
|
|
109
|
+
"""
|
|
110
|
+
# Normalise: replace underscores with hyphens
|
|
111
|
+
normalised = variant.replace("_", "-").upper()
|
|
112
|
+
|
|
113
|
+
match = cls._PATTERN.match(normalised)
|
|
114
|
+
if not match:
|
|
115
|
+
valid = [
|
|
116
|
+
"DE/rand/1/bin", "DE/rand/1/exp", "DE/rand/2/bin", "DE/rand/2/exp",
|
|
117
|
+
"DE/best/1/bin", "DE/best/1/exp", "DE/best/2/bin", "DE/best/2/exp",
|
|
118
|
+
"DE/current-to-best/1/bin", "DE/current-to-best/1/exp",
|
|
119
|
+
"DE/current-to-rand/1"
|
|
120
|
+
]
|
|
121
|
+
raise ValueError(
|
|
122
|
+
f"Invalid DE variant '{variant}'. "
|
|
123
|
+
f"Valid variants: {', '.join(valid)}"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
mutation = match.group(1).lower().replace("-", "_")
|
|
127
|
+
n_diff = int(match.group(2))
|
|
128
|
+
crossover = match.group(3).lower() if match.group(3) else None
|
|
129
|
+
|
|
130
|
+
# current-to-rand doesn't use crossover
|
|
131
|
+
if mutation == "current_to_rand" and crossover is not None:
|
|
132
|
+
raise ValueError(
|
|
133
|
+
f"DE/current-to-rand does not use crossover. "
|
|
134
|
+
f"Use 'DE/current-to-rand/1' without crossover suffix."
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Other variants require crossover
|
|
138
|
+
if mutation != "current_to_rand" and crossover is None:
|
|
139
|
+
raise ValueError(
|
|
140
|
+
f"Variant '{variant}' requires crossover type. "
|
|
141
|
+
f"Use 'DE/{mutation}/{n_diff}/bin' or 'DE/{mutation}/{n_diff}/exp'."
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return cls(mutation=mutation, n_diff=n_diff, crossover=crossover)
|
|
145
|
+
|
|
146
|
+
def __str__(self) -> str:
|
|
147
|
+
mutation_str = self.mutation.replace("_", "-")
|
|
148
|
+
if self.crossover:
|
|
149
|
+
return f"DE/{mutation_str}/{self.n_diff}/{self.crossover}"
|
|
150
|
+
return f"DE/{mutation_str}/{self.n_diff}"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# =============================================================================
|
|
154
|
+
# Differential Evolution Algorithm
|
|
155
|
+
# =============================================================================
|
|
156
|
+
|
|
157
|
+
class DE(Algorithm):
|
|
158
|
+
"""
|
|
159
|
+
Differential Evolution (DE) for continuous optimisation.
|
|
160
|
+
|
|
161
|
+
DE evolves a population through mutation (using difference vectors),
|
|
162
|
+
crossover, and greedy selection. Supports multiple mutation strategies
|
|
163
|
+
and both binomial and exponential crossover.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
pop_size: Population size.
|
|
167
|
+
variant: DE variant string (e.g., "DE/rand/1/bin").
|
|
168
|
+
See DEVariant for valid options.
|
|
169
|
+
F: Mutation scale factor in (0, 2]. Default: 0.5.
|
|
170
|
+
CR: Crossover rate in [0, 1]. Default: 0.9.
|
|
171
|
+
sampling: Operator for initial population generation.
|
|
172
|
+
crossover: Crossover operator. If None, created from variant.
|
|
173
|
+
repair: Repair operator for constraint handling.
|
|
174
|
+
dither: F randomisation strategy (classical mode only):
|
|
175
|
+
- None: Fixed F
|
|
176
|
+
- "scalar": Randomise F once per generation
|
|
177
|
+
- "vector": Randomise F per individual
|
|
178
|
+
jitter: If True, add small per-dimension noise to F (classical only).
|
|
179
|
+
per_individual_coeffs: If True, sample F and CR independently for
|
|
180
|
+
each individual. In classical mode, sampled from Uniform(0.5, 1.0).
|
|
181
|
+
In adaptive mode, sampled around the learned base values using
|
|
182
|
+
reparameterization (gradients flow to learned parameters).
|
|
183
|
+
adaptive: If True, operators are differentiable and hyperparameters
|
|
184
|
+
(F, CR, temperatures) are learned via backpropagation.
|
|
185
|
+
differentiable: If True, population is differentiable and
|
|
186
|
+
learned via backpropagation.
|
|
187
|
+
selection_temperature: Initial temperature for Gumbel-Softmax selection.
|
|
188
|
+
dtype: Tensor dtype.
|
|
189
|
+
|
|
190
|
+
Attributes:
|
|
191
|
+
variant: Parsed DEVariant.
|
|
192
|
+
F: Current mutation scale factor.
|
|
193
|
+
CR: Current crossover rate.
|
|
194
|
+
|
|
195
|
+
Example:
|
|
196
|
+
>>> # Classical DE/rand/1/bin
|
|
197
|
+
>>> de = DE(pop_size=100, variant="DE/rand/1/bin")
|
|
198
|
+
>>>
|
|
199
|
+
>>> # Adaptive DE with learnable hyperparameters
|
|
200
|
+
>>> de = DE(variant="DE/best/1/bin", adaptive=True)
|
|
201
|
+
>>>
|
|
202
|
+
>>> # Differentiable population
|
|
203
|
+
>>> de = DE(variant="DE/rand/1/bin", differentiable=True)
|
|
204
|
+
>>>
|
|
205
|
+
>>> # Both adaptive and differentiable
|
|
206
|
+
>>> de = DE(variant="DE/current-to-best/1/bin", adaptive=True, differentiable=True)
|
|
207
|
+
>>>
|
|
208
|
+
>>> # Per-individual F and CR (jDE-style)
|
|
209
|
+
>>> de = DE(variant="DE/rand/1/bin", per_individual_coeffs=True)
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
def __init__(
|
|
213
|
+
self,
|
|
214
|
+
pop_size: int = 100,
|
|
215
|
+
variant: str = "DE/rand/1/bin",
|
|
216
|
+
F: float = 0.5,
|
|
217
|
+
CR: float = 0.9,
|
|
218
|
+
sampling: Optional[nn.Module] = None,
|
|
219
|
+
crossover: Optional[nn.Module] = None,
|
|
220
|
+
repair: Optional[nn.Module] = None,
|
|
221
|
+
dither: Optional[str] = None,
|
|
222
|
+
jitter: bool = False,
|
|
223
|
+
per_individual_coeffs: bool = False,
|
|
224
|
+
adaptive: bool = False,
|
|
225
|
+
differentiable: bool = False,
|
|
226
|
+
selection_temperature: float = 1.0,
|
|
227
|
+
dtype: torch.dtype = torch.float32,
|
|
228
|
+
) -> None:
|
|
229
|
+
# Parse variant
|
|
230
|
+
self.variant = DEVariant.parse(variant)
|
|
231
|
+
self.dither = dither
|
|
232
|
+
self.jitter = jitter
|
|
233
|
+
self.per_individual_coeffs = per_individual_coeffs
|
|
234
|
+
self.adaptive = adaptive
|
|
235
|
+
self._init_F = F
|
|
236
|
+
self._init_CR = CR
|
|
237
|
+
self._selection_temperature = selection_temperature
|
|
238
|
+
|
|
239
|
+
# Create crossover operator if not provided
|
|
240
|
+
if crossover is None and self.variant.crossover is not None:
|
|
241
|
+
crossover = self._create_crossover(CR, adaptive)
|
|
242
|
+
|
|
243
|
+
# Create selection operator for parent selection in mutation
|
|
244
|
+
# Selection is differentiable when adaptive=True
|
|
245
|
+
selection = self._create_random_selection(adaptive, selection_temperature)
|
|
246
|
+
|
|
247
|
+
# Call base class
|
|
248
|
+
super().__init__(
|
|
249
|
+
pop_size=pop_size,
|
|
250
|
+
sampling=sampling,
|
|
251
|
+
selection=selection,
|
|
252
|
+
crossover=crossover,
|
|
253
|
+
mutation=None, # DE mutation is handled internally
|
|
254
|
+
survival=None, # DE uses greedy one-to-one selection
|
|
255
|
+
repair=repair,
|
|
256
|
+
eliminate_duplicates=False, # DE doesn't eliminate duplicates
|
|
257
|
+
n_offsprings=pop_size, # DE creates one trial per individual
|
|
258
|
+
differentiable=differentiable,
|
|
259
|
+
adaptive=adaptive,
|
|
260
|
+
dtype=dtype,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
def _create_crossover(
|
|
264
|
+
self,
|
|
265
|
+
CR: float,
|
|
266
|
+
adaptive: bool,
|
|
267
|
+
) -> nn.Module:
|
|
268
|
+
"""
|
|
269
|
+
Create the appropriate crossover operator.
|
|
270
|
+
|
|
271
|
+
When adaptive=True, crossover is differentiable with learnable CR.
|
|
272
|
+
"""
|
|
273
|
+
if self.variant.crossover == "bin":
|
|
274
|
+
from evograd.operators.crossover import BinomialCrossover
|
|
275
|
+
return BinomialCrossover(
|
|
276
|
+
cr=CR,
|
|
277
|
+
adaptive=adaptive, # Differentiable when adaptive
|
|
278
|
+
learn_cr=adaptive, # Learn CR when adaptive
|
|
279
|
+
)
|
|
280
|
+
elif self.variant.crossover == "exp":
|
|
281
|
+
from evograd.operators.crossover import ExponentialCrossover
|
|
282
|
+
return ExponentialCrossover(
|
|
283
|
+
cr=CR,
|
|
284
|
+
adaptive=adaptive, # Differentiable when adaptive
|
|
285
|
+
learn_cr=adaptive, # Learn CR when adaptive
|
|
286
|
+
)
|
|
287
|
+
return None
|
|
288
|
+
|
|
289
|
+
def _create_random_selection(self, adaptive: bool, temperature: float) -> nn.Module:
|
|
290
|
+
"""
|
|
291
|
+
Create selection operator for parent selection in mutation.
|
|
292
|
+
When adaptive=True, selection is differentiable with learnable temperature.
|
|
293
|
+
"""
|
|
294
|
+
from evograd.operators.selection import RandomSelection
|
|
295
|
+
return RandomSelection(replacement=True,
|
|
296
|
+
adaptive=adaptive,
|
|
297
|
+
temperature=temperature,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# =========================================================================
|
|
301
|
+
# Setup and Hyperparameters
|
|
302
|
+
# =========================================================================
|
|
303
|
+
|
|
304
|
+
def _setup(self) -> None:
|
|
305
|
+
"""DE-specific setup after initialization."""
|
|
306
|
+
n_var = self.problem.n_var
|
|
307
|
+
|
|
308
|
+
# Setup F parameter
|
|
309
|
+
if self.adaptive:
|
|
310
|
+
# Learnable F stored as log(F) for positivity
|
|
311
|
+
self._log_F = nn.Parameter(
|
|
312
|
+
torch.tensor(self._init_F, device=self.device, dtype=self.dtype).log()
|
|
313
|
+
)
|
|
314
|
+
else:
|
|
315
|
+
self.register_buffer(
|
|
316
|
+
"_F_buffer",
|
|
317
|
+
torch.tensor(self._init_F, device=self.device, dtype=self.dtype)
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
@property
|
|
321
|
+
def F(self) -> Tensor:
|
|
322
|
+
"""Current mutation scale factor."""
|
|
323
|
+
if self.adaptive:
|
|
324
|
+
return self._log_F.exp()
|
|
325
|
+
return self._F_buffer
|
|
326
|
+
|
|
327
|
+
@property
|
|
328
|
+
def CR(self) -> Tensor:
|
|
329
|
+
"""Current crossover rate."""
|
|
330
|
+
if self.crossover is not None and hasattr(self.crossover, 'cr'):
|
|
331
|
+
return self.crossover.cr
|
|
332
|
+
return torch.tensor(self._init_CR, device=self.device)
|
|
333
|
+
|
|
334
|
+
# =========================================================================
|
|
335
|
+
# Core DE Methods
|
|
336
|
+
# =========================================================================
|
|
337
|
+
|
|
338
|
+
def _get_F_values(self, n: int) -> Tensor:
|
|
339
|
+
"""
|
|
340
|
+
Get F values, optionally with dither/jitter/per_individual.
|
|
341
|
+
|
|
342
|
+
In adaptive mode, noise is added around the learned base_F using
|
|
343
|
+
reparameterization so gradients flow to the learnable parameter.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
n: Number of F values needed.
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
F values tensor of shape [n] or [n, n_var].
|
|
350
|
+
"""
|
|
351
|
+
base_F = self.F
|
|
352
|
+
|
|
353
|
+
# Per-individual coefficients: sample F around base (or Uniform if classical)
|
|
354
|
+
if self.per_individual_coeffs:
|
|
355
|
+
if self.adaptive:
|
|
356
|
+
# Reparameterized: noise around learned base_F (gradients flow)
|
|
357
|
+
# F_i = base_F + 0.25 * (2u - 1), u ~ Uniform(0,1) -> F_i ~ Uniform(base_F-0.25, base_F+0.25)
|
|
358
|
+
noise = 0.25 * (2 * torch.rand(n, device=self.device, dtype=self.dtype) - 1)
|
|
359
|
+
F_val = base_F + noise
|
|
360
|
+
else:
|
|
361
|
+
# Classical: F ~ Uniform(0.5, 1.0)
|
|
362
|
+
F_val = 0.5 + 0.5 * torch.rand(n, device=self.device, dtype=self.dtype)
|
|
363
|
+
|
|
364
|
+
F_val = F_val.clamp(0.01, 2.0)
|
|
365
|
+
|
|
366
|
+
if self.jitter:
|
|
367
|
+
n_var = self.n_var
|
|
368
|
+
jitter_noise = 0.001 * (2 * torch.rand(n, n_var, device=self.device, dtype=self.dtype) - 1)
|
|
369
|
+
F_val = F_val.unsqueeze(-1) + jitter_noise
|
|
370
|
+
|
|
371
|
+
return F_val
|
|
372
|
+
|
|
373
|
+
# Dither: randomize F per-generation or per-individual
|
|
374
|
+
if self.dither == "scalar":
|
|
375
|
+
# Same random F for all individuals this generation
|
|
376
|
+
if self.adaptive:
|
|
377
|
+
# Noise around learned base_F
|
|
378
|
+
noise = 0.1 * (2 * torch.rand(1, device=self.device, dtype=self.dtype) - 1)
|
|
379
|
+
F_val = (base_F + noise).expand(n)
|
|
380
|
+
else:
|
|
381
|
+
F_val = base_F + 0.1 * (2 * torch.rand(1, device=self.device, dtype=self.dtype) - 1)
|
|
382
|
+
F_val = F_val.expand(n)
|
|
383
|
+
elif self.dither == "vector":
|
|
384
|
+
# Different random F for each individual
|
|
385
|
+
if self.adaptive:
|
|
386
|
+
# Noise around learned base_F
|
|
387
|
+
noise = 0.25 * (2 * torch.rand(n, device=self.device, dtype=self.dtype) - 1)
|
|
388
|
+
F_val = base_F + noise
|
|
389
|
+
else:
|
|
390
|
+
F_val = 0.5 + 0.5 * torch.rand(n, device=self.device, dtype=self.dtype)
|
|
391
|
+
else:
|
|
392
|
+
# No dither: use base_F directly
|
|
393
|
+
F_val = base_F.expand(n)
|
|
394
|
+
|
|
395
|
+
F_val = F_val.clamp(0.01, 2.0)
|
|
396
|
+
|
|
397
|
+
# Jitter: add small per-dimension noise
|
|
398
|
+
if self.jitter:
|
|
399
|
+
n_var = self.n_var
|
|
400
|
+
jitter_noise = 0.001 * (2 * torch.rand(n, n_var, device=self.device, dtype=self.dtype) - 1)
|
|
401
|
+
if F_val.dim() == 1:
|
|
402
|
+
F_val = F_val.unsqueeze(-1) + jitter_noise
|
|
403
|
+
else:
|
|
404
|
+
F_val = F_val + jitter_noise
|
|
405
|
+
|
|
406
|
+
return F_val
|
|
407
|
+
|
|
408
|
+
def _get_CR_values(self, n: int) -> Optional[Tensor]:
|
|
409
|
+
"""
|
|
410
|
+
Get CR values for per-individual crossover.
|
|
411
|
+
|
|
412
|
+
In adaptive mode, noise is added around the learned CR using
|
|
413
|
+
reparameterization so gradients flow to the learnable parameter.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
n: Number of CR values needed.
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
CR values tensor of shape [n], or None if not using per_individual_coeffs.
|
|
420
|
+
"""
|
|
421
|
+
if not self.per_individual_coeffs:
|
|
422
|
+
return None
|
|
423
|
+
|
|
424
|
+
if self.adaptive:
|
|
425
|
+
# Reparameterized: noise around learned CR (gradients flow)
|
|
426
|
+
# CR_i = base_CR + 0.25 * (2u - 1), u ~ Uniform(0,1)
|
|
427
|
+
base_CR = self.CR
|
|
428
|
+
noise = 0.25 * (2 * torch.rand(n, device=self.device, dtype=self.dtype) - 1)
|
|
429
|
+
CR_val = base_CR + noise
|
|
430
|
+
else:
|
|
431
|
+
# Classical: CR ~ Uniform(0.5, 1.0)
|
|
432
|
+
CR_val = 0.5 + 0.5 * torch.rand(n, device=self.device, dtype=self.dtype)
|
|
433
|
+
|
|
434
|
+
return CR_val.clamp(0.0, 1.0)
|
|
435
|
+
|
|
436
|
+
def _select_parents(
|
|
437
|
+
self,
|
|
438
|
+
n_select: int,
|
|
439
|
+
) -> Tensor:
|
|
440
|
+
"""
|
|
441
|
+
Select parents for mutation using the selection operator.
|
|
442
|
+
|
|
443
|
+
In adaptive (differentiable) mode, uses the soft Gumbel-Softmax
|
|
444
|
+
selection operator so that gradients flow through parent selection.
|
|
445
|
+
|
|
446
|
+
Args:
|
|
447
|
+
n_select: Number of parents to select.
|
|
448
|
+
|
|
449
|
+
Returns:
|
|
450
|
+
Selected individuals [n_select, n_var].
|
|
451
|
+
"""
|
|
452
|
+
return self.selection(self.population, self.fitness, n_select=n_select)
|
|
453
|
+
|
|
454
|
+
@staticmethod
|
|
455
|
+
def _sample_distinct_indices(
|
|
456
|
+
N: int,
|
|
457
|
+
n_needed: int,
|
|
458
|
+
device: torch.device,
|
|
459
|
+
exclude: Optional[Tensor] = None,
|
|
460
|
+
) -> List[Tensor]:
|
|
461
|
+
"""
|
|
462
|
+
Sample ``n_needed`` mutually exclusive random index vectors of
|
|
463
|
+
length ``N``, each different from the optional ``exclude`` indices.
|
|
464
|
+
|
|
465
|
+
This is the canonical DE requirement: for each target *i* the
|
|
466
|
+
selected donor indices r1, r2, … must be distinct from each other
|
|
467
|
+
and from *i*.
|
|
468
|
+
|
|
469
|
+
Args:
|
|
470
|
+
N: Population size.
|
|
471
|
+
n_needed: How many distinct index vectors to draw (e.g. 3
|
|
472
|
+
for DE/rand/1).
|
|
473
|
+
device: Target device.
|
|
474
|
+
exclude: Optional ``[N]`` tensor of indices to avoid
|
|
475
|
+
(typically ``torch.arange(N)`` for the target vector).
|
|
476
|
+
|
|
477
|
+
Returns:
|
|
478
|
+
List of ``n_needed`` tensors, each of shape ``[N]``.
|
|
479
|
+
"""
|
|
480
|
+
# Build a pool of candidate indices for each target
|
|
481
|
+
# For each row i we need n_needed indices from {0..N-1} \ {exclude[i]}
|
|
482
|
+
all_indices: List[Tensor] = []
|
|
483
|
+
for _ in range(n_needed):
|
|
484
|
+
idx = torch.randint(0, N, (N,), device=device)
|
|
485
|
+
all_indices.append(idx)
|
|
486
|
+
|
|
487
|
+
# Rejection-resample collisions (vectorised, one pass per pair)
|
|
488
|
+
targets = exclude if exclude is not None else torch.full((N,), -1, device=device)
|
|
489
|
+
|
|
490
|
+
for k in range(len(all_indices)):
|
|
491
|
+
# Avoid target index
|
|
492
|
+
collides = all_indices[k] == targets
|
|
493
|
+
while collides.any():
|
|
494
|
+
all_indices[k][collides] = torch.randint(0, N, (int(collides.sum()),), device=device)
|
|
495
|
+
collides = all_indices[k] == targets
|
|
496
|
+
|
|
497
|
+
# Avoid previously selected indices
|
|
498
|
+
for j in range(k):
|
|
499
|
+
collides = all_indices[k] == all_indices[j]
|
|
500
|
+
while collides.any():
|
|
501
|
+
all_indices[k][collides] = torch.randint(0, N, (int(collides.sum()),), device=device)
|
|
502
|
+
# Re-check all constraints for the resampled positions
|
|
503
|
+
collides = all_indices[k] == targets
|
|
504
|
+
for jj in range(k):
|
|
505
|
+
collides = collides | (all_indices[k] == all_indices[jj])
|
|
506
|
+
|
|
507
|
+
return all_indices
|
|
508
|
+
|
|
509
|
+
def _mutate(self) -> Tensor:
|
|
510
|
+
"""
|
|
511
|
+
Generate donor vectors using the mutation strategy.
|
|
512
|
+
|
|
513
|
+
In classical mode, parent indices are sampled to be mutually
|
|
514
|
+
exclusive and different from the target index (canonical DE).
|
|
515
|
+
In adaptive mode, the soft selection operator is used instead
|
|
516
|
+
so that gradients can flow through the selection process; the
|
|
517
|
+
exclusion constraint is relaxed in that case.
|
|
518
|
+
|
|
519
|
+
Returns:
|
|
520
|
+
Donor vectors [pop_size, n_var].
|
|
521
|
+
"""
|
|
522
|
+
N = self.pop_size
|
|
523
|
+
F = self._get_F_values(N)
|
|
524
|
+
|
|
525
|
+
# Ensure F has correct shape for broadcasting
|
|
526
|
+
if F.dim() == 1:
|
|
527
|
+
F = F.unsqueeze(-1) # [N, 1] for broadcasting
|
|
528
|
+
|
|
529
|
+
mutation_type = self.variant.mutation
|
|
530
|
+
|
|
531
|
+
# -----------------------------------------------------------------
|
|
532
|
+
# Helper: pick parents (hard distinct indices or soft selection)
|
|
533
|
+
# -----------------------------------------------------------------
|
|
534
|
+
def _hard_parents(n_needed: int, exclude: Optional[Tensor] = None) -> List[Tensor]:
|
|
535
|
+
"""Return list of n_needed parent tensors [N, n_var] via hard distinct sampling."""
|
|
536
|
+
idx_list = self._sample_distinct_indices(N, n_needed, self.device, exclude=exclude)
|
|
537
|
+
return [self.population[idx] for idx in idx_list]
|
|
538
|
+
|
|
539
|
+
def _soft_parents(n_needed: int) -> List[Tensor]:
|
|
540
|
+
"""Return list of n_needed parent tensors [N, n_var] via soft selection."""
|
|
541
|
+
return [self._select_parents(N) for _ in range(n_needed)]
|
|
542
|
+
|
|
543
|
+
use_soft = self.adaptive
|
|
544
|
+
target_idx = torch.arange(N, device=self.device)
|
|
545
|
+
|
|
546
|
+
if mutation_type == "rand":
|
|
547
|
+
# DE/rand: v = x_r1 + F * (x_r2 - x_r3)
|
|
548
|
+
n_parents = 3 if self.variant.n_diff == 1 else 5
|
|
549
|
+
if use_soft:
|
|
550
|
+
parents = _soft_parents(n_parents)
|
|
551
|
+
else:
|
|
552
|
+
parents = _hard_parents(n_parents, exclude=target_idx)
|
|
553
|
+
|
|
554
|
+
if self.variant.n_diff == 1:
|
|
555
|
+
donor = parents[0] + F * (parents[1] - parents[2])
|
|
556
|
+
else:
|
|
557
|
+
donor = parents[0] + F * (parents[1] - parents[2]) + F * (parents[3] - parents[4])
|
|
558
|
+
|
|
559
|
+
elif mutation_type == "best":
|
|
560
|
+
# DE/best: v = x_best + F * (x_r1 - x_r2)
|
|
561
|
+
best_idx = torch.argmin(self.fitness)
|
|
562
|
+
x_best = self.population[best_idx].unsqueeze(0).expand(N, -1)
|
|
563
|
+
|
|
564
|
+
n_parents = 2 if self.variant.n_diff == 1 else 4
|
|
565
|
+
if use_soft:
|
|
566
|
+
parents = _soft_parents(n_parents)
|
|
567
|
+
else:
|
|
568
|
+
parents = _hard_parents(n_parents, exclude=target_idx)
|
|
569
|
+
|
|
570
|
+
if self.variant.n_diff == 1:
|
|
571
|
+
donor = x_best + F * (parents[0] - parents[1])
|
|
572
|
+
else:
|
|
573
|
+
donor = x_best + F * (parents[0] - parents[1]) + F * (parents[2] - parents[3])
|
|
574
|
+
|
|
575
|
+
elif mutation_type == "current_to_best":
|
|
576
|
+
# DE/current-to-best: v = x_i + F * (x_best - x_i) + F * (x_r1 - x_r2)
|
|
577
|
+
best_idx = torch.argmin(self.fitness)
|
|
578
|
+
x_best = self.population[best_idx].unsqueeze(0).expand(N, -1)
|
|
579
|
+
|
|
580
|
+
if use_soft:
|
|
581
|
+
parents = _soft_parents(2)
|
|
582
|
+
else:
|
|
583
|
+
parents = _hard_parents(2, exclude=target_idx)
|
|
584
|
+
|
|
585
|
+
donor = self.population + F * (x_best - self.population) + F * (parents[0] - parents[1])
|
|
586
|
+
|
|
587
|
+
elif mutation_type == "current_to_rand":
|
|
588
|
+
# DE/current-to-rand: v = x_i + K * (x_r1 - x_i) + F * (x_r2 - x_r3)
|
|
589
|
+
K = torch.rand(N, 1, device=self.device, dtype=self.dtype)
|
|
590
|
+
|
|
591
|
+
if use_soft:
|
|
592
|
+
parents = _soft_parents(3)
|
|
593
|
+
else:
|
|
594
|
+
parents = _hard_parents(3, exclude=target_idx)
|
|
595
|
+
|
|
596
|
+
donor = self.population + K * (parents[0] - self.population) + F * (parents[1] - parents[2])
|
|
597
|
+
|
|
598
|
+
else:
|
|
599
|
+
raise ValueError(f"Unknown mutation type: {mutation_type}")
|
|
600
|
+
|
|
601
|
+
return donor
|
|
602
|
+
|
|
603
|
+
def _infill(self) -> Tensor:
|
|
604
|
+
"""
|
|
605
|
+
Generate trial vectors through mutation and crossover.
|
|
606
|
+
|
|
607
|
+
Returns:
|
|
608
|
+
Trial vectors [pop_size, n_var].
|
|
609
|
+
"""
|
|
610
|
+
# 1. Mutation: create donor vectors
|
|
611
|
+
donor = self._mutate()
|
|
612
|
+
|
|
613
|
+
# 2. Crossover: combine target (population) and donor
|
|
614
|
+
if self.crossover is not None:
|
|
615
|
+
# Get per-individual CR if enabled
|
|
616
|
+
cr_values = self._get_CR_values(self.pop_size)
|
|
617
|
+
if cr_values is not None:
|
|
618
|
+
trial = self.crossover(self.population, donor, cr=cr_values)
|
|
619
|
+
else:
|
|
620
|
+
trial = self.crossover(self.population, donor)
|
|
621
|
+
else:
|
|
622
|
+
# current-to-rand: no crossover, donor is the trial
|
|
623
|
+
trial = donor
|
|
624
|
+
|
|
625
|
+
# 3. Repair bounds
|
|
626
|
+
if self.repair is not None:
|
|
627
|
+
trial = self.repair(trial, self.xl, self.xu)
|
|
628
|
+
else:
|
|
629
|
+
# Default: clamp to bounds
|
|
630
|
+
trial = torch.clamp(trial, self.xl, self.xu)
|
|
631
|
+
|
|
632
|
+
return trial
|
|
633
|
+
|
|
634
|
+
def _advance(self, offspring: Tensor, offspring_fitness: Tensor) -> None:
|
|
635
|
+
"""
|
|
636
|
+
Apply greedy one-to-one selection.
|
|
637
|
+
|
|
638
|
+
Each trial vector replaces the corresponding target if it has
|
|
639
|
+
better (lower for minimisation) fitness.
|
|
640
|
+
|
|
641
|
+
Args:
|
|
642
|
+
offspring: Trial vectors [pop_size, n_var].
|
|
643
|
+
offspring_fitness: Fitness of trial vectors [pop_size].
|
|
644
|
+
"""
|
|
645
|
+
# Greedy selection: trial replaces target if better
|
|
646
|
+
improved = offspring_fitness < self.fitness
|
|
647
|
+
|
|
648
|
+
# Update population
|
|
649
|
+
new_pop = torch.where(
|
|
650
|
+
improved.unsqueeze(-1),
|
|
651
|
+
offspring,
|
|
652
|
+
self.population
|
|
653
|
+
)
|
|
654
|
+
new_fitness = torch.where(improved, offspring_fitness, self.fitness)
|
|
655
|
+
|
|
656
|
+
# Update internal state
|
|
657
|
+
self._update_population(new_pop, new_fitness)
|
|
658
|
+
|
|
659
|
+
# Update best solution tracking
|
|
660
|
+
self.state.update_best(self.population, self.state.fitness)
|
|
661
|
+
|
|
662
|
+
def _update_population(self, new_pop: Tensor, new_fitness: Tensor) -> None:
|
|
663
|
+
"""Update population and fitness tensors."""
|
|
664
|
+
with torch.no_grad():
|
|
665
|
+
self._population.copy_(new_pop)
|
|
666
|
+
self.state.fitness = new_fitness
|
|
667
|
+
self.state.population = self._population
|
|
668
|
+
|
|
669
|
+
# =========================================================================
|
|
670
|
+
# Properties
|
|
671
|
+
# =========================================================================
|
|
672
|
+
|
|
673
|
+
@property
|
|
674
|
+
def population(self) -> Tensor:
|
|
675
|
+
"""Current population."""
|
|
676
|
+
return self._population
|
|
677
|
+
|
|
678
|
+
@property
|
|
679
|
+
def fitness(self) -> Tensor:
|
|
680
|
+
"""Current fitness values."""
|
|
681
|
+
return self.state.fitness
|
|
682
|
+
|
|
683
|
+
# =========================================================================
|
|
684
|
+
# Hyperparameter Access
|
|
685
|
+
# =========================================================================
|
|
686
|
+
|
|
687
|
+
def _get_hyperparams(self) -> Dict[str, Any]:
|
|
688
|
+
"""Return current hyperparameter values."""
|
|
689
|
+
params = {
|
|
690
|
+
'pop_size': self.pop_size,
|
|
691
|
+
'variant': str(self.variant),
|
|
692
|
+
'F': float(self.F.item()),
|
|
693
|
+
'per_individual_coeffs': self.per_individual_coeffs,
|
|
694
|
+
'adaptive': self.adaptive,
|
|
695
|
+
'differentiable': self.differentiable,
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
# Add CR from crossover operator
|
|
699
|
+
if self.crossover is not None and hasattr(self.crossover, 'cr'):
|
|
700
|
+
cr = self.crossover.cr
|
|
701
|
+
if isinstance(cr, Tensor):
|
|
702
|
+
params['CR'] = float(cr.mean().item())
|
|
703
|
+
else:
|
|
704
|
+
params['CR'] = float(cr)
|
|
705
|
+
|
|
706
|
+
# Add selection temperature
|
|
707
|
+
if hasattr(self.selection, 'temperature'):
|
|
708
|
+
params['selection_temperature'] = float(self.selection.temperature.item())
|
|
709
|
+
|
|
710
|
+
# Add crossover temperature
|
|
711
|
+
if self.crossover is not None and hasattr(self.crossover, 'temperature'):
|
|
712
|
+
params['crossover_temperature'] = float(self.crossover.temperature.item())
|
|
713
|
+
|
|
714
|
+
return params
|
|
715
|
+
|
|
716
|
+
# =========================================================================
|
|
717
|
+
# State Management for Adaptive Mode
|
|
718
|
+
# =========================================================================
|
|
719
|
+
|
|
720
|
+
@torch.no_grad()
|
|
721
|
+
def _clamp_hyperparams(self) -> None:
|
|
722
|
+
"""Clamp learnable hyperparameters to valid ranges."""
|
|
723
|
+
if self.adaptive:
|
|
724
|
+
# F in (0.01, 2.0) -> log(F) in (log(0.01), log(2.0))
|
|
725
|
+
self._log_F.clamp_(min=-4.6, max=0.7)
|
|
726
|
+
|
|
727
|
+
def update_state(self) -> None:
|
|
728
|
+
"""Commit pending changes and clamp hyperparameters."""
|
|
729
|
+
super().update_state()
|
|
730
|
+
self._clamp_hyperparams()
|
|
731
|
+
|
|
732
|
+
# =========================================================================
|
|
733
|
+
# String Representation
|
|
734
|
+
# =========================================================================
|
|
735
|
+
|
|
736
|
+
def __repr__(self) -> str:
|
|
737
|
+
return (
|
|
738
|
+
f"DE(pop_size={self.pop_size}, "
|
|
739
|
+
f"variant='{self.variant}', "
|
|
740
|
+
f"F={float(self.F.item()):.3f}, "
|
|
741
|
+
f"per_individual_coeffs={self.per_individual_coeffs}, "
|
|
742
|
+
f"adaptive={self.adaptive}, "
|
|
743
|
+
f"differentiable={self.differentiable})"
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
# =============================================================================
|
|
748
|
+
# Convenience Factory Functions
|
|
749
|
+
# =============================================================================
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def de_default(
|
|
753
|
+
pop_size: int = 100,
|
|
754
|
+
F: float = 0.5,
|
|
755
|
+
CR: float = 0.9,
|
|
756
|
+
per_individual_coeffs: bool = False,
|
|
757
|
+
adaptive: bool = False,
|
|
758
|
+
differentiable: bool = False,
|
|
759
|
+
**kwargs,
|
|
760
|
+
) -> "DE":
|
|
761
|
+
"""
|
|
762
|
+
Create a default Differential Evolution instance (DE/rand/1/bin).
|
|
763
|
+
|
|
764
|
+
This is the canonical DE configuration and the recommended starting point.
|
|
765
|
+
|
|
766
|
+
Args:
|
|
767
|
+
pop_size: Population size.
|
|
768
|
+
F: Mutation scale factor.
|
|
769
|
+
CR: Crossover rate.
|
|
770
|
+
per_individual_coeffs: If True, sample F and CR per individual.
|
|
771
|
+
adaptive: If True, operators are differentiable with learnable hyperparams.
|
|
772
|
+
differentiable: If True, population is learnable.
|
|
773
|
+
**kwargs: Additional arguments passed to DE.
|
|
774
|
+
|
|
775
|
+
Returns:
|
|
776
|
+
Configured DE instance.
|
|
777
|
+
"""
|
|
778
|
+
return DE(
|
|
779
|
+
pop_size=pop_size,
|
|
780
|
+
variant="DE/rand/1/bin",
|
|
781
|
+
F=F,
|
|
782
|
+
CR=CR,
|
|
783
|
+
per_individual_coeffs=per_individual_coeffs,
|
|
784
|
+
adaptive=adaptive,
|
|
785
|
+
differentiable=differentiable,
|
|
786
|
+
**kwargs,
|
|
787
|
+
)
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def de_rand_1_bin(
|
|
791
|
+
pop_size: int = 100,
|
|
792
|
+
F: float = 0.5,
|
|
793
|
+
CR: float = 0.9,
|
|
794
|
+
per_individual_coeffs: bool = False,
|
|
795
|
+
adaptive: bool = False,
|
|
796
|
+
differentiable: bool = False,
|
|
797
|
+
**kwargs,
|
|
798
|
+
) -> DE:
|
|
799
|
+
"""
|
|
800
|
+
Create DE/rand/1/bin - the classic DE variant.
|
|
801
|
+
|
|
802
|
+
Args:
|
|
803
|
+
pop_size: Population size.
|
|
804
|
+
F: Mutation scale factor.
|
|
805
|
+
CR: Crossover rate.
|
|
806
|
+
per_individual_coeffs: If True, sample F and CR per individual.
|
|
807
|
+
adaptive: If True, operators are differentiable with learnable hyperparams.
|
|
808
|
+
differentiable: If True, population is learnable.
|
|
809
|
+
**kwargs: Additional arguments passed to DE.
|
|
810
|
+
|
|
811
|
+
Returns:
|
|
812
|
+
Configured DE instance.
|
|
813
|
+
"""
|
|
814
|
+
return DE(
|
|
815
|
+
pop_size=pop_size,
|
|
816
|
+
variant="DE/rand/1/bin",
|
|
817
|
+
F=F,
|
|
818
|
+
CR=CR,
|
|
819
|
+
per_individual_coeffs=per_individual_coeffs,
|
|
820
|
+
adaptive=adaptive,
|
|
821
|
+
differentiable=differentiable,
|
|
822
|
+
**kwargs,
|
|
823
|
+
)
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
def de_best_1_bin(
|
|
827
|
+
pop_size: int = 100,
|
|
828
|
+
F: float = 0.5,
|
|
829
|
+
CR: float = 0.9,
|
|
830
|
+
per_individual_coeffs: bool = False,
|
|
831
|
+
adaptive: bool = False,
|
|
832
|
+
differentiable: bool = False,
|
|
833
|
+
**kwargs,
|
|
834
|
+
) -> DE:
|
|
835
|
+
"""
|
|
836
|
+
Create DE/best/1/bin - greedy variant using best individual.
|
|
837
|
+
|
|
838
|
+
Args:
|
|
839
|
+
pop_size: Population size.
|
|
840
|
+
F: Mutation scale factor.
|
|
841
|
+
CR: Crossover rate.
|
|
842
|
+
per_individual_coeffs: If True, sample F and CR per individual.
|
|
843
|
+
adaptive: If True, operators are differentiable with learnable hyperparams.
|
|
844
|
+
differentiable: If True, population is learnable.
|
|
845
|
+
**kwargs: Additional arguments passed to DE.
|
|
846
|
+
|
|
847
|
+
Returns:
|
|
848
|
+
Configured DE instance.
|
|
849
|
+
"""
|
|
850
|
+
return DE(
|
|
851
|
+
pop_size=pop_size,
|
|
852
|
+
variant="DE/best/1/bin",
|
|
853
|
+
F=F,
|
|
854
|
+
CR=CR,
|
|
855
|
+
per_individual_coeffs=per_individual_coeffs,
|
|
856
|
+
adaptive=adaptive,
|
|
857
|
+
differentiable=differentiable,
|
|
858
|
+
**kwargs,
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
def de_current_to_best_1_bin(
|
|
863
|
+
pop_size: int = 100,
|
|
864
|
+
F: float = 0.5,
|
|
865
|
+
CR: float = 0.9,
|
|
866
|
+
per_individual_coeffs: bool = False,
|
|
867
|
+
adaptive: bool = False,
|
|
868
|
+
differentiable: bool = False,
|
|
869
|
+
**kwargs,
|
|
870
|
+
) -> DE:
|
|
871
|
+
"""
|
|
872
|
+
Create DE/current-to-best/1/bin - balances exploration and exploitation.
|
|
873
|
+
|
|
874
|
+
Args:
|
|
875
|
+
pop_size: Population size.
|
|
876
|
+
F: Mutation scale factor.
|
|
877
|
+
CR: Crossover rate.
|
|
878
|
+
per_individual_coeffs: If True, sample F and CR per individual.
|
|
879
|
+
adaptive: If True, operators are differentiable with learnable hyperparams.
|
|
880
|
+
differentiable: If True, population is learnable.
|
|
881
|
+
**kwargs: Additional arguments passed to DE.
|
|
882
|
+
|
|
883
|
+
Returns:
|
|
884
|
+
Configured DE instance.
|
|
885
|
+
"""
|
|
886
|
+
return DE(
|
|
887
|
+
pop_size=pop_size,
|
|
888
|
+
variant="DE/current-to-best/1/bin",
|
|
889
|
+
F=F,
|
|
890
|
+
CR=CR,
|
|
891
|
+
per_individual_coeffs=per_individual_coeffs,
|
|
892
|
+
adaptive=adaptive,
|
|
893
|
+
differentiable=differentiable,
|
|
894
|
+
**kwargs,
|
|
895
|
+
)
|