evograd-diff 0.1.0__tar.gz → 0.1.2__tar.gz
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_diff-0.1.0 → evograd_diff-0.1.2}/.claude/settings.local.json +6 -1
- evograd_diff-0.1.2/.gitignore +15 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/PKG-INFO +34 -26
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/README.md +33 -25
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/algorithms/cmaes.py +8 -5
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/core/algorithm.py +20 -4
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/tests/run_all.py +22 -1
- evograd_diff-0.1.2/evograd/tests/test_cmaes.py +143 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/tests/test_ga.py +21 -40
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/tests/test_operators.py +8 -8
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/tests/test_per_individual.py +26 -18
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/tests/test_utils.py +4 -2
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/pyproject.toml +1 -1
- evograd_diff-0.1.2/test.py +32 -0
- evograd_diff-0.1.2/test2.py +20 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/uv.lock +2 -2
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/.python-version +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/LICENSE +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/Test_new_evograd.ipynb +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/__init__.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/algorithms/__init__.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/algorithms/de.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/algorithms/ga.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/algorithms/pso.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/algorithms/shade.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/benchmarks/functions/__init__.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/benchmarks/functions/base.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/benchmarks/functions/cec2017/__init__.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/benchmarks/functions/cec2017/basic.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/benchmarks/functions/cec2017/composition.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/benchmarks/functions/cec2017/data.pkl +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/benchmarks/functions/cec2017/data.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/benchmarks/functions/cec2017/hybrid.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/benchmarks/functions/cec2017/simple.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/benchmarks/functions/classical.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/benchmarks/functions/smoothed_funnel.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/benchmarks/functions/transforms.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/benchmarks/run_benchmark_functions.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/core/__init__.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/core/maximize.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/core/minimize.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/core/problem.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/core/result.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/core/termination.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/operators/__init__.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/operators/crossover.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/operators/mutation.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/operators/relaxations.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/operators/repair.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/operators/sampling.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/operators/selection.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/operators/survival.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/tests/__init__.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/tests/test_core.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/utils/__init__.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/utils/callbacks.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/utils/device.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/utils/duplicates.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.2}/plot_benchmarks.py +0 -0
|
@@ -6,7 +6,12 @@
|
|
|
6
6
|
"Bash(git push *)",
|
|
7
7
|
"Bash(uv build *)",
|
|
8
8
|
"Bash(python -m zipfile -l dist/evograd-0.1.0-py3-none-any.whl)",
|
|
9
|
-
"WebFetch(domain:pypi.org)"
|
|
9
|
+
"WebFetch(domain:pypi.org)",
|
|
10
|
+
"Bash(uv run *)",
|
|
11
|
+
"Bash(git check-ignore *)",
|
|
12
|
+
"Bash(uv lock *)",
|
|
13
|
+
"Bash(git stash *)",
|
|
14
|
+
"Bash(grep -n '```' README.md)"
|
|
10
15
|
]
|
|
11
16
|
}
|
|
12
17
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: evograd-diff
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: PyTorch-based framework for differentiable evolutionary computation and swarm intelligence
|
|
5
5
|
Project-URL: Homepage, https://github.com/andreatangherloni/EvoGrad
|
|
6
6
|
Project-URL: Repository, https://github.com/andreatangherloni/EvoGrad
|
|
@@ -47,14 +47,21 @@ Description-Content-Type: text/markdown
|
|
|
47
47
|
## 📦 Installation
|
|
48
48
|
|
|
49
49
|
```bash
|
|
50
|
-
#
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
# From PyPI (the import name is `evograd`)
|
|
51
|
+
pip install evograd-diff
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Or install directly from the repository:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install "git+https://github.com/andreatangherloni/EvoGrad.git"
|
|
58
|
+
```
|
|
53
59
|
|
|
54
|
-
|
|
55
|
-
pip install torch numpy
|
|
60
|
+
For local development:
|
|
56
61
|
|
|
57
|
-
|
|
62
|
+
```bash
|
|
63
|
+
git clone https://github.com/andreatangherloni/EvoGrad.git
|
|
64
|
+
cd EvoGrad
|
|
58
65
|
pip install -e .
|
|
59
66
|
```
|
|
60
67
|
|
|
@@ -62,7 +69,7 @@ pip install -e .
|
|
|
62
69
|
|
|
63
70
|
```python
|
|
64
71
|
import torch
|
|
65
|
-
from evograd.core import Problem, minimize
|
|
72
|
+
from evograd.core import Problem, minimize, MaxEvaluations
|
|
66
73
|
from evograd.algorithms import GA, DE, PSO, CMAES
|
|
67
74
|
|
|
68
75
|
# Define an optimisation problem
|
|
@@ -75,22 +82,22 @@ problem = Problem(
|
|
|
75
82
|
|
|
76
83
|
# Run with Genetic Algorithm
|
|
77
84
|
ga = GA(pop_size=100, differentiable=True)
|
|
78
|
-
result = minimize(problem, ga,
|
|
85
|
+
result = minimize(problem, ga, termination=MaxEvaluations(10000), seed=42)
|
|
79
86
|
print(f"GA Best: {result.best_fitness:.6f}")
|
|
80
87
|
|
|
81
88
|
# Run with Differential Evolution
|
|
82
89
|
de = DE(pop_size=100, variant="DE/rand/1/bin", adaptive=True)
|
|
83
|
-
result = minimize(problem, de,
|
|
90
|
+
result = minimize(problem, de, termination=MaxEvaluations(10000), seed=42)
|
|
84
91
|
print(f"DE Best: {result.best_fitness:.6f}")
|
|
85
92
|
|
|
86
93
|
# Run with Particle Swarm Optimisation
|
|
87
94
|
pso = PSO(pop_size=100, adaptive=True, differentiable=True)
|
|
88
|
-
result = minimize(problem, pso,
|
|
95
|
+
result = minimize(problem, pso, termination=MaxEvaluations(10000), seed=42)
|
|
89
96
|
print(f"PSO Best: {result.best_fitness:.6f}")
|
|
90
97
|
|
|
91
98
|
# Run with CMA-ES
|
|
92
99
|
cmaes = CMAES(sigma=0.5, adaptive=True)
|
|
93
|
-
result = minimize(problem, cmaes,
|
|
100
|
+
result = minimize(problem, cmaes, termination=MaxEvaluations(10000), seed=42)
|
|
94
101
|
print(f"CMA-ES Best: {result.best_fitness:.6f}")
|
|
95
102
|
```
|
|
96
103
|
|
|
@@ -115,10 +122,10 @@ ga = GA(pop_size=100, differentiable=False)
|
|
|
115
122
|
# Fully differentiable GA with custom operators
|
|
116
123
|
ga = GA(
|
|
117
124
|
pop_size=100,
|
|
118
|
-
selection=RouletteSelection(
|
|
119
|
-
crossover=SBXCrossover(
|
|
120
|
-
mutation=PolynomialMutation(
|
|
121
|
-
survival=MergeSurvival(
|
|
125
|
+
selection=RouletteSelection(adaptive=True, learn_temperature=True),
|
|
126
|
+
crossover=SBXCrossover(adaptive=True, learn_eta=True, learn_prob=True),
|
|
127
|
+
mutation=PolynomialMutation(adaptive=True, learn_eta=True, learn_prob=True),
|
|
128
|
+
survival=MergeSurvival(elitism=True, adaptive=True),
|
|
122
129
|
differentiable=True, # Makes population learnable
|
|
123
130
|
)
|
|
124
131
|
```
|
|
@@ -127,7 +134,7 @@ ga = GA(
|
|
|
127
134
|
|-----------|--------|
|
|
128
135
|
| `differentiable=False` | Classical GA with discrete operators |
|
|
129
136
|
| `differentiable=True` | Population is an `nn.Parameter` (learnable via backprop) |
|
|
130
|
-
| Operator `
|
|
137
|
+
| Operator `adaptive=True` | Operator uses Gumbel-Softmax/Binary-Concrete for gradient flow |
|
|
131
138
|
| Operator `learn_*=True` | Operator hyperparameters become learnable `nn.Parameter` |
|
|
132
139
|
|
|
133
140
|
### Differential Evolution (DE)
|
|
@@ -168,10 +175,10 @@ de = DE(pop_size=100, variant="DE/current-to-best/1/bin", adaptive=True, differe
|
|
|
168
175
|
PSO uses the same **algorithm-level flags** as DE:
|
|
169
176
|
|
|
170
177
|
```python
|
|
171
|
-
from evograd.algorithms import PSO, pso_constriction,
|
|
178
|
+
from evograd.algorithms import PSO, pso_constriction, pso_default
|
|
172
179
|
|
|
173
180
|
# Classical PSO
|
|
174
|
-
pso = PSO(pop_size=100,
|
|
181
|
+
pso = PSO(pop_size=100, w=0.7, c1=1.5, c2=1.5)
|
|
175
182
|
|
|
176
183
|
# Adaptive PSO (learnable inertia, c1, c2)
|
|
177
184
|
pso = PSO(pop_size=100, adaptive=True)
|
|
@@ -249,7 +256,7 @@ EvoGrad provides a comprehensive library of evolutionary operators:
|
|
|
249
256
|
| `BlendCrossover` | BLX-α crossover | ✓ |
|
|
250
257
|
| `ArithmeticCrossover` | Weighted average | ✓ |
|
|
251
258
|
| `UniformCrossover` | Gene-wise uniform swap | ✓ |
|
|
252
|
-
| `
|
|
259
|
+
| `NPointCrossover` | N-point crossover | ✓ |
|
|
253
260
|
|
|
254
261
|
### Mutation
|
|
255
262
|
| Operator | Description | Differentiable |
|
|
@@ -257,21 +264,22 @@ EvoGrad provides a comprehensive library of evolutionary operators:
|
|
|
257
264
|
| `PolynomialMutation` | Polynomial bounded mutation | ✓ |
|
|
258
265
|
| `GaussianMutation` | Additive Gaussian noise | ✓ |
|
|
259
266
|
| `UniformMutation` | Uniform random replacement | ✓ |
|
|
260
|
-
| `
|
|
267
|
+
| `NonUniformMutation` | Annealed mutation strength | ✓ |
|
|
261
268
|
|
|
262
269
|
### Survival
|
|
263
270
|
| Operator | Description |
|
|
264
271
|
|----------|-------------|
|
|
265
272
|
| `MergeSurvival` | (μ+λ) with optional elitism |
|
|
266
|
-
| `
|
|
267
|
-
| `
|
|
273
|
+
| `CommaSurvival` | (μ,λ) generational replacement |
|
|
274
|
+
| `ReplaceWorstSurvival` | Steady-state worst replacement |
|
|
275
|
+
| `AgeSurvival` | Age-based replacement |
|
|
268
276
|
| `FitnessSurvival` | Pure fitness-based truncation |
|
|
269
277
|
|
|
270
278
|
### Repair
|
|
271
279
|
| Operator | Description |
|
|
272
280
|
|----------|-------------|
|
|
273
281
|
| `BoundsRepair` | Clamp to bounds |
|
|
274
|
-
| `
|
|
282
|
+
| `ReflectRepair` | Bounce off boundaries |
|
|
275
283
|
| `WrapRepair` | Toroidal wrap-around |
|
|
276
284
|
| `RandomRepair` | Random resampling |
|
|
277
285
|
|
|
@@ -283,7 +291,7 @@ EvoGrad provides a comprehensive library of evolutionary operators:
|
|
|
283
291
|
import torch
|
|
284
292
|
import torch.nn as nn
|
|
285
293
|
from evograd.algorithms import CMAES
|
|
286
|
-
from evograd.core import Problem
|
|
294
|
+
from evograd.core import Problem, minimize
|
|
287
295
|
from evograd.core.termination import MaxEvaluations
|
|
288
296
|
|
|
289
297
|
|
|
@@ -337,7 +345,7 @@ print(f"Final loss: {result.best_fitness:.6f}")
|
|
|
337
345
|
### Callbacks for Logging
|
|
338
346
|
|
|
339
347
|
```python
|
|
340
|
-
from evograd.core import minimize
|
|
348
|
+
from evograd.core import minimize, MaxEvaluations
|
|
341
349
|
from evograd.utils import HistoryCallback, PrintCallback
|
|
342
350
|
|
|
343
351
|
callbacks = [
|
|
@@ -20,14 +20,21 @@
|
|
|
20
20
|
## 📦 Installation
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
|
-
#
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
# From PyPI (the import name is `evograd`)
|
|
24
|
+
pip install evograd-diff
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or install directly from the repository:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install "git+https://github.com/andreatangherloni/EvoGrad.git"
|
|
31
|
+
```
|
|
26
32
|
|
|
27
|
-
|
|
28
|
-
pip install torch numpy
|
|
33
|
+
For local development:
|
|
29
34
|
|
|
30
|
-
|
|
35
|
+
```bash
|
|
36
|
+
git clone https://github.com/andreatangherloni/EvoGrad.git
|
|
37
|
+
cd EvoGrad
|
|
31
38
|
pip install -e .
|
|
32
39
|
```
|
|
33
40
|
|
|
@@ -35,7 +42,7 @@ pip install -e .
|
|
|
35
42
|
|
|
36
43
|
```python
|
|
37
44
|
import torch
|
|
38
|
-
from evograd.core import Problem, minimize
|
|
45
|
+
from evograd.core import Problem, minimize, MaxEvaluations
|
|
39
46
|
from evograd.algorithms import GA, DE, PSO, CMAES
|
|
40
47
|
|
|
41
48
|
# Define an optimisation problem
|
|
@@ -48,22 +55,22 @@ problem = Problem(
|
|
|
48
55
|
|
|
49
56
|
# Run with Genetic Algorithm
|
|
50
57
|
ga = GA(pop_size=100, differentiable=True)
|
|
51
|
-
result = minimize(problem, ga,
|
|
58
|
+
result = minimize(problem, ga, termination=MaxEvaluations(10000), seed=42)
|
|
52
59
|
print(f"GA Best: {result.best_fitness:.6f}")
|
|
53
60
|
|
|
54
61
|
# Run with Differential Evolution
|
|
55
62
|
de = DE(pop_size=100, variant="DE/rand/1/bin", adaptive=True)
|
|
56
|
-
result = minimize(problem, de,
|
|
63
|
+
result = minimize(problem, de, termination=MaxEvaluations(10000), seed=42)
|
|
57
64
|
print(f"DE Best: {result.best_fitness:.6f}")
|
|
58
65
|
|
|
59
66
|
# Run with Particle Swarm Optimisation
|
|
60
67
|
pso = PSO(pop_size=100, adaptive=True, differentiable=True)
|
|
61
|
-
result = minimize(problem, pso,
|
|
68
|
+
result = minimize(problem, pso, termination=MaxEvaluations(10000), seed=42)
|
|
62
69
|
print(f"PSO Best: {result.best_fitness:.6f}")
|
|
63
70
|
|
|
64
71
|
# Run with CMA-ES
|
|
65
72
|
cmaes = CMAES(sigma=0.5, adaptive=True)
|
|
66
|
-
result = minimize(problem, cmaes,
|
|
73
|
+
result = minimize(problem, cmaes, termination=MaxEvaluations(10000), seed=42)
|
|
67
74
|
print(f"CMA-ES Best: {result.best_fitness:.6f}")
|
|
68
75
|
```
|
|
69
76
|
|
|
@@ -88,10 +95,10 @@ ga = GA(pop_size=100, differentiable=False)
|
|
|
88
95
|
# Fully differentiable GA with custom operators
|
|
89
96
|
ga = GA(
|
|
90
97
|
pop_size=100,
|
|
91
|
-
selection=RouletteSelection(
|
|
92
|
-
crossover=SBXCrossover(
|
|
93
|
-
mutation=PolynomialMutation(
|
|
94
|
-
survival=MergeSurvival(
|
|
98
|
+
selection=RouletteSelection(adaptive=True, learn_temperature=True),
|
|
99
|
+
crossover=SBXCrossover(adaptive=True, learn_eta=True, learn_prob=True),
|
|
100
|
+
mutation=PolynomialMutation(adaptive=True, learn_eta=True, learn_prob=True),
|
|
101
|
+
survival=MergeSurvival(elitism=True, adaptive=True),
|
|
95
102
|
differentiable=True, # Makes population learnable
|
|
96
103
|
)
|
|
97
104
|
```
|
|
@@ -100,7 +107,7 @@ ga = GA(
|
|
|
100
107
|
|-----------|--------|
|
|
101
108
|
| `differentiable=False` | Classical GA with discrete operators |
|
|
102
109
|
| `differentiable=True` | Population is an `nn.Parameter` (learnable via backprop) |
|
|
103
|
-
| Operator `
|
|
110
|
+
| Operator `adaptive=True` | Operator uses Gumbel-Softmax/Binary-Concrete for gradient flow |
|
|
104
111
|
| Operator `learn_*=True` | Operator hyperparameters become learnable `nn.Parameter` |
|
|
105
112
|
|
|
106
113
|
### Differential Evolution (DE)
|
|
@@ -141,10 +148,10 @@ de = DE(pop_size=100, variant="DE/current-to-best/1/bin", adaptive=True, differe
|
|
|
141
148
|
PSO uses the same **algorithm-level flags** as DE:
|
|
142
149
|
|
|
143
150
|
```python
|
|
144
|
-
from evograd.algorithms import PSO, pso_constriction,
|
|
151
|
+
from evograd.algorithms import PSO, pso_constriction, pso_default
|
|
145
152
|
|
|
146
153
|
# Classical PSO
|
|
147
|
-
pso = PSO(pop_size=100,
|
|
154
|
+
pso = PSO(pop_size=100, w=0.7, c1=1.5, c2=1.5)
|
|
148
155
|
|
|
149
156
|
# Adaptive PSO (learnable inertia, c1, c2)
|
|
150
157
|
pso = PSO(pop_size=100, adaptive=True)
|
|
@@ -222,7 +229,7 @@ EvoGrad provides a comprehensive library of evolutionary operators:
|
|
|
222
229
|
| `BlendCrossover` | BLX-α crossover | ✓ |
|
|
223
230
|
| `ArithmeticCrossover` | Weighted average | ✓ |
|
|
224
231
|
| `UniformCrossover` | Gene-wise uniform swap | ✓ |
|
|
225
|
-
| `
|
|
232
|
+
| `NPointCrossover` | N-point crossover | ✓ |
|
|
226
233
|
|
|
227
234
|
### Mutation
|
|
228
235
|
| Operator | Description | Differentiable |
|
|
@@ -230,21 +237,22 @@ EvoGrad provides a comprehensive library of evolutionary operators:
|
|
|
230
237
|
| `PolynomialMutation` | Polynomial bounded mutation | ✓ |
|
|
231
238
|
| `GaussianMutation` | Additive Gaussian noise | ✓ |
|
|
232
239
|
| `UniformMutation` | Uniform random replacement | ✓ |
|
|
233
|
-
| `
|
|
240
|
+
| `NonUniformMutation` | Annealed mutation strength | ✓ |
|
|
234
241
|
|
|
235
242
|
### Survival
|
|
236
243
|
| Operator | Description |
|
|
237
244
|
|----------|-------------|
|
|
238
245
|
| `MergeSurvival` | (μ+λ) with optional elitism |
|
|
239
|
-
| `
|
|
240
|
-
| `
|
|
246
|
+
| `CommaSurvival` | (μ,λ) generational replacement |
|
|
247
|
+
| `ReplaceWorstSurvival` | Steady-state worst replacement |
|
|
248
|
+
| `AgeSurvival` | Age-based replacement |
|
|
241
249
|
| `FitnessSurvival` | Pure fitness-based truncation |
|
|
242
250
|
|
|
243
251
|
### Repair
|
|
244
252
|
| Operator | Description |
|
|
245
253
|
|----------|-------------|
|
|
246
254
|
| `BoundsRepair` | Clamp to bounds |
|
|
247
|
-
| `
|
|
255
|
+
| `ReflectRepair` | Bounce off boundaries |
|
|
248
256
|
| `WrapRepair` | Toroidal wrap-around |
|
|
249
257
|
| `RandomRepair` | Random resampling |
|
|
250
258
|
|
|
@@ -256,7 +264,7 @@ EvoGrad provides a comprehensive library of evolutionary operators:
|
|
|
256
264
|
import torch
|
|
257
265
|
import torch.nn as nn
|
|
258
266
|
from evograd.algorithms import CMAES
|
|
259
|
-
from evograd.core import Problem
|
|
267
|
+
from evograd.core import Problem, minimize
|
|
260
268
|
from evograd.core.termination import MaxEvaluations
|
|
261
269
|
|
|
262
270
|
|
|
@@ -310,7 +318,7 @@ print(f"Final loss: {result.best_fitness:.6f}")
|
|
|
310
318
|
### Callbacks for Logging
|
|
311
319
|
|
|
312
320
|
```python
|
|
313
|
-
from evograd.core import minimize
|
|
321
|
+
from evograd.core import minimize, MaxEvaluations
|
|
314
322
|
from evograd.utils import HistoryCallback, PrintCallback
|
|
315
323
|
|
|
316
324
|
callbacks = [
|
|
@@ -281,15 +281,18 @@ class CMAES(Algorithm):
|
|
|
281
281
|
# Setup
|
|
282
282
|
# =========================================================================
|
|
283
283
|
|
|
284
|
-
def
|
|
285
|
-
"""CMA-ES
|
|
286
|
-
n_var = self.problem.n_var
|
|
287
|
-
|
|
284
|
+
def _setup_pop_size(self) -> None:
|
|
285
|
+
"""Compute the default CMA-ES population size (lambda) before sampling."""
|
|
288
286
|
# Compute default population size if not provided
|
|
289
287
|
if self._requested_pop_size is None:
|
|
288
|
+
n_var = self.problem.n_var
|
|
290
289
|
self.pop_size = 4 + int(3 * math.log(n_var))
|
|
291
290
|
self.n_offsprings = self.pop_size
|
|
292
|
-
|
|
291
|
+
|
|
292
|
+
def _setup(self) -> None:
|
|
293
|
+
"""CMA-ES specific setup after initialization."""
|
|
294
|
+
n_var = self.problem.n_var
|
|
295
|
+
|
|
293
296
|
# Initialize restart state
|
|
294
297
|
self.restart_state.initial_pop_size = self.pop_size
|
|
295
298
|
self.restart_state.current_pop_size = self.pop_size
|
|
@@ -322,10 +322,21 @@ class Algorithm(nn.Module, ABC):
|
|
|
322
322
|
# Optional Hooks (can be overridden)
|
|
323
323
|
# =========================================================================
|
|
324
324
|
|
|
325
|
+
def _setup_pop_size(self) -> None:
|
|
326
|
+
"""
|
|
327
|
+
Resolve the population size before the initial population is sampled.
|
|
328
|
+
|
|
329
|
+
Override for algorithms that derive pop_size from the problem
|
|
330
|
+
(e.g. CMA-ES default lambda = 4 + floor(3*ln(n))). Called near the
|
|
331
|
+
start of initialize(), after the problem is attached but before
|
|
332
|
+
sampling, so the population is allocated at the final size.
|
|
333
|
+
"""
|
|
334
|
+
pass
|
|
335
|
+
|
|
325
336
|
def _setup(self) -> None:
|
|
326
337
|
"""
|
|
327
338
|
One-time setup after initialisation.
|
|
328
|
-
|
|
339
|
+
|
|
329
340
|
Override to perform algorithm-specific setup that requires
|
|
330
341
|
the problem and population to be initialized.
|
|
331
342
|
Called at the end of initialize().
|
|
@@ -370,14 +381,17 @@ class Algorithm(nn.Module, ABC):
|
|
|
370
381
|
|
|
371
382
|
# Move problem bounds to device
|
|
372
383
|
self.register_buffer(
|
|
373
|
-
"xl",
|
|
384
|
+
"xl",
|
|
374
385
|
problem.xl.to(device=self.device, dtype=self.dtype)
|
|
375
386
|
)
|
|
376
387
|
self.register_buffer(
|
|
377
388
|
"xu",
|
|
378
389
|
problem.xu.to(device=self.device, dtype=self.dtype)
|
|
379
390
|
)
|
|
380
|
-
|
|
391
|
+
|
|
392
|
+
# Resolve final population size (e.g. CMA-ES auto lambda) before sampling
|
|
393
|
+
self._setup_pop_size()
|
|
394
|
+
|
|
381
395
|
# Create initial population using sampling operator
|
|
382
396
|
population = self.sampling(self.pop_size, problem)
|
|
383
397
|
|
|
@@ -686,13 +700,15 @@ class Algorithm(nn.Module, ABC):
|
|
|
686
700
|
mode = "differentiable" if self.differentiable else "classical"
|
|
687
701
|
status = "initialized" if self._is_initialized else "not initialized"
|
|
688
702
|
n_var = self.n_var if self.problem else "?"
|
|
703
|
+
# device is only assigned in initialize(); fall back before then.
|
|
704
|
+
device = getattr(self, "device", "?")
|
|
689
705
|
return (
|
|
690
706
|
f"{self.__class__.__name__}("
|
|
691
707
|
f"pop_size={self.pop_size}, "
|
|
692
708
|
f"n_var={n_var}, "
|
|
693
709
|
f"mode={mode}, "
|
|
694
710
|
f"status={status}, "
|
|
695
|
-
f"device={
|
|
711
|
+
f"device={device})"
|
|
696
712
|
)
|
|
697
713
|
|
|
698
714
|
def summary(self) -> str:
|
|
@@ -16,6 +16,9 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
16
16
|
from tests.test_utils import run_all_tests as test_utils
|
|
17
17
|
from tests.test_core import run_all_tests as test_core
|
|
18
18
|
from tests.test_operators import run_all_tests as test_operators
|
|
19
|
+
from tests.test_cmaes import run_all_tests as test_cmaes
|
|
20
|
+
from tests.test_ga import run_all_tests as test_ga
|
|
21
|
+
from tests.test_per_individual import main as test_per_individual
|
|
19
22
|
|
|
20
23
|
|
|
21
24
|
def run_all():
|
|
@@ -45,7 +48,25 @@ def run_all():
|
|
|
45
48
|
print("▶ RUNNING OPERATORS TESTS")
|
|
46
49
|
print("▶"*60)
|
|
47
50
|
results['operators'] = test_operators()
|
|
48
|
-
|
|
51
|
+
|
|
52
|
+
# Run CMA-ES tests
|
|
53
|
+
print("\n\n" + "▶"*60)
|
|
54
|
+
print("▶ RUNNING CMA-ES TESTS")
|
|
55
|
+
print("▶"*60)
|
|
56
|
+
results['cmaes'] = test_cmaes()
|
|
57
|
+
|
|
58
|
+
# Run GA tests
|
|
59
|
+
print("\n\n" + "▶"*60)
|
|
60
|
+
print("▶ RUNNING GA TESTS")
|
|
61
|
+
print("▶"*60)
|
|
62
|
+
results['ga'] = test_ga()
|
|
63
|
+
|
|
64
|
+
# Run per-individual parameter tests
|
|
65
|
+
print("\n\n" + "▶"*60)
|
|
66
|
+
print("▶ RUNNING PER-INDIVIDUAL TESTS")
|
|
67
|
+
print("▶"*60)
|
|
68
|
+
results['per_individual'] = test_per_individual()
|
|
69
|
+
|
|
49
70
|
# Summary
|
|
50
71
|
print("\n\n" + "█"*60)
|
|
51
72
|
print("█" + " "*58 + "█")
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test script for EvoGrad CMA-ES implementation.
|
|
3
|
+
|
|
4
|
+
Tests:
|
|
5
|
+
- Default (auto) population size: lambda = 4 + floor(3*ln(n))
|
|
6
|
+
- Explicit population size
|
|
7
|
+
- Population tensor is allocated at the resolved pop_size
|
|
8
|
+
- End-to-end minimize() runs and converges
|
|
9
|
+
|
|
10
|
+
Regression:
|
|
11
|
+
With no pop_size, CMA-ES used to allocate the population at a
|
|
12
|
+
placeholder size (10) and only recompute lambda afterwards, causing
|
|
13
|
+
a tensor-size mismatch on the first update for any n_var where
|
|
14
|
+
4 + floor(3*ln(n)) != 10. See _setup_pop_size().
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
cd evograd && python tests/test_cmaes.py
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import sys
|
|
21
|
+
import os
|
|
22
|
+
import math
|
|
23
|
+
import torch
|
|
24
|
+
|
|
25
|
+
# Add parent directory to path for imports
|
|
26
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
27
|
+
|
|
28
|
+
from evograd.core.problem import Problem
|
|
29
|
+
from evograd.core.minimize import minimize
|
|
30
|
+
from evograd.core.termination import MaxEvaluations
|
|
31
|
+
from evograd.algorithms.cmaes import CMAES
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def sphere(x):
|
|
35
|
+
"""Sphere function: sum of squares. Global optimum at origin."""
|
|
36
|
+
return (x ** 2).sum(dim=-1)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _expected_lambda(n_var):
|
|
40
|
+
"""CMA-ES default population size (lambda)."""
|
|
41
|
+
return 4 + int(3 * math.log(n_var))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def make_problem(n_var):
|
|
45
|
+
return Problem(objective=sphere, n_var=n_var, xl=-5.0, xu=5.0)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# =============================================================================
|
|
49
|
+
# Tests
|
|
50
|
+
# =============================================================================
|
|
51
|
+
|
|
52
|
+
def test_auto_pop_size():
|
|
53
|
+
"""Default pop_size must resolve to lambda and size the population."""
|
|
54
|
+
print("\n1. Testing auto pop_size (no pop_size given)...")
|
|
55
|
+
|
|
56
|
+
# Cover sizes where the auto value differs from the old placeholder (10).
|
|
57
|
+
for n_var in (2, 30, 50):
|
|
58
|
+
expected = _expected_lambda(n_var)
|
|
59
|
+
|
|
60
|
+
cmaes = CMAES(sigma=0.5)
|
|
61
|
+
cmaes.initialize(make_problem(n_var))
|
|
62
|
+
|
|
63
|
+
assert cmaes.pop_size == expected, (
|
|
64
|
+
f"n_var={n_var}: pop_size {cmaes.pop_size} != expected {expected}"
|
|
65
|
+
)
|
|
66
|
+
assert cmaes.n_offsprings == expected, (
|
|
67
|
+
f"n_var={n_var}: n_offsprings {cmaes.n_offsprings} != {expected}"
|
|
68
|
+
)
|
|
69
|
+
assert tuple(cmaes.population.shape) == (expected, n_var), (
|
|
70
|
+
f"n_var={n_var}: population shape {tuple(cmaes.population.shape)} "
|
|
71
|
+
f"!= {(expected, n_var)}"
|
|
72
|
+
)
|
|
73
|
+
print(f" n_var={n_var:>3}: pop_size={cmaes.pop_size} "
|
|
74
|
+
f"population={tuple(cmaes.population.shape)} ✓")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_explicit_pop_size():
|
|
78
|
+
"""Explicit pop_size must be honoured exactly."""
|
|
79
|
+
print("\n2. Testing explicit pop_size...")
|
|
80
|
+
|
|
81
|
+
cmaes = CMAES(pop_size=50, sigma=0.5)
|
|
82
|
+
cmaes.initialize(make_problem(30))
|
|
83
|
+
|
|
84
|
+
assert cmaes.pop_size == 50, f"pop_size {cmaes.pop_size} != 50"
|
|
85
|
+
assert tuple(cmaes.population.shape) == (50, 30)
|
|
86
|
+
print(f" pop_size={cmaes.pop_size} "
|
|
87
|
+
f"population={tuple(cmaes.population.shape)} ✓")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_step_no_crash():
|
|
91
|
+
"""The first update must not raise a size mismatch (the regression)."""
|
|
92
|
+
print("\n3. Testing step() with auto pop_size (regression)...")
|
|
93
|
+
|
|
94
|
+
cmaes = CMAES(sigma=0.5)
|
|
95
|
+
cmaes.initialize(make_problem(30))
|
|
96
|
+
for _ in range(3):
|
|
97
|
+
cmaes.step()
|
|
98
|
+
print(f" 3 steps ran; generation={cmaes.generation}, "
|
|
99
|
+
f"pop_size={cmaes.pop_size} ✓")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_convergence():
|
|
103
|
+
"""End-to-end minimize() should converge on the sphere function."""
|
|
104
|
+
print("\n4. Testing convergence via minimize()...")
|
|
105
|
+
|
|
106
|
+
problem = make_problem(10)
|
|
107
|
+
cmaes = CMAES(sigma=0.5) # auto pop_size
|
|
108
|
+
result = minimize(
|
|
109
|
+
problem, cmaes, termination=MaxEvaluations(5000),
|
|
110
|
+
seed=42, verbose=False,
|
|
111
|
+
)
|
|
112
|
+
assert result.best_fitness < 1.0, (
|
|
113
|
+
f"did not converge: best_fitness={result.best_fitness}"
|
|
114
|
+
)
|
|
115
|
+
print(f" best_fitness={result.best_fitness:.6f} (< 1.0) ✓")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def run_all_tests():
|
|
119
|
+
"""Run all CMA-ES tests."""
|
|
120
|
+
print("\n" + "#"*60)
|
|
121
|
+
print("# EvoGrad CMA-ES Tests")
|
|
122
|
+
print("#"*60)
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
test_auto_pop_size()
|
|
126
|
+
test_explicit_pop_size()
|
|
127
|
+
test_step_no_crash()
|
|
128
|
+
test_convergence()
|
|
129
|
+
|
|
130
|
+
print("\n" + "="*60)
|
|
131
|
+
print("✓ ALL CMA-ES TESTS PASSED!")
|
|
132
|
+
print("="*60)
|
|
133
|
+
return True
|
|
134
|
+
except Exception as e:
|
|
135
|
+
print(f"\n✗ TEST FAILED: {e}")
|
|
136
|
+
import traceback
|
|
137
|
+
traceback.print_exc()
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
if __name__ == "__main__":
|
|
142
|
+
success = run_all_tests()
|
|
143
|
+
sys.exit(0 if success else 1)
|
|
@@ -85,10 +85,10 @@ def test_ga_creation():
|
|
|
85
85
|
print("\n2. Testing GA with custom operators...")
|
|
86
86
|
ga = GA(
|
|
87
87
|
pop_size=30,
|
|
88
|
-
selection=RouletteSelection(
|
|
89
|
-
crossover=BlendCrossover(alpha=0.5,
|
|
90
|
-
mutation=GaussianMutation(sigma=0.1,
|
|
91
|
-
survival=MergeSurvival(elitism=True, n_elite=2,
|
|
88
|
+
selection=RouletteSelection(adaptive=True),
|
|
89
|
+
crossover=BlendCrossover(alpha=0.5, adaptive=True),
|
|
90
|
+
mutation=GaussianMutation(sigma=0.1, adaptive=True),
|
|
91
|
+
survival=MergeSurvival(elitism=True, n_elite=2, adaptive=True),
|
|
92
92
|
)
|
|
93
93
|
print(f" Created: {ga}")
|
|
94
94
|
print(f" Survival: {ga.survival}")
|
|
@@ -165,10 +165,10 @@ def test_ga_step():
|
|
|
165
165
|
ga = GA(
|
|
166
166
|
pop_size=30,
|
|
167
167
|
sampling=UniformSampling(seed=42),
|
|
168
|
-
selection=TournamentSelection(tournament_size=3,
|
|
169
|
-
crossover=SBXCrossover(eta=15, prob=0.9,
|
|
170
|
-
mutation=PolynomialMutation(eta=20,
|
|
171
|
-
survival=MergeSurvival(n_survive=30, elitism=True, n_elite=1,
|
|
168
|
+
selection=TournamentSelection(tournament_size=3, adaptive=True),
|
|
169
|
+
crossover=SBXCrossover(eta=15, prob=0.9, adaptive=True),
|
|
170
|
+
mutation=PolynomialMutation(eta=20, adaptive=True),
|
|
171
|
+
survival=MergeSurvival(n_survive=30, elitism=True, n_elite=1, adaptive=True),
|
|
172
172
|
differentiable=True,
|
|
173
173
|
)
|
|
174
174
|
ga.initialize(problem)
|
|
@@ -212,9 +212,7 @@ def test_survival_strategies():
|
|
|
212
212
|
print("\n1. Testing MergeSurvival (mu+lambda)...")
|
|
213
213
|
ga_plus = GA(
|
|
214
214
|
pop_size=20,
|
|
215
|
-
survival=MergeSurvival(n_survive=20, elitism=True, n_elite=1),
|
|
216
|
-
seed=42,
|
|
217
|
-
)
|
|
215
|
+
survival=MergeSurvival(n_survive=20, elitism=True, n_elite=1), )
|
|
218
216
|
ga_plus.initialize(problem)
|
|
219
217
|
for _ in range(5):
|
|
220
218
|
ga_plus.step()
|
|
@@ -225,9 +223,7 @@ def test_survival_strategies():
|
|
|
225
223
|
ga_comma_inst = GA(
|
|
226
224
|
pop_size=20,
|
|
227
225
|
n_offsprings=40, # Must be >= pop_size
|
|
228
|
-
survival=CommaSurvival(n_survive=20, elitism=True, n_elite=1),
|
|
229
|
-
seed=42,
|
|
230
|
-
)
|
|
226
|
+
survival=CommaSurvival(n_survive=20, elitism=True, n_elite=1), )
|
|
231
227
|
ga_comma_inst.initialize(problem)
|
|
232
228
|
for _ in range(5):
|
|
233
229
|
ga_comma_inst.step()
|
|
@@ -238,9 +234,7 @@ def test_survival_strategies():
|
|
|
238
234
|
ga_replace = GA(
|
|
239
235
|
pop_size=20,
|
|
240
236
|
n_offsprings=5,
|
|
241
|
-
survival=ReplaceWorstSurvival(n_survive=20, elitism=True, n_elite=1),
|
|
242
|
-
seed=42,
|
|
243
|
-
)
|
|
237
|
+
survival=ReplaceWorstSurvival(n_survive=20, elitism=True, n_elite=1), )
|
|
244
238
|
ga_replace.initialize(problem)
|
|
245
239
|
for _ in range(20): # More generations since fewer offspring per gen
|
|
246
240
|
ga_replace.step()
|
|
@@ -250,9 +244,7 @@ def test_survival_strategies():
|
|
|
250
244
|
print("\n4. Testing FitnessSurvival (pure truncation)...")
|
|
251
245
|
ga_fitness = GA(
|
|
252
246
|
pop_size=20,
|
|
253
|
-
survival=FitnessSurvival(n_survive=20),
|
|
254
|
-
seed=42,
|
|
255
|
-
)
|
|
247
|
+
survival=FitnessSurvival(n_survive=20), )
|
|
256
248
|
ga_fitness.initialize(problem)
|
|
257
249
|
for _ in range(5):
|
|
258
250
|
ga_fitness.step()
|
|
@@ -278,9 +270,7 @@ def test_elitism():
|
|
|
278
270
|
print("\n1. Testing GA with elitism...")
|
|
279
271
|
ga_elite = GA(
|
|
280
272
|
pop_size=20,
|
|
281
|
-
survival=MergeSurvival(n_survive=20, elitism=True, n_elite=1),
|
|
282
|
-
seed=42,
|
|
283
|
-
)
|
|
273
|
+
survival=MergeSurvival(n_survive=20, elitism=True, n_elite=1), )
|
|
284
274
|
ga_elite.initialize(problem)
|
|
285
275
|
|
|
286
276
|
best_values = [ga_elite.best_fitness]
|
|
@@ -299,7 +289,6 @@ def test_elitism():
|
|
|
299
289
|
ga_no_elite = GA(
|
|
300
290
|
pop_size=20,
|
|
301
291
|
survival=FitnessSurvival(n_survive=20), # No elitism
|
|
302
|
-
seed=42,
|
|
303
292
|
)
|
|
304
293
|
ga_no_elite.initialize(problem)
|
|
305
294
|
|
|
@@ -325,9 +314,7 @@ def test_differentiable_mode():
|
|
|
325
314
|
|
|
326
315
|
ga = GA(
|
|
327
316
|
pop_size=20,
|
|
328
|
-
differentiable=True,
|
|
329
|
-
seed=42,
|
|
330
|
-
)
|
|
317
|
+
differentiable=True, )
|
|
331
318
|
ga.initialize(problem)
|
|
332
319
|
|
|
333
320
|
# Create optimizer for learnable parameters
|
|
@@ -386,9 +373,7 @@ def test_state_persistence():
|
|
|
386
373
|
sampling=UniformSampling(seed=42),
|
|
387
374
|
selection=TournamentSelection(tournament_size=3),
|
|
388
375
|
crossover=SBXCrossover(eta=15, prob=0.9),
|
|
389
|
-
mutation=PolynomialMutation(eta=20),
|
|
390
|
-
seed=42,
|
|
391
|
-
)
|
|
376
|
+
mutation=PolynomialMutation(eta=20), )
|
|
392
377
|
ga1.initialize(problem)
|
|
393
378
|
|
|
394
379
|
for _ in range(10):
|
|
@@ -408,9 +393,7 @@ def test_state_persistence():
|
|
|
408
393
|
sampling=UniformSampling(seed=42),
|
|
409
394
|
selection=TournamentSelection(tournament_size=3),
|
|
410
395
|
crossover=SBXCrossover(eta=15, prob=0.9),
|
|
411
|
-
mutation=PolynomialMutation(eta=20),
|
|
412
|
-
seed=0, # Different seed
|
|
413
|
-
)
|
|
396
|
+
mutation=PolynomialMutation(eta=20), )
|
|
414
397
|
ga2.initialize(problem)
|
|
415
398
|
|
|
416
399
|
print(f"\n2. New GA before load:")
|
|
@@ -475,9 +458,7 @@ def test_convergence():
|
|
|
475
458
|
selection=TournamentSelection(tournament_size=3),
|
|
476
459
|
crossover=SBXCrossover(eta=15, prob=0.9),
|
|
477
460
|
mutation=PolynomialMutation(eta=20),
|
|
478
|
-
survival=MergeSurvival(n_survive=50, elitism=True, n_elite=1),
|
|
479
|
-
seed=42,
|
|
480
|
-
)
|
|
461
|
+
survival=MergeSurvival(n_survive=50, elitism=True, n_elite=1), )
|
|
481
462
|
ga.initialize(problem)
|
|
482
463
|
|
|
483
464
|
print(f"\n1. Running GA for 100 generations...")
|
|
@@ -513,10 +494,10 @@ def test_hyperparams():
|
|
|
513
494
|
|
|
514
495
|
ga = GA(
|
|
515
496
|
pop_size=20,
|
|
516
|
-
selection=TournamentSelection(tournament_size=3,
|
|
517
|
-
crossover=SBXCrossover(eta=15, prob=0.9,
|
|
518
|
-
mutation=PolynomialMutation(eta=20,
|
|
519
|
-
survival=MergeSurvival(n_survive=20, elitism=True, n_elite=2,
|
|
497
|
+
selection=TournamentSelection(tournament_size=3, adaptive=True),
|
|
498
|
+
crossover=SBXCrossover(eta=15, prob=0.9, adaptive=True),
|
|
499
|
+
mutation=PolynomialMutation(eta=20, adaptive=True),
|
|
500
|
+
survival=MergeSurvival(n_survive=20, elitism=True, n_elite=2, adaptive=True),
|
|
520
501
|
)
|
|
521
502
|
ga.initialize(problem)
|
|
522
503
|
ga.step()
|
|
@@ -170,7 +170,7 @@ def test_selection():
|
|
|
170
170
|
print("\n2. Testing TournamentSelection (differentiable)...")
|
|
171
171
|
tournament_diff = TournamentSelection(
|
|
172
172
|
tournament_size=3,
|
|
173
|
-
|
|
173
|
+
adaptive=True,
|
|
174
174
|
temperature=1.0,
|
|
175
175
|
learn_temperature=True,
|
|
176
176
|
)
|
|
@@ -247,7 +247,7 @@ def test_crossover():
|
|
|
247
247
|
sbx_diff = SBXCrossover(
|
|
248
248
|
eta=15,
|
|
249
249
|
prob=0.9,
|
|
250
|
-
|
|
250
|
+
adaptive=True,
|
|
251
251
|
learn_eta=True,
|
|
252
252
|
learn_prob=True,
|
|
253
253
|
)
|
|
@@ -274,7 +274,7 @@ def test_crossover():
|
|
|
274
274
|
print(f" Offspring shape: {offspring_bin.shape}")
|
|
275
275
|
|
|
276
276
|
# Test differentiable binomial
|
|
277
|
-
binomial_diff = BinomialCrossover(cr=0.9,
|
|
277
|
+
binomial_diff = BinomialCrossover(cr=0.9, adaptive=True, learn_cr=True)
|
|
278
278
|
p1 = torch.nn.Parameter(parent1.clone())
|
|
279
279
|
offspring_bin_diff = binomial_diff(p1, parent2)
|
|
280
280
|
loss = offspring_bin_diff.sum()
|
|
@@ -343,7 +343,7 @@ def test_mutation():
|
|
|
343
343
|
poly_diff = PolynomialMutation(
|
|
344
344
|
eta=20,
|
|
345
345
|
prob=0.1,
|
|
346
|
-
|
|
346
|
+
adaptive=True,
|
|
347
347
|
learn_eta=True,
|
|
348
348
|
learn_prob=True,
|
|
349
349
|
)
|
|
@@ -367,7 +367,7 @@ def test_mutation():
|
|
|
367
367
|
print(f" With sigma_frac: ✓")
|
|
368
368
|
|
|
369
369
|
# Test differentiable Gaussian
|
|
370
|
-
gauss_diff = GaussianMutation(sigma=0.1,
|
|
370
|
+
gauss_diff = GaussianMutation(sigma=0.1, adaptive=True, learn_sigma=True)
|
|
371
371
|
pop_param = torch.nn.Parameter(population.clone())
|
|
372
372
|
mutated_gauss_diff = gauss_diff(pop_param, xl, xu)
|
|
373
373
|
loss = mutated_gauss_diff.sum()
|
|
@@ -592,18 +592,18 @@ def test_operator_integration():
|
|
|
592
592
|
# Create differentiable operators
|
|
593
593
|
selection_diff = TournamentSelection(
|
|
594
594
|
tournament_size=3,
|
|
595
|
-
|
|
595
|
+
adaptive=True,
|
|
596
596
|
temperature=1.0,
|
|
597
597
|
)
|
|
598
598
|
crossover_diff = SBXCrossover(
|
|
599
599
|
eta=15,
|
|
600
600
|
prob=0.9,
|
|
601
|
-
|
|
601
|
+
adaptive=True,
|
|
602
602
|
learn_eta=True,
|
|
603
603
|
)
|
|
604
604
|
mutation_diff = GaussianMutation(
|
|
605
605
|
sigma=0.1,
|
|
606
|
-
|
|
606
|
+
adaptive=True,
|
|
607
607
|
learn_sigma=True,
|
|
608
608
|
)
|
|
609
609
|
|
|
@@ -266,7 +266,7 @@ def test_differentiable_mode():
|
|
|
266
266
|
|
|
267
267
|
# Differentiable operators
|
|
268
268
|
print("\n1. SBXCrossover with gradient flow:")
|
|
269
|
-
sbx = SBXCrossover(eta=15, prob=0.9,
|
|
269
|
+
sbx = SBXCrossover(eta=15, prob=0.9, adaptive=True, learn_eta=True)
|
|
270
270
|
|
|
271
271
|
p1 = torch.nn.Parameter(torch.randn(N, D))
|
|
272
272
|
p2 = torch.randn(N, D)
|
|
@@ -282,7 +282,7 @@ def test_differentiable_mode():
|
|
|
282
282
|
|
|
283
283
|
# PolynomialMutation with gradient flow
|
|
284
284
|
print("\n2. PolynomialMutation with gradient flow:")
|
|
285
|
-
mutation = PolynomialMutation(eta=20, prob=0.1,
|
|
285
|
+
mutation = PolynomialMutation(eta=20, prob=0.1, adaptive=True, learn_eta=True)
|
|
286
286
|
|
|
287
287
|
x = torch.nn.Parameter(torch.randn(N, D))
|
|
288
288
|
xl = torch.zeros(D)
|
|
@@ -305,22 +305,30 @@ def main():
|
|
|
305
305
|
print("\n" + "#" * 70)
|
|
306
306
|
print("# Per-Individual/Per-Gene Parameter Support Tests")
|
|
307
307
|
print("#" * 70)
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
test_crossover_configurations()
|
|
311
|
+
test_mutation_configurations()
|
|
312
|
+
test_shade_style_usage()
|
|
313
|
+
test_differentiable_mode()
|
|
314
|
+
|
|
315
|
+
print("\n" + "=" * 70)
|
|
316
|
+
print("ALL TESTS PASSED! ✓")
|
|
317
|
+
print("=" * 70)
|
|
318
|
+
|
|
319
|
+
print("\nSummary of Four Configurations:")
|
|
320
|
+
print(" 1. Fixed (scalar) - Same value for all")
|
|
321
|
+
print(" 2. Per-gene [D] - Different per variable")
|
|
322
|
+
print(" 3. Per-individual [N] - Different per individual (SHADE needs this!)")
|
|
323
|
+
print(" 4. Full matrix [N, D] - Maximum flexibility")
|
|
324
|
+
return True
|
|
325
|
+
except Exception as e:
|
|
326
|
+
print(f"\n✗ TEST FAILED: {e}")
|
|
327
|
+
import traceback
|
|
328
|
+
traceback.print_exc()
|
|
329
|
+
return False
|
|
323
330
|
|
|
324
331
|
|
|
325
332
|
if __name__ == "__main__":
|
|
326
|
-
main()
|
|
333
|
+
success = main()
|
|
334
|
+
sys.exit(0 if success else 1)
|
|
@@ -183,8 +183,10 @@ def test_callbacks():
|
|
|
183
183
|
|
|
184
184
|
print(f" Tracked generations: {len(history_cb.generations)}")
|
|
185
185
|
print(f" Best fitness history: {history_cb.best_fitness[:3]}...")
|
|
186
|
-
|
|
187
|
-
|
|
186
|
+
# on_optimisation_start records the initial state, then each of the 5
|
|
187
|
+
# on_generation_end calls records one more -> 1 + 5 = 6 entries.
|
|
188
|
+
assert len(history_cb.generations) == 6
|
|
189
|
+
assert len(history_cb.best_fitness) == 6
|
|
188
190
|
|
|
189
191
|
# Test PrintCallback
|
|
190
192
|
print("\n2. Testing PrintCallback...")
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "evograd-diff"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "PyTorch-based framework for differentiable evolutionary computation and swarm intelligence"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "Apache-2.0" }
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
from evograd.core import Problem, minimize, MaxEvaluations
|
|
3
|
+
from evograd.algorithms import GA, DE, PSO, CMAES
|
|
4
|
+
|
|
5
|
+
# Define an optimisation problem
|
|
6
|
+
problem = Problem(
|
|
7
|
+
objective=lambda x: (x**2).sum(dim=-1), # Sphere function
|
|
8
|
+
n_var=30,
|
|
9
|
+
xl=-100.0,
|
|
10
|
+
xu=100.0,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
# Run with Genetic Algorithm
|
|
14
|
+
ga = GA(pop_size=100, differentiable=True)
|
|
15
|
+
result = minimize(problem, ga, termination=MaxEvaluations(10000), seed=42)
|
|
16
|
+
print(f"GA Best: {result.best_fitness:.6f}")
|
|
17
|
+
|
|
18
|
+
# Run with Differential Evolution
|
|
19
|
+
de = DE(pop_size=100, variant="DE/rand/1/bin", adaptive=True)
|
|
20
|
+
result = minimize(problem, de, termination=MaxEvaluations(10000), seed=42)
|
|
21
|
+
print(f"DE Best: {result.best_fitness:.6f}")
|
|
22
|
+
|
|
23
|
+
# Run with Particle Swarm Optimisation
|
|
24
|
+
pso = PSO(pop_size=100, adaptive=True, differentiable=True)
|
|
25
|
+
result = minimize(problem, pso, termination=MaxEvaluations(10000), seed=42)
|
|
26
|
+
print(f"PSO Best: {result.best_fitness:.6f}")
|
|
27
|
+
|
|
28
|
+
# Run with CMA-ES
|
|
29
|
+
cmaes = CMAES(sigma=0.5, adaptive=True)
|
|
30
|
+
result = minimize(problem, cmaes, termination=MaxEvaluations(10000), seed=42)
|
|
31
|
+
print(f"CMA-ES Best: {result.best_fitness:.6f}")
|
|
32
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from evograd.algorithms import GA
|
|
2
|
+
from evograd.operators import (
|
|
3
|
+
RouletteSelection,
|
|
4
|
+
SBXCrossover,
|
|
5
|
+
PolynomialMutation,
|
|
6
|
+
MergeSurvival,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
# Classical GA (no gradients)
|
|
10
|
+
ga = GA(pop_size=100, differentiable=False)
|
|
11
|
+
|
|
12
|
+
# Fully differentiable GA with custom operators
|
|
13
|
+
ga = GA(
|
|
14
|
+
pop_size=100,
|
|
15
|
+
selection=RouletteSelection(differentiable=True, learn_temperature=True),
|
|
16
|
+
crossover=SBXCrossover(differentiable=True, learn_eta=True, learn_prob=True),
|
|
17
|
+
mutation=PolynomialMutation(differentiable=True, learn_eta=True, learn_prob=True),
|
|
18
|
+
survival=MergeSurvival(selection=RouletteSelection(differentiable=True)),
|
|
19
|
+
differentiable=True, # Makes population learnable
|
|
20
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{evograd_diff-0.1.0 → evograd_diff-0.1.2}/evograd/benchmarks/functions/cec2017/composition.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|