evograd-diff 0.2.1__tar.gz → 0.3.0__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 (57) hide show
  1. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/.gitignore +1 -0
  2. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/PKG-INFO +50 -6
  3. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/README.md +49 -5
  4. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/__init__.py +4 -3
  5. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/algorithms/__init__.py +8 -8
  6. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/algorithms/cmaes.py +134 -31
  7. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/algorithms/ga.py +11 -2
  8. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/algorithms/pso.py +47 -16
  9. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/algorithms/shade.py +21 -9
  10. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/benchmarks/functions/cec2017/basic.py +14 -6
  11. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/benchmarks/functions/cec2017/composition.py +23 -24
  12. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/benchmarks/functions/cec2017/simple.py +7 -1
  13. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/benchmarks/functions/smoothed_funnel.py +14 -5
  14. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/benchmarks/run_benchmark_functions.py +16 -5
  15. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/core/algorithm.py +26 -13
  16. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/core/maximize.py +54 -7
  17. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/core/minimize.py +118 -26
  18. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/core/problem.py +6 -0
  19. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/core/result.py +2 -1
  20. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/operators/crossover.py +55 -33
  21. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/operators/mutation.py +27 -17
  22. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/operators/relaxations.py +18 -1
  23. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/operators/repair.py +9 -8
  24. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/tests/run_all.py +7 -0
  25. evograd_diff-0.3.0/evograd/tests/test_regressions.py +440 -0
  26. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/utils/callbacks.py +2 -1
  27. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/pyproject.toml +1 -1
  28. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/LICENSE +0 -0
  29. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/algorithms/de.py +0 -0
  30. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/benchmarks/functions/__init__.py +0 -0
  31. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/benchmarks/functions/base.py +0 -0
  32. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/benchmarks/functions/cec2017/__init__.py +0 -0
  33. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/benchmarks/functions/cec2017/_base.py +0 -0
  34. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/benchmarks/functions/cec2017/data.pkl +0 -0
  35. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/benchmarks/functions/cec2017/data.py +0 -0
  36. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/benchmarks/functions/cec2017/hybrid.py +0 -0
  37. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/benchmarks/functions/classical.py +0 -0
  38. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/benchmarks/functions/transforms.py +7 -7
  39. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/core/__init__.py +0 -0
  40. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/core/termination.py +0 -0
  41. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/operators/__init__.py +0 -0
  42. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/operators/sampling.py +0 -0
  43. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/operators/selection.py +0 -0
  44. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/operators/survival.py +0 -0
  45. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/tests/__init__.py +0 -0
  46. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/tests/test_cmaes.py +0 -0
  47. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/tests/test_core.py +0 -0
  48. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/tests/test_de.py +0 -0
  49. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/tests/test_ga.py +0 -0
  50. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/tests/test_operators.py +0 -0
  51. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/tests/test_per_individual.py +0 -0
  52. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/tests/test_pso.py +0 -0
  53. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/tests/test_shade.py +0 -0
  54. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/tests/test_utils.py +0 -0
  55. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/utils/__init__.py +0 -0
  56. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/utils/device.py +0 -0
  57. {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/utils/duplicates.py +0 -0
@@ -2,6 +2,7 @@
2
2
  dist/
3
3
  build/
4
4
  *.egg-info/
5
+ docs/_build/
5
6
 
6
7
  # Python caches
7
8
  __pycache__/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evograd-diff
3
- Version: 0.2.1
3
+ Version: 0.3.0
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
@@ -85,22 +85,26 @@ problem = Problem(
85
85
 
86
86
  # Run with Genetic Algorithm
87
87
  ga = GA(pop_size=100, differentiable=True)
88
- result = minimize(problem, ga, termination=MaxEvaluations(10000), seed=42)
88
+ result = minimize(problem, ga, termination=MaxEvaluations(10000), seed=42,
89
+ lr_pop=-1, lr_hyper=-1)
89
90
  print(f"GA Best: {result.best_fitness:.6f}")
90
91
 
91
92
  # Run with Differential Evolution
92
93
  de = DE(pop_size=100, variant="DE/rand/1/bin", adaptive=True)
93
- result = minimize(problem, de, termination=MaxEvaluations(10000), seed=42)
94
+ result = minimize(problem, de, termination=MaxEvaluations(10000), seed=42,
95
+ lr_hyper=-1)
94
96
  print(f"DE Best: {result.best_fitness:.6f}")
95
97
 
96
98
  # Run with Particle Swarm Optimisation
97
99
  pso = PSO(pop_size=100, adaptive=True, differentiable=True)
98
- result = minimize(problem, pso, termination=MaxEvaluations(10000), seed=42)
100
+ result = minimize(problem, pso, termination=MaxEvaluations(10000), seed=42,
101
+ lr_pop=-1, lr_hyper=-1)
99
102
  print(f"PSO Best: {result.best_fitness:.6f}")
100
103
 
101
104
  # Run with CMA-ES
102
105
  cmaes = CMAES(sigma=0.5, adaptive=True)
103
- result = minimize(problem, cmaes, termination=MaxEvaluations(10000), seed=42)
106
+ result = minimize(problem, cmaes, termination=MaxEvaluations(10000), seed=42,
107
+ lr_hyper=-1)
104
108
  print(f"CMA-ES Best: {result.best_fitness:.6f}")
105
109
  ```
106
110
 
@@ -140,6 +144,12 @@ ga = GA(
140
144
  | Operator `adaptive=True` | Operator uses Gumbel-Softmax/Binary-Concrete for gradient flow |
141
145
  | Operator `learn_*=True` | Operator hyperparameters become learnable `nn.Parameter` |
142
146
 
147
+ Gradient optimizers are opt-in at the `minimize()` call. `lr_pop=None` and
148
+ `lr_hyper=None` (the defaults) perform no backpropagation; use `-1` to select
149
+ EvoGrad's algorithm-specific learning rate or pass a numeric value. For strict
150
+ classical behavior, also construct the algorithm with `differentiable=False`
151
+ and `adaptive=False`.
152
+
143
153
  ### Differential Evolution (DE)
144
154
 
145
155
  DE uses **algorithm-level flags** for adaptive hyperparameters and differentiable population:
@@ -453,6 +463,9 @@ The **Multi-Basin** functions aggregate `K` basins (each a full Rastrigin/Rosenb
453
463
  The runner evaluates the four EvoGrad modes — **Classical**, **Differentiable**, **Adaptive**, **Full** — and, by default, the **pymoo** and **Adam** (multi-start) baselines:
454
464
 
455
465
  ```bash
466
+ # Install the manuscript's pinned cross-library baseline
467
+ pip install -r requirements-benchmarks.txt
468
+
456
469
  # 30 runs of DE on the full CEC 2017 suite in 30D (vs pymoo + Adam)
457
470
  python -m evograd.benchmarks.run_benchmark_functions -a DE -s cec2017 -D 30 -r 30
458
471
 
@@ -494,6 +507,37 @@ The **Adaptive** variant reaches the lowest mean/variance, while **Full** matche
494
507
 
495
508
  > Full experimental details are in the paper (see [Citation](#-citation)).
496
509
 
510
+ ### Reproducibility note (v0.3.0)
511
+
512
+ The results reported above and in the paper were produced with the **paper-era
513
+ code (`evograd-diff` v0.1.2)**. Version 0.3.0 incorporates a set of
514
+ post-publication audit fixes that can change the *absolute* numbers, so results
515
+ obtained with v0.3.0 may differ from the published tables. The corrections that
516
+ affect benchmark values include:
517
+
518
+ - a restored cyclic term in the Expanded Schaffer F6 component, which changes
519
+ CEC 2017 **F16, F19, F26–F30**;
520
+ - NaN-free CEC 2017 composition weights at component centres and the true CEC
521
+ **F9** optimum location (F8/F9 numerics otherwise preserved);
522
+ - matched **pymoo** GA/CMA-ES baseline configurations (population size, per-gene
523
+ operator probabilities, un-normalised CMA-ES step size);
524
+ - a consistent **Adam** baseline (fitness paired with the evaluated iterate);
525
+ - **scale-invariant** softmax recombination in differentiable CMA-ES; and
526
+ - consistent differentiable-**PSO** state (the committed position is re-evaluated so
527
+ population and fitness agree; optimisation quality is unchanged).
528
+
529
+ These changes primarily affect absolute fitness values and the cross-library
530
+ baseline comparisons; the paper's qualitative findings (all EvoGrad variants and
531
+ baselines within a run share the same objective implementation) are not expected
532
+ to change, but the exact significance percentages and per-function rankings will.
533
+ **To reproduce the paper exactly, install the paper-era release:**
534
+
535
+ ```bash
536
+ pip install evograd-diff==0.1.2
537
+ ```
538
+
539
+ See [`CHANGELOG.md`](CHANGELOG.md) for the complete list of changes.
540
+
497
541
  ## 📖 Citation
498
542
 
499
543
  EvoGrad was accepted at the **IEEE Congress on Evolutionary Computation (CEC) 2026**. If you use EvoGrad in your research, please cite:
@@ -524,4 +568,4 @@ Contributions are welcome! Please feel free to submit a Pull Request. For major
524
568
  ## 🙏 Acknowledgements
525
569
 
526
570
  - Inspired by [pymoo](https://pymoo.org/) for API design
527
- - Built with [PyTorch](https://pytorch.org/) for automatic differentiation
571
+ - Built with [PyTorch](https://pytorch.org/) for automatic differentiation
@@ -58,22 +58,26 @@ problem = Problem(
58
58
 
59
59
  # Run with Genetic Algorithm
60
60
  ga = GA(pop_size=100, differentiable=True)
61
- result = minimize(problem, ga, termination=MaxEvaluations(10000), seed=42)
61
+ result = minimize(problem, ga, termination=MaxEvaluations(10000), seed=42,
62
+ lr_pop=-1, lr_hyper=-1)
62
63
  print(f"GA Best: {result.best_fitness:.6f}")
63
64
 
64
65
  # Run with Differential Evolution
65
66
  de = DE(pop_size=100, variant="DE/rand/1/bin", adaptive=True)
66
- result = minimize(problem, de, termination=MaxEvaluations(10000), seed=42)
67
+ result = minimize(problem, de, termination=MaxEvaluations(10000), seed=42,
68
+ lr_hyper=-1)
67
69
  print(f"DE Best: {result.best_fitness:.6f}")
68
70
 
69
71
  # Run with Particle Swarm Optimisation
70
72
  pso = PSO(pop_size=100, adaptive=True, differentiable=True)
71
- result = minimize(problem, pso, termination=MaxEvaluations(10000), seed=42)
73
+ result = minimize(problem, pso, termination=MaxEvaluations(10000), seed=42,
74
+ lr_pop=-1, lr_hyper=-1)
72
75
  print(f"PSO Best: {result.best_fitness:.6f}")
73
76
 
74
77
  # Run with CMA-ES
75
78
  cmaes = CMAES(sigma=0.5, adaptive=True)
76
- result = minimize(problem, cmaes, termination=MaxEvaluations(10000), seed=42)
79
+ result = minimize(problem, cmaes, termination=MaxEvaluations(10000), seed=42,
80
+ lr_hyper=-1)
77
81
  print(f"CMA-ES Best: {result.best_fitness:.6f}")
78
82
  ```
79
83
 
@@ -113,6 +117,12 @@ ga = GA(
113
117
  | Operator `adaptive=True` | Operator uses Gumbel-Softmax/Binary-Concrete for gradient flow |
114
118
  | Operator `learn_*=True` | Operator hyperparameters become learnable `nn.Parameter` |
115
119
 
120
+ Gradient optimizers are opt-in at the `minimize()` call. `lr_pop=None` and
121
+ `lr_hyper=None` (the defaults) perform no backpropagation; use `-1` to select
122
+ EvoGrad's algorithm-specific learning rate or pass a numeric value. For strict
123
+ classical behavior, also construct the algorithm with `differentiable=False`
124
+ and `adaptive=False`.
125
+
116
126
  ### Differential Evolution (DE)
117
127
 
118
128
  DE uses **algorithm-level flags** for adaptive hyperparameters and differentiable population:
@@ -426,6 +436,9 @@ The **Multi-Basin** functions aggregate `K` basins (each a full Rastrigin/Rosenb
426
436
  The runner evaluates the four EvoGrad modes — **Classical**, **Differentiable**, **Adaptive**, **Full** — and, by default, the **pymoo** and **Adam** (multi-start) baselines:
427
437
 
428
438
  ```bash
439
+ # Install the manuscript's pinned cross-library baseline
440
+ pip install -r requirements-benchmarks.txt
441
+
429
442
  # 30 runs of DE on the full CEC 2017 suite in 30D (vs pymoo + Adam)
430
443
  python -m evograd.benchmarks.run_benchmark_functions -a DE -s cec2017 -D 30 -r 30
431
444
 
@@ -467,6 +480,37 @@ The **Adaptive** variant reaches the lowest mean/variance, while **Full** matche
467
480
 
468
481
  > Full experimental details are in the paper (see [Citation](#-citation)).
469
482
 
483
+ ### Reproducibility note (v0.3.0)
484
+
485
+ The results reported above and in the paper were produced with the **paper-era
486
+ code (`evograd-diff` v0.1.2)**. Version 0.3.0 incorporates a set of
487
+ post-publication audit fixes that can change the *absolute* numbers, so results
488
+ obtained with v0.3.0 may differ from the published tables. The corrections that
489
+ affect benchmark values include:
490
+
491
+ - a restored cyclic term in the Expanded Schaffer F6 component, which changes
492
+ CEC 2017 **F16, F19, F26–F30**;
493
+ - NaN-free CEC 2017 composition weights at component centres and the true CEC
494
+ **F9** optimum location (F8/F9 numerics otherwise preserved);
495
+ - matched **pymoo** GA/CMA-ES baseline configurations (population size, per-gene
496
+ operator probabilities, un-normalised CMA-ES step size);
497
+ - a consistent **Adam** baseline (fitness paired with the evaluated iterate);
498
+ - **scale-invariant** softmax recombination in differentiable CMA-ES; and
499
+ - consistent differentiable-**PSO** state (the committed position is re-evaluated so
500
+ population and fitness agree; optimisation quality is unchanged).
501
+
502
+ These changes primarily affect absolute fitness values and the cross-library
503
+ baseline comparisons; the paper's qualitative findings (all EvoGrad variants and
504
+ baselines within a run share the same objective implementation) are not expected
505
+ to change, but the exact significance percentages and per-function rankings will.
506
+ **To reproduce the paper exactly, install the paper-era release:**
507
+
508
+ ```bash
509
+ pip install evograd-diff==0.1.2
510
+ ```
511
+
512
+ See [`CHANGELOG.md`](CHANGELOG.md) for the complete list of changes.
513
+
470
514
  ## 📖 Citation
471
515
 
472
516
  EvoGrad was accepted at the **IEEE Congress on Evolutionary Computation (CEC) 2026**. If you use EvoGrad in your research, please cite:
@@ -497,4 +541,4 @@ Contributions are welcome! Please feel free to submit a Pull Request. For major
497
541
  ## 🙏 Acknowledgements
498
542
 
499
543
  - Inspired by [pymoo](https://pymoo.org/) for API design
500
- - Built with [PyTorch](https://pytorch.org/) for automatic differentiation
544
+ - Built with [PyTorch](https://pytorch.org/) for automatic differentiation
@@ -36,7 +36,8 @@ Quick Start
36
36
  ... )
37
37
  >>>
38
38
  >>> # Run optimization
39
- >>> result = minimize(problem, algorithm, termination=MaxEvaluations(10000), seed=42)
39
+ >>> result = minimize(problem, algorithm, termination=MaxEvaluations(10000), seed=42,
40
+ ... lr_pop=-1, lr_hyper=-1)
40
41
  >>> print(f"Best fitness: {result.best_fitness:.6f}")
41
42
 
42
43
  Architecture
@@ -57,11 +58,11 @@ Authors
57
58
  Andrea Tangherloni <andrea.tangherloni@unibocconi.it>
58
59
  """
59
60
 
60
- __version__ = "0.2.1"
61
+ __version__ = "0.3.0"
61
62
  __author__ = "Andrea Tangherloni"
62
63
 
63
64
  __all__ = [
64
65
  # Version info
65
66
  "__version__",
66
67
  "__author__",
67
- ]
68
+ ]
@@ -18,7 +18,7 @@ the dependency injection pattern for operators.
18
18
 
19
19
  Example:
20
20
  >>> from evograd.algorithms import GA, DE, SHADE, PSO, CMAES
21
- >>> from evograd.core import Problem, minimize
21
+ >>> from evograd.core import Problem, minimize, MaxEvaluations
22
22
  >>>
23
23
  >>> problem = Problem(
24
24
  ... objective=lambda x: (x**2).sum(dim=-1),
@@ -29,27 +29,27 @@ Example:
29
29
  >>>
30
30
  >>> # Genetic Algorithm
31
31
  >>> ga = GA(pop_size=100, differentiable=True)
32
- >>> result = minimize(problem, ga, max_evals=10000)
32
+ >>> result = minimize(problem, ga, MaxEvaluations(10000), lr_pop=-1, lr_hyper=-1)
33
33
  >>>
34
34
  >>> # Differential Evolution
35
35
  >>> de = DE(pop_size=100, variant="DE/rand/1/bin", adaptive=True)
36
- >>> result = minimize(problem, de, max_evals=10000)
36
+ >>> result = minimize(problem, de, MaxEvaluations(10000), lr_hyper=-1)
37
37
  >>>
38
38
  >>> # SHADE (Self-Adaptive DE)
39
39
  >>> shade = SHADE(pop_size=100, memory_size=100)
40
- >>> result = minimize(problem, shade, max_evals=10000)
40
+ >>> result = minimize(problem, shade, MaxEvaluations(10000))
41
41
  >>>
42
42
  >>> # L-SHADE (SHADE with population reduction)
43
43
  >>> lshade = LSHADE(pop_size_init=18*30, pop_size_min=4)
44
- >>> result = minimize(problem, lshade, max_evals=10000)
44
+ >>> result = minimize(problem, lshade, MaxEvaluations(10000))
45
45
  >>>
46
46
  >>> # Particle Swarm Optimisation
47
47
  >>> pso = PSO(pop_size=100, adaptive=True)
48
- >>> result = minimize(problem, pso, max_evals=10000)
48
+ >>> result = minimize(problem, pso, MaxEvaluations(10000), lr_hyper=-1)
49
49
  >>>
50
50
  >>> # CMA-ES
51
51
  >>> cmaes = CMAES(pop_size=50, adaptive=True)
52
- >>> result = minimize(problem, cmaes, max_evals=10000)
52
+ >>> result = minimize(problem, cmaes, MaxEvaluations(10000), lr_hyper=-1)
53
53
  """
54
54
 
55
55
  # Genetic Algorithm
@@ -135,4 +135,4 @@ __all__ = [
135
135
  "cmaes_adaptive",
136
136
  "cmaes_ipop",
137
137
  "cmaes_bipop",
138
- ]
138
+ ]
@@ -234,6 +234,7 @@ class CMAES(Algorithm):
234
234
  repair: Optional[nn.Module] = None,
235
235
  adaptive: bool = False,
236
236
  differentiable: bool = False,
237
+ selection_temperature: float = 1.0,
237
238
  dtype: torch.dtype = torch.float32,
238
239
  ) -> None:
239
240
  self.adaptive = adaptive
@@ -244,6 +245,9 @@ class CMAES(Algorithm):
244
245
  self._init_c1 = c1
245
246
  self._init_cmu = cmu
246
247
  self._init_damps = damps
248
+ if selection_temperature <= 0:
249
+ raise ValueError("selection_temperature must be > 0")
250
+ self._init_selection_temperature = selection_temperature
247
251
 
248
252
  # Restart parameters
249
253
  self._restarts = restarts
@@ -287,7 +291,9 @@ class CMAES(Algorithm):
287
291
  # Compute default population size if not provided
288
292
  if self._requested_pop_size is None:
289
293
  n_var = self.problem.n_var
290
- self.pop_size = 4 + int(3 * math.log(n_var))
294
+ default_size = 4 + int(3 * math.log(n_var))
295
+ factor = float(getattr(self, "_pop_size_factor", 1.0))
296
+ self.pop_size = max(2, int(round(default_size * factor)))
291
297
  self.n_offsprings = self.pop_size
292
298
 
293
299
  def _setup(self) -> None:
@@ -303,6 +309,16 @@ class CMAES(Algorithm):
303
309
 
304
310
  # Compute recombination weights
305
311
  self._setup_weights()
312
+
313
+ log_temperature = torch.tensor(
314
+ math.log(self._init_selection_temperature),
315
+ device=self.device,
316
+ dtype=self.dtype,
317
+ )
318
+ if self.adaptive:
319
+ self._log_selection_temperature = nn.Parameter(log_temperature)
320
+ else:
321
+ self.register_buffer("_log_selection_temperature", log_temperature)
306
322
 
307
323
  # Setup mean
308
324
  self._setup_mean(n_var)
@@ -517,6 +533,37 @@ class CMAES(Algorithm):
517
533
  """Covariance matrix C = L @ L.T."""
518
534
  L = self.L
519
535
  return L @ L.T
536
+
537
+ @property
538
+ def selection_temperature(self) -> Tensor:
539
+ """Temperature used by differentiable fitness recombination."""
540
+ return self._log_selection_temperature.exp()
541
+
542
+ def _soft_recombination_weights(self, fitness: Tensor) -> Tensor:
543
+ """Scale-invariant temperature-controlled fitness weights."""
544
+ spread = fitness.detach().std(unbiased=False).clamp_min(
545
+ torch.finfo(fitness.dtype).tiny
546
+ )
547
+ temperature = self.selection_temperature * spread
548
+ # Center first so a constant, non-zero fitness vector does not overflow
549
+ # when its (correctly tiny) scale is used as the temperature.
550
+ centered = fitness - fitness.detach().mean()
551
+ return torch.softmax(-centered / temperature, dim=0)
552
+
553
+ def _coefficients_for_mass(self, mu_eff: Tensor, soft: bool) -> tuple:
554
+ """Return CMA adaptation coefficients consistent with selection mass."""
555
+ if not soft or self.adaptive:
556
+ return self.cc, self.cs, self.c1, self.cmu, self.damps
557
+
558
+ d = float(self.n_var)
559
+ m = float(mu_eff.detach())
560
+ cc = (4 + m / d) / (d + 4 + 2 * m / d)
561
+ cs = (m + 2) / (d + m + 5)
562
+ c1 = 2 / ((d + 1.3) ** 2 + m)
563
+ cmu = max(0.0, min(1 - c1, 2 * (m - 2 + 1 / m) / ((d + 2) ** 2 + m)))
564
+ damps = 1 + 2 * max(0, math.sqrt((m - 1) / (d + 1)) - 1) + cs
565
+ values = (cc, cs, c1, cmu, damps)
566
+ return tuple(torch.as_tensor(v, device=self.device, dtype=self.dtype) for v in values)
520
567
 
521
568
  @property
522
569
  def p_sigma(self) -> Tensor:
@@ -616,6 +663,20 @@ class CMAES(Algorithm):
616
663
  offspring = torch.clamp(offspring, self.xl, self.xu)
617
664
 
618
665
  return offspring
666
+
667
+ def forward(self, reduction: str = "mean", live_selection: bool = True) -> Tensor:
668
+ """Build a generation graph with soft fitness recombination.
669
+
670
+ Classical CMA-ES keeps the requested generic reduction. Differentiable
671
+ and adaptive variants use temperature-controlled softmax weights so
672
+ selection itself contributes gradients, matching the manuscript model.
673
+ """
674
+ loss = super().forward(reduction=reduction, live_selection=live_selection)
675
+ if not (self.differentiable or self.adaptive):
676
+ return loss
677
+ fitness = self._pending_fitness
678
+ weights = self._soft_recombination_weights(fitness)
679
+ return (weights * fitness).sum()
619
680
 
620
681
  def _advance(self, offspring: Tensor, offspring_fitness: Tensor) -> None:
621
682
  """
@@ -625,30 +686,52 @@ class CMAES(Algorithm):
625
686
  offspring: Offspring population [pop_size, n_var].
626
687
  offspring_fitness: Fitness values [pop_size].
627
688
  """
628
- N, D = self.pop_size, self.n_var
629
- mu = self._mu
630
-
631
- # Sort by fitness (ascending for minimization)
632
- sorted_indices = torch.argsort(offspring_fitness)
633
- selected_indices = sorted_indices[:mu]
634
-
635
- # Get selected y vectors (in transformed space)
636
- y_selected = self._pending_y[selected_indices] # [mu, D]
637
-
689
+ if self.differentiable or self.adaptive:
690
+ recombination_weights = self._soft_recombination_weights(offspring_fitness)
691
+ y_selected = self._pending_y
692
+ soft_recombination = True
693
+ else:
694
+ sorted_indices = torch.argsort(offspring_fitness)
695
+ selected_indices = sorted_indices[:self._mu]
696
+ y_selected = self._pending_y[selected_indices]
697
+ recombination_weights = self._weights
698
+ soft_recombination = False
699
+
700
+ mu_eff = 1.0 / recombination_weights.square().sum().clamp_min(self._eps)
701
+ cc, cs, c1, cmu, damps = self._coefficients_for_mass(
702
+ mu_eff,
703
+ soft_recombination,
704
+ )
705
+ self._last_mu_eff = float(mu_eff.detach())
706
+
638
707
  # Weighted recombination in y-space
639
- y_w = (self._weights.unsqueeze(-1) * y_selected).sum(dim=0) # [D]
708
+ y_w = (recombination_weights.unsqueeze(-1) * y_selected).sum(dim=0)
640
709
 
641
710
  # Update mean
642
711
  new_mean = self.mean + self.sigma * y_w
643
712
 
644
713
  # Update evolution paths
645
- new_p_sigma, new_p_c, h_sigma = self._update_evolution_paths(y_w)
714
+ new_p_sigma, new_p_c, h_sigma = self._update_evolution_paths(
715
+ y_w,
716
+ mu_eff,
717
+ cc,
718
+ cs,
719
+ soft_recombination,
720
+ )
646
721
 
647
722
  # Update covariance
648
- new_L = self._update_covariance(y_selected, new_p_c, h_sigma)
723
+ new_L = self._update_covariance(
724
+ y_selected,
725
+ new_p_c,
726
+ h_sigma,
727
+ recombination_weights,
728
+ cc,
729
+ c1,
730
+ cmu,
731
+ )
649
732
 
650
733
  # Update step-size
651
- new_sigma = self._update_sigma(new_p_sigma)
734
+ new_sigma = self._update_sigma(new_p_sigma, cs, damps)
652
735
 
653
736
  # Commit updates
654
737
  self._commit_updates(
@@ -682,7 +765,14 @@ class CMAES(Algorithm):
682
765
  del self._pending_z
683
766
  del self._pending_y
684
767
 
685
- def _update_evolution_paths(self, y_w: Tensor) -> tuple:
768
+ def _update_evolution_paths(
769
+ self,
770
+ y_w: Tensor,
771
+ mu_eff: Tensor,
772
+ cc: Tensor,
773
+ cs: Tensor,
774
+ soft: bool,
775
+ ) -> tuple:
686
776
  """
687
777
  Update evolution paths p_σ and p_c.
688
778
 
@@ -693,7 +783,6 @@ class CMAES(Algorithm):
693
783
  Tuple of (new_p_sigma, new_p_c, h_sigma).
694
784
  """
695
785
  D = self.n_var
696
- mu_eff = self._mu_eff
697
786
 
698
787
  # Compute C^(-1/2) @ y_w using L^(-1) @ y_w
699
788
  L = self.L
@@ -701,8 +790,15 @@ class CMAES(Algorithm):
701
790
  z_w = torch.linalg.solve_triangular(L, y_w.unsqueeze(-1), upper=False).squeeze(-1)
702
791
 
703
792
  # Update p_sigma (conjugate evolution path)
704
- cs = self.cs
705
- new_p_sigma = (1 - cs) * self.p_sigma + math.sqrt(cs * (2 - cs) * mu_eff) * z_w
793
+ if soft:
794
+ sigma_path_scale = torch.sqrt(cs * (2 - cs) * mu_eff)
795
+ covariance_path_scale = torch.sqrt(cc * (2 - cc) * mu_eff)
796
+ else:
797
+ # Preserve the original classical implementation's scalar path
798
+ # exactly; only relaxed recombination needs tensor-valued scales.
799
+ sigma_path_scale = math.sqrt(cs * (2 - cs) * mu_eff)
800
+ covariance_path_scale = math.sqrt(cc * (2 - cc) * mu_eff)
801
+ new_p_sigma = (1 - cs) * self.p_sigma + sigma_path_scale * z_w
706
802
 
707
803
  # Heaviside function h_sigma (smooth approximation)
708
804
  norm_p_sigma = new_p_sigma.norm()
@@ -710,8 +806,7 @@ class CMAES(Algorithm):
710
806
  h_sigma = torch.sigmoid(10 * (threshold - norm_p_sigma / self._chi_n))
711
807
 
712
808
  # Update p_c (evolution path for covariance)
713
- cc = self.cc
714
- new_p_c = (1 - cc) * self.p_c + h_sigma * math.sqrt(cc * (2 - cc) * mu_eff) * y_w
809
+ new_p_c = (1 - cc) * self.p_c + h_sigma * covariance_path_scale * y_w
715
810
 
716
811
  return new_p_sigma, new_p_c, h_sigma
717
812
 
@@ -720,6 +815,10 @@ class CMAES(Algorithm):
720
815
  y_selected: Tensor,
721
816
  new_p_c: Tensor,
722
817
  h_sigma: Tensor,
818
+ recombination_weights: Tensor,
819
+ cc: Tensor,
820
+ c1: Tensor,
821
+ cmu: Tensor,
723
822
  ) -> Tensor:
724
823
  """
725
824
  Update covariance matrix C.
@@ -733,9 +832,6 @@ class CMAES(Algorithm):
733
832
  New Cholesky factor L.
734
833
  """
735
834
  D = self.n_var
736
- c1 = self.c1
737
- cmu = self.cmu
738
- cc = self.cc
739
835
 
740
836
  # Current covariance
741
837
  L = self.L
@@ -746,7 +842,7 @@ class CMAES(Algorithm):
746
842
 
747
843
  # Rank-mu update
748
844
  rank_mu = (
749
- self._weights.unsqueeze(-1).unsqueeze(-1) *
845
+ recombination_weights.unsqueeze(-1).unsqueeze(-1) *
750
846
  y_selected.unsqueeze(-1) * y_selected.unsqueeze(-2)
751
847
  ).sum(dim=0)
752
848
 
@@ -850,7 +946,12 @@ class CMAES(Algorithm):
850
946
 
851
947
  return L_identity
852
948
 
853
- def _update_sigma(self, new_p_sigma: Tensor) -> Tensor:
949
+ def _update_sigma(
950
+ self,
951
+ new_p_sigma: Tensor,
952
+ cs: Tensor,
953
+ damps: Tensor,
954
+ ) -> Tensor:
854
955
  """
855
956
  Update step-size using CSA (Cumulative Step-size Adaptation).
856
957
 
@@ -860,9 +961,6 @@ class CMAES(Algorithm):
860
961
  Returns:
861
962
  New step-size.
862
963
  """
863
- cs = self.cs
864
- damps = self.damps
865
-
866
964
  # Step-size update factor
867
965
  norm_p_sigma = new_p_sigma.norm()
868
966
  factor = torch.exp((cs / damps) * (norm_p_sigma / self._chi_n - 1))
@@ -1098,6 +1196,10 @@ class CMAES(Algorithm):
1098
1196
  if self.adaptive:
1099
1197
  # Sigma in (1e-10, 1e10)
1100
1198
  self._log_sigma.clamp_(min=-23, max=23)
1199
+ self._log_selection_temperature.clamp_(
1200
+ min=math.log(0.05),
1201
+ max=math.log(100.0),
1202
+ )
1101
1203
 
1102
1204
  # Ensure c1 + cmu <= 1
1103
1205
  c1 = torch.sigmoid(self._c1_logit)
@@ -1118,7 +1220,8 @@ class CMAES(Algorithm):
1118
1220
  return {
1119
1221
  'pop_size': self.pop_size,
1120
1222
  'mu': self._mu,
1121
- 'mu_eff': self._mu_eff,
1223
+ 'selection_temperature': float(self.selection_temperature.detach()),
1224
+ 'mu_eff': getattr(self, '_last_mu_eff', self._mu_eff),
1122
1225
  'sigma': float(self.sigma.item()),
1123
1226
  'cc': float(self.cc.item()),
1124
1227
  'cs': float(self.cs.item()),
@@ -1362,4 +1465,4 @@ def cmaes_bipop(
1362
1465
  incpopsize=incpopsize,
1363
1466
  bipop=True,
1364
1467
  **kwargs,
1365
- )
1468
+ )
@@ -57,7 +57,7 @@ Example:
57
57
  Reference:
58
58
  Holland, J. H. (1992). Genetic Algorithms. Scientific American.
59
59
  Goldberg, D. E. (1989). Genetic Algorithms in Search, Optimization,
60
- and Machine Learning.
60
+ and Machine Learning.
61
61
  """
62
62
 
63
63
  from __future__ import annotations
@@ -69,6 +69,7 @@ import torch.nn as nn
69
69
  from torch import Tensor
70
70
 
71
71
  from evograd.core.algorithm import Algorithm
72
+ from evograd.operators.repair import clamp_to_bounds
72
73
 
73
74
  if TYPE_CHECKING:
74
75
  from evograd.core.problem import Problem
@@ -274,7 +275,15 @@ class GA(Algorithm):
274
275
 
275
276
  # 3. MUTATION: Introduce variation
276
277
  offspring = self.mutation(offspring, self.xl, self.xu)
277
-
278
+
279
+ # 4. Bound safety: when no repair operator is configured, clamp to the
280
+ # box so out-of-bounds points are never evaluated regardless of the
281
+ # mutation/crossover operators (mirrors DE/PSO/CMA-ES/SHADE). When a
282
+ # repair IS configured the base step/forward applies it. This is a
283
+ # no-op for the bounded default PolynomialMutation.
284
+ if self.repair is None:
285
+ offspring = clamp_to_bounds(offspring, self.xl, self.xu)
286
+
278
287
  return offspring
279
288
 
280
289
  def _advance(self, offspring: Tensor, offspring_fitness: Tensor) -> None: