evograd-diff 0.1.0__tar.gz → 0.1.1__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.1}/.claude/settings.local.json +5 -1
- evograd_diff-0.1.1/.gitignore +15 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/PKG-INFO +19 -12
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/README.md +18 -11
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/algorithms/cmaes.py +8 -5
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/core/algorithm.py +20 -4
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/tests/run_all.py +22 -1
- evograd_diff-0.1.1/evograd/tests/test_cmaes.py +143 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/tests/test_ga.py +21 -40
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/tests/test_operators.py +8 -8
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/tests/test_per_individual.py +26 -18
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/tests/test_utils.py +4 -2
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/pyproject.toml +1 -1
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/uv.lock +2 -2
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/.python-version +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/LICENSE +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/Test_new_evograd.ipynb +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/__init__.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/algorithms/__init__.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/algorithms/de.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/algorithms/ga.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/algorithms/pso.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/algorithms/shade.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/benchmarks/functions/__init__.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/benchmarks/functions/base.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/benchmarks/functions/cec2017/__init__.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/benchmarks/functions/cec2017/basic.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/benchmarks/functions/cec2017/composition.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/benchmarks/functions/cec2017/data.pkl +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/benchmarks/functions/cec2017/data.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/benchmarks/functions/cec2017/hybrid.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/benchmarks/functions/cec2017/simple.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/benchmarks/functions/classical.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/benchmarks/functions/smoothed_funnel.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/benchmarks/functions/transforms.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/benchmarks/run_benchmark_functions.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/core/__init__.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/core/maximize.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/core/minimize.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/core/problem.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/core/result.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/core/termination.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/operators/__init__.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/operators/crossover.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/operators/mutation.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/operators/relaxations.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/operators/repair.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/operators/sampling.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/operators/selection.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/operators/survival.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/tests/__init__.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/tests/test_core.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/utils/__init__.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/utils/callbacks.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/utils/device.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/utils/duplicates.py +0 -0
- {evograd_diff-0.1.0 → evograd_diff-0.1.1}/plot_benchmarks.py +0 -0
|
@@ -6,7 +6,11 @@
|
|
|
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 *)"
|
|
10
14
|
]
|
|
11
15
|
}
|
|
12
16
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: evograd-diff
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
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
|
|
|
@@ -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
|
|
|
@@ -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.1"
|
|
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" }
|
|
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.1}/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
|