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.
Files changed (63) hide show
  1. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/PKG-INFO +84 -4
  2. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/README.md +83 -3
  3. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/__init__.py +1 -1
  4. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/algorithms/cmaes.py +4 -7
  5. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/algorithms/de.py +4 -4
  6. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/algorithms/pso.py +3 -2
  7. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/algorithms/shade.py +7 -8
  8. evograd_diff-0.2.1/evograd/benchmarks/functions/cec2017/_base.py +51 -0
  9. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/benchmarks/functions/cec2017/hybrid.py +10 -40
  10. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/benchmarks/functions/cec2017/simple.py +9 -46
  11. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/core/algorithm.py +53 -9
  12. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/core/maximize.py +4 -0
  13. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/core/minimize.py +19 -3
  14. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/operators/crossover.py +3 -5
  15. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/operators/mutation.py +6 -8
  16. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/operators/relaxations.py +30 -1
  17. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/operators/repair.py +11 -1
  18. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/operators/sampling.py +5 -7
  19. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/operators/selection.py +2 -4
  20. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/tests/run_all.py +21 -0
  21. evograd_diff-0.2.1/evograd/tests/test_de.py +62 -0
  22. evograd_diff-0.2.1/evograd/tests/test_pso.py +62 -0
  23. evograd_diff-0.2.1/evograd/tests/test_shade.py +64 -0
  24. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/pyproject.toml +12 -1
  25. evograd_diff-0.1.2/.claude/settings.local.json +0 -17
  26. evograd_diff-0.1.2/.python-version +0 -1
  27. evograd_diff-0.1.2/Test_new_evograd.ipynb +0 -31427
  28. evograd_diff-0.1.2/plot_benchmarks.py +0 -896
  29. evograd_diff-0.1.2/test.py +0 -32
  30. evograd_diff-0.1.2/test2.py +0 -20
  31. evograd_diff-0.1.2/uv.lock +0 -1014
  32. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/.gitignore +0 -0
  33. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/LICENSE +0 -0
  34. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/algorithms/__init__.py +0 -0
  35. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/algorithms/ga.py +0 -0
  36. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/benchmarks/functions/__init__.py +0 -0
  37. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/benchmarks/functions/base.py +0 -0
  38. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/benchmarks/functions/cec2017/__init__.py +0 -0
  39. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/benchmarks/functions/cec2017/basic.py +0 -0
  40. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/benchmarks/functions/cec2017/composition.py +0 -0
  41. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/benchmarks/functions/cec2017/data.pkl +0 -0
  42. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/benchmarks/functions/cec2017/data.py +0 -0
  43. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/benchmarks/functions/classical.py +0 -0
  44. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/benchmarks/functions/smoothed_funnel.py +0 -0
  45. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/benchmarks/functions/transforms.py +0 -0
  46. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/benchmarks/run_benchmark_functions.py +0 -0
  47. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/core/__init__.py +0 -0
  48. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/core/problem.py +0 -0
  49. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/core/result.py +0 -0
  50. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/core/termination.py +0 -0
  51. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/operators/__init__.py +0 -0
  52. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/operators/survival.py +0 -0
  53. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/tests/__init__.py +0 -0
  54. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/tests/test_cmaes.py +0 -0
  55. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/tests/test_core.py +0 -0
  56. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/tests/test_ga.py +0 -0
  57. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/tests/test_operators.py +0 -0
  58. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/tests/test_per_individual.py +0 -0
  59. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/tests/test_utils.py +0 -0
  60. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/utils/__init__.py +0 -0
  61. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/utils/callbacks.py +0 -0
  62. {evograd_diff-0.1.2 → evograd_diff-0.2.1}/evograd/utils/device.py +0 -0
  63. {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.2
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
- TODO
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
- TBA
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
- TODO
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
- TBA
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
 
@@ -57,7 +57,7 @@ Authors
57
57
  Andrea Tangherloni <andrea.tangherloni@unibocconi.it>
58
58
  """
59
59
 
60
- __version__ = "0.1.0"
60
+ __version__ = "0.2.1"
61
61
  __author__ = "Andrea Tangherloni"
62
62
 
63
63
  __all__ = [
@@ -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 = nn.Parameter(
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 = nn.Parameter(
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 = torch.randn(N, D, device=self.device, dtype=self.dtype)
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 = nn.Parameter(
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 = torch.clamp(trial, self.xl, self.xu)
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 = torch.clamp(pos_repaired, self.xl, self.xu)
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 = torch.clamp(combined, self.xl, self.xu)
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 * torch.randn(n, device=device, dtype=dtype)
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 = torch.randn(N, device=self.device, dtype=self.dtype)
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
- self._pop_size = self.pop_size_init
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(BenchmarkFunction):
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
- Initialize CEC 2017 hybrid function.
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(BenchmarkFunction):
32
- """Base class for CEC 2017 functions with shift and rotation support."""
33
-
34
- def __init__(
35
- self,
36
- func_num: int,
37
- n_var: int = 10,
38
- rotation: Optional[Tensor] = None,
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, returning the best fitness
532
- as a differentiable scalar loss. Call update_state() after
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
- Best fitness as a scalar tensor (for backprop).
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
- # Return best fitness as loss
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()