evograd-diff 0.1.2__tar.gz → 0.2.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.2 → evograd_diff-0.2.1}/PKG-INFO +84 -4
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/README.md +83 -3
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/__init__.py +1 -1
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/algorithms/cmaes.py +4 -7
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/algorithms/de.py +4 -4
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/algorithms/pso.py +3 -2
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/algorithms/shade.py +7 -8
- evograd_diff-0.2.1/evograd/benchmarks/functions/cec2017/_base.py +51 -0
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/benchmarks/functions/cec2017/hybrid.py +10 -40
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/benchmarks/functions/cec2017/simple.py +9 -46
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/core/algorithm.py +53 -9
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/core/maximize.py +4 -0
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/core/minimize.py +19 -3
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/operators/crossover.py +3 -5
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/operators/mutation.py +6 -8
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/operators/relaxations.py +30 -1
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/operators/repair.py +11 -1
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/operators/sampling.py +5 -7
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/operators/selection.py +2 -4
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/tests/run_all.py +21 -0
- evograd_diff-0.2.1/evograd/tests/test_de.py +62 -0
- evograd_diff-0.2.1/evograd/tests/test_pso.py +62 -0
- evograd_diff-0.2.1/evograd/tests/test_shade.py +64 -0
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/pyproject.toml +12 -1
- evograd_diff-0.1.2/.claude/settings.local.json +0 -17
- evograd_diff-0.1.2/.python-version +0 -1
- evograd_diff-0.1.2/Test_new_evograd.ipynb +0 -31427
- evograd_diff-0.1.2/plot_benchmarks.py +0 -896
- evograd_diff-0.1.2/test.py +0 -32
- evograd_diff-0.1.2/test2.py +0 -20
- evograd_diff-0.1.2/uv.lock +0 -1014
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/.gitignore +0 -0
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/LICENSE +0 -0
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/algorithms/__init__.py +0 -0
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/algorithms/ga.py +0 -0
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/benchmarks/functions/__init__.py +0 -0
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/benchmarks/functions/base.py +0 -0
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/benchmarks/functions/cec2017/__init__.py +0 -0
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/benchmarks/functions/cec2017/basic.py +0 -0
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/benchmarks/functions/cec2017/composition.py +0 -0
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/benchmarks/functions/cec2017/data.pkl +0 -0
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/benchmarks/functions/cec2017/data.py +0 -0
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/benchmarks/functions/classical.py +0 -0
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/benchmarks/functions/smoothed_funnel.py +0 -0
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/benchmarks/functions/transforms.py +0 -0
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/benchmarks/run_benchmark_functions.py +0 -0
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/core/__init__.py +0 -0
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/core/problem.py +0 -0
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/core/result.py +0 -0
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/core/termination.py +0 -0
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/operators/__init__.py +0 -0
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/operators/survival.py +0 -0
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/tests/__init__.py +0 -0
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/tests/test_cmaes.py +0 -0
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/tests/test_core.py +0 -0
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/tests/test_ga.py +0 -0
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/tests/test_operators.py +0 -0
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/tests/test_per_individual.py +0 -0
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/tests/test_utils.py +0 -0
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/utils/__init__.py +0 -0
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/utils/callbacks.py +0 -0
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/utils/device.py +0 -0
- {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/utils/duplicates.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: evograd-diff
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.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
|
|
@@ -25,15 +25,18 @@ Requires-Dist: seaborn>=0.13.2
|
|
|
25
25
|
Requires-Dist: torch>=2.11.0
|
|
26
26
|
Description-Content-Type: text/markdown
|
|
27
27
|
|
|
28
|
-
# EvoGrad: Metaheuristics in a Differentiable Wonderland
|
|
28
|
+
# EvoGrad: Accelerated Metaheuristics in a Differentiable Wonderland
|
|
29
29
|
|
|
30
30
|
<p align="center">
|
|
31
31
|
<img src="https://img.shields.io/badge/python-3.9+-blue.svg" alt="Python 3.9+">
|
|
32
32
|
<img src="https://img.shields.io/badge/pytorch-2.0+-orange.svg" alt="PyTorch 2.0+">
|
|
33
33
|
<img src="https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg" alt="License: Apache-2.0">
|
|
34
|
+
<img src="https://img.shields.io/badge/IEEE%20CEC-2026-success.svg" alt="IEEE CEC 2026">
|
|
34
35
|
|
|
35
36
|
</p>
|
|
36
37
|
|
|
38
|
+
> 🎉 **EvoGrad has been accepted at [IEEE CEC 2026](#-citation)!**
|
|
39
|
+
|
|
37
40
|
**EvoGrad** is a PyTorch-based framework for differentiable Evolutionary Computation and Swarm Intelligence. It bridges classical population-based optimisation with modern differentiable programming by enabling gradient flow through evolutionary operators.
|
|
38
41
|
|
|
39
42
|
## 🌟 Key Features
|
|
@@ -421,11 +424,88 @@ EvoGrad makes evolutionary algorithms differentiable through:
|
|
|
421
424
|
|
|
422
425
|
## 📊 Benchmarks
|
|
423
426
|
|
|
424
|
-
|
|
427
|
+
EvoGrad ships a self-contained, **PyTorch-native benchmark suite** (`evograd.benchmarks`) together with a parallel runner that evaluates every algorithm in its four operating modes against two reference baselines.
|
|
428
|
+
|
|
429
|
+
### Function library
|
|
430
|
+
|
|
431
|
+
All functions share a common `BenchmarkFunction` interface (`f(x)` on an `(N, n_var)` batch, plus `.bounds` and the known optimum) and run on CPU/GPU/MPS.
|
|
432
|
+
|
|
433
|
+
| Category | Functions |
|
|
434
|
+
|----------|-----------|
|
|
435
|
+
| **Classical — unimodal** | Sphere, Ellipsoid, SumOfDifferentPowers, Schwefel 2.22, Cigar, Discus, BentCigar, Rosenbrock, DixonPrice, Powell, Trid |
|
|
436
|
+
| **Classical — multimodal** | Rastrigin, Ackley, Griewank, Schwefel, Levy, Michalewicz, Zakharov, Weierstrass, Alpine, Salomon, Styblinski–Tang |
|
|
437
|
+
| **CEC 2017** (`F1`–`F30`) | Simple/unimodal (F1–F10), Hybrid (F11–F20), Composition (F21–F30) — the full competition suite, **rewritten from scratch in PyTorch** |
|
|
438
|
+
| **Multi-Basin / Smoothed-Funnel** | `MultiBasinRastrigin`, `MultiBasinRosenbrock`, `DeceptiveLandscape` — designed for differentiable EAs |
|
|
439
|
+
| **Transforms** | Shifted / Rotated / Scaled / Asymmetric / Oscillated / Biased wrappers for building custom variants |
|
|
440
|
+
|
|
441
|
+
```python
|
|
442
|
+
import torch
|
|
443
|
+
from evograd.benchmarks.functions import Sphere, Rastrigin, get_cec2017_function, MultiBasinRastrigin
|
|
444
|
+
|
|
445
|
+
f = get_cec2017_function(14, n_var=30) # CEC 2017 F14 in 30D
|
|
446
|
+
y = f(torch.randn(100, 30)) # batch evaluation -> shape [100]
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
The **Multi-Basin** functions aggregate `K` basins (each a full Rastrigin/Rosenbrock landscape) with a smooth *log-sum-exp* minimum, so the surface stays differentiable everywhere while still trapping pure gradient descent in distractor basins — exactly the setting where population search combined with gradient refinement pays off.
|
|
450
|
+
|
|
451
|
+
### Running the benchmarks
|
|
452
|
+
|
|
453
|
+
The runner evaluates the four EvoGrad modes — **Classical**, **Differentiable**, **Adaptive**, **Full** — and, by default, the **pymoo** and **Adam** (multi-start) baselines:
|
|
454
|
+
|
|
455
|
+
```bash
|
|
456
|
+
# 30 runs of DE on the full CEC 2017 suite in 30D (vs pymoo + Adam)
|
|
457
|
+
python -m evograd.benchmarks.run_benchmark_functions -a DE -s cec2017 -D 30 -r 30
|
|
458
|
+
|
|
459
|
+
# CMA-ES on the multi-basin functions, on GPU
|
|
460
|
+
python -m evograd.benchmarks.run_benchmark_functions -a CMAES -s funnel -D 30 --device cuda
|
|
461
|
+
|
|
462
|
+
# List every available function and suite
|
|
463
|
+
python -m evograd.benchmarks.run_benchmark_functions --list_functions
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Key flags: `-a {DE,SHADE,PSO,GA,CMAES,ADAM}`, `-s` suite (`classical`, `standard`, `cec2017[_simple|_hybrid|_composition]`, `funnel`, …), `-D` dimensionality, `-r` runs, `-p` population size, `--no_pymoo` / `--no_adam` to drop baselines. Plotting utilities live in `plot_benchmarks.py`.
|
|
467
|
+
|
|
468
|
+
### Results
|
|
469
|
+
|
|
470
|
+
The three differentiable variants are compared against the **Classical** baseline and pymoo:
|
|
471
|
+
|
|
472
|
+
- **Adaptive** — learnable hyperparameters, purely stochastic variation (no gradient through the population).
|
|
473
|
+
- **Diff** (Differentiable) — fixed hyperparameters, gradients refine the population.
|
|
474
|
+
- **Full** — both: learnable hyperparameters *and* gradient-based population refinement.
|
|
475
|
+
|
|
476
|
+
**CEC 2017 (30D & 100D).** 29 functions (F2 excluded, per the competition), search space `[-100, 100]^D`, 100 individuals, `10000·D` evaluations, 30 independent paired runs, one-sided Wilcoxon signed-rank test with Benjamini–Hochberg correction. Highlights:
|
|
477
|
+
|
|
478
|
+
- Differentiable variants are **statistically significantly better than the classical baseline in ~31% of all comparisons**, and **never substantially worse** — gradient refinement can be added to EAs safely.
|
|
479
|
+
- Gains concentrate where local refinement helps most: **GA (70.1%)** and **DE (46.0%)** of comparisons improved, versus **PSO (6.9%)** and **CMA-ES (1.1%)**, which already include strong built-in adaptation.
|
|
480
|
+
- Across variants, **Full (41.4%) > Adaptive (35.3%) > Diff (16.4%)** — combining hyperparameter learning with population refinement helps the most, increasingly so at 100D.
|
|
481
|
+
- CMA-ES is the strongest method overall (especially on hybrid/composition functions), and EvoGrad runs ~**3× faster** than the pymoo baselines on CPU *despite* the added gradient computation.
|
|
482
|
+
|
|
483
|
+
**Multi-Basin Rastrigin** (`D=30`, bounds `[-5, 5]^D`, 150,000 evaluations, 30 runs). Every CMA-ES variant locates the global basin (best fitness `0.00`); a multi-start **Adam** baseline (100 parallel solutions) stays trapped in distractor basins:
|
|
484
|
+
|
|
485
|
+
| Configuration | Best | Mean | Std | Time (s) |
|
|
486
|
+
|---|---|---|---|---|
|
|
487
|
+
| CMA-ES Classical | 0.00 | 2.22 | 3.04 | 25.66 |
|
|
488
|
+
| CMA-ES Differentiable | 0.00 | 1.49 | 2.16 | 9.77 |
|
|
489
|
+
| CMA-ES Adaptive | 0.00 | **0.99** | **1.36** | 45.24 |
|
|
490
|
+
| CMA-ES Full | 0.00 | 1.29 | 2.12 | **7.94** |
|
|
491
|
+
| Adam (multi-start, pop-based) | 116.41 | 153.77 | 13.98 | 3.88 |
|
|
492
|
+
|
|
493
|
+
The **Adaptive** variant reaches the lowest mean/variance, while **Full** matches it closely at the **fastest** runtime — gradient flow yields large speed-ups while population search secures the global basin. Adam alone is **>2 orders of magnitude worse**, confirming that pure gradient descent cannot escape distractor basins.
|
|
494
|
+
|
|
495
|
+
> Full experimental details are in the paper (see [Citation](#-citation)).
|
|
425
496
|
|
|
426
497
|
## 📖 Citation
|
|
427
498
|
|
|
428
|
-
|
|
499
|
+
EvoGrad was accepted at the **IEEE Congress on Evolutionary Computation (CEC) 2026**. If you use EvoGrad in your research, please cite:
|
|
500
|
+
|
|
501
|
+
```bibtex
|
|
502
|
+
@inproceedings{citterio2026evograd,
|
|
503
|
+
title = {{EvoGrad}: Accelerated Metaheuristics in a Differentiable Wonderland},
|
|
504
|
+
author = {Citterio, Beatrice F. R. and Papetti, Daniele M. and Dimitri, Giovanna Maria and Tangherloni, Andrea},
|
|
505
|
+
booktitle = {Proceedings of the IEEE Congress on Evolutionary Computation (CEC)},
|
|
506
|
+
year = {2026},
|
|
507
|
+
}
|
|
508
|
+
```
|
|
429
509
|
|
|
430
510
|
## 📄 License
|
|
431
511
|
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
# EvoGrad: Metaheuristics in a Differentiable Wonderland
|
|
1
|
+
# EvoGrad: Accelerated Metaheuristics in a Differentiable Wonderland
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
4
|
<img src="https://img.shields.io/badge/python-3.9+-blue.svg" alt="Python 3.9+">
|
|
5
5
|
<img src="https://img.shields.io/badge/pytorch-2.0+-orange.svg" alt="PyTorch 2.0+">
|
|
6
6
|
<img src="https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg" alt="License: Apache-2.0">
|
|
7
|
+
<img src="https://img.shields.io/badge/IEEE%20CEC-2026-success.svg" alt="IEEE CEC 2026">
|
|
7
8
|
|
|
8
9
|
</p>
|
|
9
10
|
|
|
11
|
+
> 🎉 **EvoGrad has been accepted at [IEEE CEC 2026](#-citation)!**
|
|
12
|
+
|
|
10
13
|
**EvoGrad** is a PyTorch-based framework for differentiable Evolutionary Computation and Swarm Intelligence. It bridges classical population-based optimisation with modern differentiable programming by enabling gradient flow through evolutionary operators.
|
|
11
14
|
|
|
12
15
|
## 🌟 Key Features
|
|
@@ -394,11 +397,88 @@ EvoGrad makes evolutionary algorithms differentiable through:
|
|
|
394
397
|
|
|
395
398
|
## 📊 Benchmarks
|
|
396
399
|
|
|
397
|
-
|
|
400
|
+
EvoGrad ships a self-contained, **PyTorch-native benchmark suite** (`evograd.benchmarks`) together with a parallel runner that evaluates every algorithm in its four operating modes against two reference baselines.
|
|
401
|
+
|
|
402
|
+
### Function library
|
|
403
|
+
|
|
404
|
+
All functions share a common `BenchmarkFunction` interface (`f(x)` on an `(N, n_var)` batch, plus `.bounds` and the known optimum) and run on CPU/GPU/MPS.
|
|
405
|
+
|
|
406
|
+
| Category | Functions |
|
|
407
|
+
|----------|-----------|
|
|
408
|
+
| **Classical — unimodal** | Sphere, Ellipsoid, SumOfDifferentPowers, Schwefel 2.22, Cigar, Discus, BentCigar, Rosenbrock, DixonPrice, Powell, Trid |
|
|
409
|
+
| **Classical — multimodal** | Rastrigin, Ackley, Griewank, Schwefel, Levy, Michalewicz, Zakharov, Weierstrass, Alpine, Salomon, Styblinski–Tang |
|
|
410
|
+
| **CEC 2017** (`F1`–`F30`) | Simple/unimodal (F1–F10), Hybrid (F11–F20), Composition (F21–F30) — the full competition suite, **rewritten from scratch in PyTorch** |
|
|
411
|
+
| **Multi-Basin / Smoothed-Funnel** | `MultiBasinRastrigin`, `MultiBasinRosenbrock`, `DeceptiveLandscape` — designed for differentiable EAs |
|
|
412
|
+
| **Transforms** | Shifted / Rotated / Scaled / Asymmetric / Oscillated / Biased wrappers for building custom variants |
|
|
413
|
+
|
|
414
|
+
```python
|
|
415
|
+
import torch
|
|
416
|
+
from evograd.benchmarks.functions import Sphere, Rastrigin, get_cec2017_function, MultiBasinRastrigin
|
|
417
|
+
|
|
418
|
+
f = get_cec2017_function(14, n_var=30) # CEC 2017 F14 in 30D
|
|
419
|
+
y = f(torch.randn(100, 30)) # batch evaluation -> shape [100]
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
The **Multi-Basin** functions aggregate `K` basins (each a full Rastrigin/Rosenbrock landscape) with a smooth *log-sum-exp* minimum, so the surface stays differentiable everywhere while still trapping pure gradient descent in distractor basins — exactly the setting where population search combined with gradient refinement pays off.
|
|
423
|
+
|
|
424
|
+
### Running the benchmarks
|
|
425
|
+
|
|
426
|
+
The runner evaluates the four EvoGrad modes — **Classical**, **Differentiable**, **Adaptive**, **Full** — and, by default, the **pymoo** and **Adam** (multi-start) baselines:
|
|
427
|
+
|
|
428
|
+
```bash
|
|
429
|
+
# 30 runs of DE on the full CEC 2017 suite in 30D (vs pymoo + Adam)
|
|
430
|
+
python -m evograd.benchmarks.run_benchmark_functions -a DE -s cec2017 -D 30 -r 30
|
|
431
|
+
|
|
432
|
+
# CMA-ES on the multi-basin functions, on GPU
|
|
433
|
+
python -m evograd.benchmarks.run_benchmark_functions -a CMAES -s funnel -D 30 --device cuda
|
|
434
|
+
|
|
435
|
+
# List every available function and suite
|
|
436
|
+
python -m evograd.benchmarks.run_benchmark_functions --list_functions
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
Key flags: `-a {DE,SHADE,PSO,GA,CMAES,ADAM}`, `-s` suite (`classical`, `standard`, `cec2017[_simple|_hybrid|_composition]`, `funnel`, …), `-D` dimensionality, `-r` runs, `-p` population size, `--no_pymoo` / `--no_adam` to drop baselines. Plotting utilities live in `plot_benchmarks.py`.
|
|
440
|
+
|
|
441
|
+
### Results
|
|
442
|
+
|
|
443
|
+
The three differentiable variants are compared against the **Classical** baseline and pymoo:
|
|
444
|
+
|
|
445
|
+
- **Adaptive** — learnable hyperparameters, purely stochastic variation (no gradient through the population).
|
|
446
|
+
- **Diff** (Differentiable) — fixed hyperparameters, gradients refine the population.
|
|
447
|
+
- **Full** — both: learnable hyperparameters *and* gradient-based population refinement.
|
|
448
|
+
|
|
449
|
+
**CEC 2017 (30D & 100D).** 29 functions (F2 excluded, per the competition), search space `[-100, 100]^D`, 100 individuals, `10000·D` evaluations, 30 independent paired runs, one-sided Wilcoxon signed-rank test with Benjamini–Hochberg correction. Highlights:
|
|
450
|
+
|
|
451
|
+
- Differentiable variants are **statistically significantly better than the classical baseline in ~31% of all comparisons**, and **never substantially worse** — gradient refinement can be added to EAs safely.
|
|
452
|
+
- Gains concentrate where local refinement helps most: **GA (70.1%)** and **DE (46.0%)** of comparisons improved, versus **PSO (6.9%)** and **CMA-ES (1.1%)**, which already include strong built-in adaptation.
|
|
453
|
+
- Across variants, **Full (41.4%) > Adaptive (35.3%) > Diff (16.4%)** — combining hyperparameter learning with population refinement helps the most, increasingly so at 100D.
|
|
454
|
+
- CMA-ES is the strongest method overall (especially on hybrid/composition functions), and EvoGrad runs ~**3× faster** than the pymoo baselines on CPU *despite* the added gradient computation.
|
|
455
|
+
|
|
456
|
+
**Multi-Basin Rastrigin** (`D=30`, bounds `[-5, 5]^D`, 150,000 evaluations, 30 runs). Every CMA-ES variant locates the global basin (best fitness `0.00`); a multi-start **Adam** baseline (100 parallel solutions) stays trapped in distractor basins:
|
|
457
|
+
|
|
458
|
+
| Configuration | Best | Mean | Std | Time (s) |
|
|
459
|
+
|---|---|---|---|---|
|
|
460
|
+
| CMA-ES Classical | 0.00 | 2.22 | 3.04 | 25.66 |
|
|
461
|
+
| CMA-ES Differentiable | 0.00 | 1.49 | 2.16 | 9.77 |
|
|
462
|
+
| CMA-ES Adaptive | 0.00 | **0.99** | **1.36** | 45.24 |
|
|
463
|
+
| CMA-ES Full | 0.00 | 1.29 | 2.12 | **7.94** |
|
|
464
|
+
| Adam (multi-start, pop-based) | 116.41 | 153.77 | 13.98 | 3.88 |
|
|
465
|
+
|
|
466
|
+
The **Adaptive** variant reaches the lowest mean/variance, while **Full** matches it closely at the **fastest** runtime — gradient flow yields large speed-ups while population search secures the global basin. Adam alone is **>2 orders of magnitude worse**, confirming that pure gradient descent cannot escape distractor basins.
|
|
467
|
+
|
|
468
|
+
> Full experimental details are in the paper (see [Citation](#-citation)).
|
|
398
469
|
|
|
399
470
|
## 📖 Citation
|
|
400
471
|
|
|
401
|
-
|
|
472
|
+
EvoGrad was accepted at the **IEEE Congress on Evolutionary Computation (CEC) 2026**. If you use EvoGrad in your research, please cite:
|
|
473
|
+
|
|
474
|
+
```bibtex
|
|
475
|
+
@inproceedings{citterio2026evograd,
|
|
476
|
+
title = {{EvoGrad}: Accelerated Metaheuristics in a Differentiable Wonderland},
|
|
477
|
+
author = {Citterio, Beatrice F. R. and Papetti, Daniele M. and Dimitri, Giovanna Maria and Tangherloni, Andrea},
|
|
478
|
+
booktitle = {Proceedings of the IEEE Congress on Evolutionary Computation (CEC)},
|
|
479
|
+
year = {2026},
|
|
480
|
+
}
|
|
481
|
+
```
|
|
402
482
|
|
|
403
483
|
## 📄 License
|
|
404
484
|
|
|
@@ -83,6 +83,7 @@ import torch.nn as nn
|
|
|
83
83
|
from torch import Tensor
|
|
84
84
|
|
|
85
85
|
from evograd.core.algorithm import Algorithm
|
|
86
|
+
from evograd.operators.relaxations import standard_normal, log_param
|
|
86
87
|
|
|
87
88
|
if TYPE_CHECKING:
|
|
88
89
|
from evograd.core.problem import Problem
|
|
@@ -386,9 +387,7 @@ class CMAES(Algorithm):
|
|
|
386
387
|
with torch.no_grad():
|
|
387
388
|
self._log_sigma.fill_(math.log(sigma_val))
|
|
388
389
|
else:
|
|
389
|
-
self._log_sigma =
|
|
390
|
-
torch.tensor(sigma_val, device=self.device, dtype=self.dtype).log()
|
|
391
|
-
)
|
|
390
|
+
self._log_sigma = log_param(sigma_val, device=self.device, dtype=self.dtype)
|
|
392
391
|
else:
|
|
393
392
|
if hasattr(self, '_sigma_buffer'):
|
|
394
393
|
self._sigma_buffer.fill_(sigma_val)
|
|
@@ -467,9 +466,7 @@ class CMAES(Algorithm):
|
|
|
467
466
|
self._to_logit(cmu).to(device=self.device, dtype=self.dtype)
|
|
468
467
|
)
|
|
469
468
|
# damps is positive, store as log
|
|
470
|
-
self._log_damps =
|
|
471
|
-
torch.tensor(damps, device=self.device, dtype=self.dtype).log()
|
|
472
|
-
)
|
|
469
|
+
self._log_damps = log_param(damps, device=self.device, dtype=self.dtype)
|
|
473
470
|
else:
|
|
474
471
|
if not hasattr(self, '_cc'):
|
|
475
472
|
self.register_buffer("_cc", torch.tensor(cc, device=self.device, dtype=self.dtype))
|
|
@@ -587,7 +584,7 @@ class CMAES(Algorithm):
|
|
|
587
584
|
N, D = self.pop_size, self.n_var
|
|
588
585
|
|
|
589
586
|
# Sample standard normal
|
|
590
|
-
z =
|
|
587
|
+
z = standard_normal(N, D, device=self.device, dtype=self.dtype)
|
|
591
588
|
|
|
592
589
|
# Transform: y = L @ z
|
|
593
590
|
L = self.L
|
|
@@ -63,6 +63,8 @@ import torch.nn as nn
|
|
|
63
63
|
from torch import Tensor
|
|
64
64
|
|
|
65
65
|
from evograd.core.algorithm import Algorithm
|
|
66
|
+
from evograd.operators.repair import clamp_to_bounds
|
|
67
|
+
from evograd.operators.relaxations import log_param
|
|
66
68
|
|
|
67
69
|
if TYPE_CHECKING:
|
|
68
70
|
from evograd.core.problem import Problem
|
|
@@ -308,9 +310,7 @@ class DE(Algorithm):
|
|
|
308
310
|
# Setup F parameter
|
|
309
311
|
if self.adaptive:
|
|
310
312
|
# Learnable F stored as log(F) for positivity
|
|
311
|
-
self._log_F =
|
|
312
|
-
torch.tensor(self._init_F, device=self.device, dtype=self.dtype).log()
|
|
313
|
-
)
|
|
313
|
+
self._log_F = log_param(self._init_F, device=self.device, dtype=self.dtype)
|
|
314
314
|
else:
|
|
315
315
|
self.register_buffer(
|
|
316
316
|
"_F_buffer",
|
|
@@ -627,7 +627,7 @@ class DE(Algorithm):
|
|
|
627
627
|
trial = self.repair(trial, self.xl, self.xu)
|
|
628
628
|
else:
|
|
629
629
|
# Default: clamp to bounds
|
|
630
|
-
trial =
|
|
630
|
+
trial = clamp_to_bounds(trial, self.xl, self.xu)
|
|
631
631
|
|
|
632
632
|
return trial
|
|
633
633
|
|
|
@@ -52,6 +52,7 @@ import torch.nn as nn
|
|
|
52
52
|
from torch import Tensor
|
|
53
53
|
|
|
54
54
|
from evograd.core.algorithm import Algorithm
|
|
55
|
+
from evograd.operators.repair import clamp_to_bounds
|
|
55
56
|
|
|
56
57
|
if TYPE_CHECKING:
|
|
57
58
|
from evograd.core.problem import Problem
|
|
@@ -357,7 +358,7 @@ class PSO(Algorithm):
|
|
|
357
358
|
pos_repaired = torch.where(above, 2 * self.xu - position, pos_repaired)
|
|
358
359
|
|
|
359
360
|
# Clamp to ensure within bounds (in case of large violations)
|
|
360
|
-
pos_repaired =
|
|
361
|
+
pos_repaired = clamp_to_bounds(pos_repaired, self.xl, self.xu)
|
|
361
362
|
|
|
362
363
|
# Reverse velocity at boundaries
|
|
363
364
|
vel_repaired = velocity.clone()
|
|
@@ -473,7 +474,7 @@ class PSO(Algorithm):
|
|
|
473
474
|
old_pos = new_pos - new_velocity # P₀
|
|
474
475
|
grad_delta = self._population.data - old_pos # −lr·∇
|
|
475
476
|
combined = new_pos + grad_delta # P₀ + v − lr·∇
|
|
476
|
-
combined =
|
|
477
|
+
combined = clamp_to_bounds(combined, self.xl, self.xu)
|
|
477
478
|
self._population.copy_(combined)
|
|
478
479
|
else:
|
|
479
480
|
self._population.copy_(new_pos)
|
|
@@ -65,6 +65,7 @@ import torch.nn as nn
|
|
|
65
65
|
from torch import Tensor
|
|
66
66
|
|
|
67
67
|
from evograd.core.algorithm import Algorithm
|
|
68
|
+
from evograd.operators.relaxations import standard_normal
|
|
68
69
|
|
|
69
70
|
if TYPE_CHECKING:
|
|
70
71
|
from evograd.core.problem import Problem
|
|
@@ -197,7 +198,7 @@ class SHADEMemory:
|
|
|
197
198
|
|
|
198
199
|
# Sample from Normal distribution
|
|
199
200
|
sigma = 0.1 # Standard SHADE scale parameter
|
|
200
|
-
CR = mu_CR + sigma *
|
|
201
|
+
CR = mu_CR + sigma * standard_normal(n, device=device, dtype=dtype)
|
|
201
202
|
|
|
202
203
|
# Truncate to [0, 1]
|
|
203
204
|
CR = torch.clamp(CR, min=0.0, max=1.0)
|
|
@@ -520,7 +521,7 @@ class SHADE(Algorithm):
|
|
|
520
521
|
|
|
521
522
|
# Reparameterized Normal for CR
|
|
522
523
|
sigma = 0.1
|
|
523
|
-
eps =
|
|
524
|
+
eps = standard_normal(N, device=self.device, dtype=self.dtype)
|
|
524
525
|
CR = mu_CR + sigma * eps
|
|
525
526
|
CR = torch.clamp(CR, min=0.0, max=1.0)
|
|
526
527
|
else:
|
|
@@ -886,8 +887,6 @@ class LSHADE(SHADE):
|
|
|
886
887
|
adaptive: bool = False,
|
|
887
888
|
differentiable: bool = False,
|
|
888
889
|
selection_temperature: float = 1.0,
|
|
889
|
-
seed: Optional[int] = None,
|
|
890
|
-
device: Optional[Union[str, torch.device]] = None,
|
|
891
890
|
dtype: torch.dtype = torch.float32,
|
|
892
891
|
) -> None:
|
|
893
892
|
self.pop_size_init = pop_size_init # Will be set in _setup if None
|
|
@@ -909,8 +908,6 @@ class LSHADE(SHADE):
|
|
|
909
908
|
differentiable=differentiable,
|
|
910
909
|
adaptive=adaptive,
|
|
911
910
|
selection_temperature=selection_temperature,
|
|
912
|
-
seed=seed,
|
|
913
|
-
device=device,
|
|
914
911
|
dtype=dtype,
|
|
915
912
|
)
|
|
916
913
|
|
|
@@ -919,8 +916,10 @@ class LSHADE(SHADE):
|
|
|
919
916
|
# Set default pop_size_init based on problem dimension
|
|
920
917
|
if self.pop_size_init is None:
|
|
921
918
|
self.pop_size_init = 18 * self.problem.n_var
|
|
922
|
-
|
|
923
|
-
|
|
919
|
+
# Always initialise the working population size (fixes target_pop_size
|
|
920
|
+
# AttributeError when pop_size_init was provided explicitly).
|
|
921
|
+
self._pop_size = self.pop_size_init
|
|
922
|
+
|
|
924
923
|
# Create success-history memory with L-SHADE archive size
|
|
925
924
|
self.memory = SHADEMemory.create(
|
|
926
925
|
H=self.memory_size,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared base for CEC 2017 functions.
|
|
3
|
+
|
|
4
|
+
Single source of truth for the bias, shift/rotation loading, default bounds, and
|
|
5
|
+
the shift+rotate transform used by the simple and hybrid CEC 2017 families
|
|
6
|
+
(previously copy-pasted in simple.py and hybrid.py).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Optional, Tuple
|
|
11
|
+
|
|
12
|
+
from torch import Tensor
|
|
13
|
+
|
|
14
|
+
from ..base import BenchmarkFunction
|
|
15
|
+
from . import data as cec_data
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CEC2017Base(BenchmarkFunction):
|
|
19
|
+
"""Base for shifted+rotated CEC 2017 functions (search space [-100, 100]^D)."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
func_num: int,
|
|
24
|
+
n_var: int = 10,
|
|
25
|
+
rotation: Optional[Tensor] = None,
|
|
26
|
+
shift: Optional[Tensor] = None,
|
|
27
|
+
seed: Optional[int] = None,
|
|
28
|
+
):
|
|
29
|
+
super().__init__(n_var=n_var, xl=-100.0, xu=100.0)
|
|
30
|
+
|
|
31
|
+
self.func_num = func_num
|
|
32
|
+
self.bias = func_num * 100.0 # F_i* = i * 100
|
|
33
|
+
|
|
34
|
+
if rotation is not None:
|
|
35
|
+
self.rotation = rotation
|
|
36
|
+
else:
|
|
37
|
+
self.rotation = cec_data.get_rotation(func_num, n_var, seed=seed)
|
|
38
|
+
|
|
39
|
+
if shift is not None:
|
|
40
|
+
self.shift = shift
|
|
41
|
+
else:
|
|
42
|
+
self.shift = cec_data.get_shift(func_num, n_var, seed=seed)
|
|
43
|
+
|
|
44
|
+
def default_bounds(self) -> Tuple[float, float]:
|
|
45
|
+
return (-100.0, 100.0)
|
|
46
|
+
|
|
47
|
+
def _shift_rotate(self, x: Tensor) -> Tensor:
|
|
48
|
+
"""Apply the shift + rotation transform (single source of truth)."""
|
|
49
|
+
shift = self.shift.to(x.device, x.dtype)
|
|
50
|
+
rotation = self.rotation.to(x.device, x.dtype)
|
|
51
|
+
return cec_data.shift_rotate(x, shift, rotation)
|
|
@@ -26,11 +26,15 @@ from torch import Tensor
|
|
|
26
26
|
from ..base import BenchmarkFunction
|
|
27
27
|
from . import basic
|
|
28
28
|
from . import data as cec_data
|
|
29
|
+
from ._base import CEC2017Base
|
|
29
30
|
|
|
30
31
|
|
|
31
|
-
class CEC2017HybridFunction(
|
|
32
|
-
"""Base class for CEC 2017 hybrid functions.
|
|
33
|
-
|
|
32
|
+
class CEC2017HybridFunction(CEC2017Base):
|
|
33
|
+
"""Base class for CEC 2017 hybrid functions (adds a shuffle permutation).
|
|
34
|
+
|
|
35
|
+
Shared shift/rotation handling is inherited from CEC2017Base (see _base.py).
|
|
36
|
+
"""
|
|
37
|
+
|
|
34
38
|
def __init__(
|
|
35
39
|
self,
|
|
36
40
|
func_num: int,
|
|
@@ -40,47 +44,13 @@ class CEC2017HybridFunction(BenchmarkFunction):
|
|
|
40
44
|
shuffle: Optional[Tensor] = None,
|
|
41
45
|
seed: Optional[int] = None,
|
|
42
46
|
):
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
Args:
|
|
47
|
-
func_num: Function number (11-20).
|
|
48
|
-
n_var: Number of variables.
|
|
49
|
-
rotation: Optional rotation matrix.
|
|
50
|
-
shift: Optional shift vector.
|
|
51
|
-
shuffle: Optional shuffle permutation.
|
|
52
|
-
seed: Random seed for generating transforms if not provided.
|
|
53
|
-
"""
|
|
54
|
-
super().__init__(n_var=n_var, xl=-100.0, xu=100.0)
|
|
55
|
-
|
|
56
|
-
self.func_num = func_num
|
|
57
|
-
self.bias = func_num * 100.0
|
|
58
|
-
|
|
59
|
-
# Load or generate transforms
|
|
60
|
-
if rotation is not None:
|
|
61
|
-
self.rotation = rotation
|
|
62
|
-
else:
|
|
63
|
-
self.rotation = cec_data.get_rotation(func_num, n_var, seed=seed)
|
|
64
|
-
|
|
65
|
-
if shift is not None:
|
|
66
|
-
self.shift = shift
|
|
67
|
-
else:
|
|
68
|
-
self.shift = cec_data.get_shift(func_num, n_var, seed=seed)
|
|
69
|
-
|
|
47
|
+
super().__init__(func_num=func_num, n_var=n_var,
|
|
48
|
+
rotation=rotation, shift=shift, seed=seed)
|
|
70
49
|
if shuffle is not None:
|
|
71
50
|
self.shuffle = shuffle
|
|
72
51
|
else:
|
|
73
52
|
self.shuffle = cec_data.get_shuffle(func_num, n_var, seed=seed)
|
|
74
|
-
|
|
75
|
-
def default_bounds(self) -> Tuple[float, float]:
|
|
76
|
-
return (-100.0, 100.0)
|
|
77
|
-
|
|
78
|
-
def _shift_rotate(self, x: Tensor) -> Tensor:
|
|
79
|
-
"""Apply shift and rotation transformation."""
|
|
80
|
-
shift = self.shift.to(x.device, x.dtype)
|
|
81
|
-
rotation = self.rotation.to(x.device, x.dtype)
|
|
82
|
-
return cec_data.shift_rotate(x, shift, rotation)
|
|
83
|
-
|
|
53
|
+
|
|
84
54
|
def _shuffle_and_partition(self, x: Tensor, partitions: List[float]) -> List[Tensor]:
|
|
85
55
|
"""Apply shuffle and partition."""
|
|
86
56
|
shuffle = self.shuffle.to(x.device)
|
|
@@ -26,54 +26,17 @@ from torch import Tensor
|
|
|
26
26
|
from ..base import BenchmarkFunction
|
|
27
27
|
from . import basic
|
|
28
28
|
from . import data as cec_data
|
|
29
|
+
from ._base import CEC2017Base
|
|
29
30
|
|
|
30
31
|
|
|
31
|
-
class CEC2017Function(
|
|
32
|
-
"""Base class for CEC 2017 functions
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
shift: Optional[Tensor] = None,
|
|
40
|
-
seed: Optional[int] = None,
|
|
41
|
-
):
|
|
42
|
-
"""
|
|
43
|
-
Initialize CEC 2017 function.
|
|
44
|
-
|
|
45
|
-
Args:
|
|
46
|
-
func_num: Function number (1-30).
|
|
47
|
-
n_var: Number of variables (2, 10, 20, 30, 50, or 100 for official data).
|
|
48
|
-
rotation: Optional rotation matrix. If None, loads from data or generates.
|
|
49
|
-
shift: Optional shift vector. If None, loads from data or generates.
|
|
50
|
-
seed: Random seed for generating transforms if not provided.
|
|
51
|
-
"""
|
|
52
|
-
super().__init__(n_var=n_var, xl=-100.0, xu=100.0)
|
|
53
|
-
|
|
54
|
-
self.func_num = func_num
|
|
55
|
-
self.bias = func_num * 100.0 # F_i* = i * 100
|
|
56
|
-
|
|
57
|
-
# Load or generate rotation matrix
|
|
58
|
-
if rotation is not None:
|
|
59
|
-
self.rotation = rotation
|
|
60
|
-
else:
|
|
61
|
-
self.rotation = cec_data.get_rotation(func_num, n_var, seed=seed)
|
|
62
|
-
|
|
63
|
-
# Load or generate shift vector
|
|
64
|
-
if shift is not None:
|
|
65
|
-
self.shift = shift
|
|
66
|
-
else:
|
|
67
|
-
self.shift = cec_data.get_shift(func_num, n_var, seed=seed)
|
|
68
|
-
|
|
69
|
-
def default_bounds(self) -> Tuple[float, float]:
|
|
70
|
-
return (-100.0, 100.0)
|
|
71
|
-
|
|
72
|
-
def _shift_rotate(self, x: Tensor) -> Tensor:
|
|
73
|
-
"""Apply shift and rotation transformation."""
|
|
74
|
-
shift = self.shift.to(x.device, x.dtype)
|
|
75
|
-
rotation = self.rotation.to(x.device, x.dtype)
|
|
76
|
-
return cec_data.shift_rotate(x, shift, rotation)
|
|
32
|
+
class CEC2017Function(CEC2017Base):
|
|
33
|
+
"""Base class for CEC 2017 simple functions (shift + rotation).
|
|
34
|
+
|
|
35
|
+
The shared __init__, default_bounds, and _shift_rotate live in
|
|
36
|
+
CEC2017Base (see _base.py).
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
pass
|
|
77
40
|
|
|
78
41
|
|
|
79
42
|
class CEC2017_F1(CEC2017Function):
|
|
@@ -523,26 +523,65 @@ class Algorithm(nn.Module, ABC):
|
|
|
523
523
|
|
|
524
524
|
return self.state.best_fitness
|
|
525
525
|
|
|
526
|
-
def forward(self) -> Tensor:
|
|
526
|
+
def forward(self, reduction: str = "mean", live_selection: bool = True) -> Tensor:
|
|
527
527
|
"""
|
|
528
528
|
PyTorch forward pass for differentiable optimisation.
|
|
529
|
-
|
|
529
|
+
|
|
530
530
|
In differentiable mode, this builds a computation graph
|
|
531
|
-
through the entire generation
|
|
532
|
-
|
|
531
|
+
through the entire generation and reduces the per-offspring
|
|
532
|
+
fitness to a scalar loss. Call update_state() after
|
|
533
533
|
loss.backward() and optimizer.step() to commit changes.
|
|
534
|
-
|
|
534
|
+
|
|
535
|
+
Args:
|
|
536
|
+
reduction: How to reduce the (n_offsprings,) offspring fitness
|
|
537
|
+
into the scalar loss that is backpropagated:
|
|
538
|
+
- 'mean' (default): average fitness — gradient reaches the
|
|
539
|
+
whole population, driving every member downhill.
|
|
540
|
+
- 'sum': total fitness — same per-member gradient direction
|
|
541
|
+
as 'mean', scaled by n_offsprings.
|
|
542
|
+
- 'min': best offspring only — gradient flows solely through
|
|
543
|
+
the single best offspring's ancestry (sparse signal).
|
|
544
|
+
live_selection: Whether selection routing carries gradient back to
|
|
545
|
+
the population.
|
|
546
|
+
- True (default, "live"): re-evaluate the current population so
|
|
547
|
+
the selection logits depend on the live parameter — the
|
|
548
|
+
Gumbel-Softmax selection gradient then reaches the population
|
|
549
|
+
(fully end-to-end differentiable generation). For a
|
|
550
|
+
deterministic objective this re-evaluation reproduces the
|
|
551
|
+
committed fitness values exactly, so it is graph
|
|
552
|
+
reconstruction, not new sampling: it is intentionally NOT
|
|
553
|
+
counted in the evaluation budget (n_evals). It is, however, a
|
|
554
|
+
real extra objective pass (wall-clock/FLOPs). For a stochastic
|
|
555
|
+
objective the values may differ from the committed fitness.
|
|
556
|
+
- False ("detached", memetic): selection uses the cached,
|
|
557
|
+
detached committed fitness as fixed routing weights; gradient
|
|
558
|
+
only refines positions. Cheaper (no extra pass), lower
|
|
559
|
+
variance, and the correct choice for stochastic objectives.
|
|
560
|
+
|
|
535
561
|
Returns:
|
|
536
|
-
|
|
537
|
-
|
|
562
|
+
Reduced offspring fitness as a scalar tensor (for backprop).
|
|
563
|
+
|
|
538
564
|
Raises:
|
|
539
565
|
RuntimeError: If algorithm not initialized.
|
|
566
|
+
ValueError: If reduction is not one of 'mean', 'sum', 'min'.
|
|
540
567
|
"""
|
|
541
568
|
if not self._is_initialized:
|
|
542
569
|
raise RuntimeError(
|
|
543
570
|
"Algorithm not initialized. Call initialize(problem) first."
|
|
544
571
|
)
|
|
545
|
-
|
|
572
|
+
|
|
573
|
+
if reduction not in ("mean", "sum", "min"):
|
|
574
|
+
raise ValueError(
|
|
575
|
+
f"reduction must be one of 'mean', 'sum', 'min'; got {reduction!r}"
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
# Live selection: attach a fresh autograd graph to the parent fitness so
|
|
579
|
+
# selection gradients flow into the population. Deliberately does NOT
|
|
580
|
+
# increment n_evals (values match the committed fitness for a
|
|
581
|
+
# deterministic objective — this only rebuilds the graph).
|
|
582
|
+
if live_selection:
|
|
583
|
+
self.state.fitness = self._evaluate(self.population)
|
|
584
|
+
|
|
546
585
|
# Generate offspring (differentiable)
|
|
547
586
|
offspring = self._infill()
|
|
548
587
|
|
|
@@ -561,7 +600,12 @@ class Algorithm(nn.Module, ABC):
|
|
|
561
600
|
self._pending_offspring = offspring
|
|
562
601
|
self._pending_fitness = offspring_fitness
|
|
563
602
|
|
|
564
|
-
#
|
|
603
|
+
# Reduce per-offspring fitness to the scalar loss for backprop.
|
|
604
|
+
if reduction == "mean":
|
|
605
|
+
return offspring_fitness.mean()
|
|
606
|
+
if reduction == "sum":
|
|
607
|
+
return offspring_fitness.sum()
|
|
608
|
+
# reduction == "min"
|
|
565
609
|
return offspring_fitness.min()
|
|
566
610
|
|
|
567
611
|
@torch.no_grad()
|