evograd-diff 0.2.0__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.0 → evograd_diff-0.3.0}/.gitignore +1 -0
  2. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/PKG-INFO +50 -6
  3. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/README.md +49 -5
  4. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/__init__.py +4 -3
  5. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/algorithms/__init__.py +8 -8
  6. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/algorithms/cmaes.py +138 -38
  7. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/algorithms/de.py +4 -4
  8. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/algorithms/ga.py +11 -2
  9. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/algorithms/pso.py +50 -18
  10. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/algorithms/shade.py +26 -15
  11. evograd_diff-0.3.0/evograd/benchmarks/functions/cec2017/_base.py +51 -0
  12. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/benchmarks/functions/cec2017/basic.py +14 -6
  13. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/benchmarks/functions/cec2017/composition.py +23 -24
  14. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/benchmarks/functions/cec2017/hybrid.py +10 -40
  15. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/benchmarks/functions/cec2017/simple.py +16 -47
  16. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/benchmarks/functions/smoothed_funnel.py +14 -5
  17. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/benchmarks/run_benchmark_functions.py +16 -5
  18. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/core/algorithm.py +26 -13
  19. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/core/maximize.py +54 -7
  20. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/core/minimize.py +118 -26
  21. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/core/problem.py +6 -0
  22. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/core/result.py +2 -1
  23. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/operators/crossover.py +58 -38
  24. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/operators/mutation.py +33 -25
  25. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/operators/relaxations.py +48 -2
  26. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/operators/repair.py +20 -9
  27. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/operators/sampling.py +5 -7
  28. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/operators/selection.py +2 -4
  29. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/tests/run_all.py +28 -0
  30. evograd_diff-0.3.0/evograd/tests/test_de.py +62 -0
  31. evograd_diff-0.3.0/evograd/tests/test_pso.py +62 -0
  32. evograd_diff-0.3.0/evograd/tests/test_regressions.py +440 -0
  33. evograd_diff-0.3.0/evograd/tests/test_shade.py +64 -0
  34. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/utils/callbacks.py +2 -1
  35. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/pyproject.toml +1 -1
  36. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/LICENSE +0 -0
  37. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/benchmarks/functions/__init__.py +0 -0
  38. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/benchmarks/functions/base.py +0 -0
  39. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/benchmarks/functions/cec2017/__init__.py +0 -0
  40. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/benchmarks/functions/cec2017/data.pkl +0 -0
  41. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/benchmarks/functions/cec2017/data.py +0 -0
  42. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/benchmarks/functions/classical.py +0 -0
  43. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/benchmarks/functions/transforms.py +7 -7
  44. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/core/__init__.py +0 -0
  45. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/core/termination.py +0 -0
  46. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/operators/__init__.py +0 -0
  47. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/operators/survival.py +0 -0
  48. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/tests/__init__.py +0 -0
  49. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/tests/test_cmaes.py +0 -0
  50. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/tests/test_core.py +0 -0
  51. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/tests/test_ga.py +0 -0
  52. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/tests/test_operators.py +0 -0
  53. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/tests/test_per_individual.py +0 -0
  54. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/tests/test_utils.py +0 -0
  55. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/utils/__init__.py +0 -0
  56. {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/utils/device.py +0 -0
  57. {evograd_diff-0.2.0 → 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.0
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.0"
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
+ ]
@@ -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
@@ -233,6 +234,7 @@ class CMAES(Algorithm):
233
234
  repair: Optional[nn.Module] = None,
234
235
  adaptive: bool = False,
235
236
  differentiable: bool = False,
237
+ selection_temperature: float = 1.0,
236
238
  dtype: torch.dtype = torch.float32,
237
239
  ) -> None:
238
240
  self.adaptive = adaptive
@@ -243,6 +245,9 @@ class CMAES(Algorithm):
243
245
  self._init_c1 = c1
244
246
  self._init_cmu = cmu
245
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
246
251
 
247
252
  # Restart parameters
248
253
  self._restarts = restarts
@@ -286,7 +291,9 @@ class CMAES(Algorithm):
286
291
  # Compute default population size if not provided
287
292
  if self._requested_pop_size is None:
288
293
  n_var = self.problem.n_var
289
- 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)))
290
297
  self.n_offsprings = self.pop_size
291
298
 
292
299
  def _setup(self) -> None:
@@ -302,6 +309,16 @@ class CMAES(Algorithm):
302
309
 
303
310
  # Compute recombination weights
304
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)
305
322
 
306
323
  # Setup mean
307
324
  self._setup_mean(n_var)
@@ -386,9 +403,7 @@ class CMAES(Algorithm):
386
403
  with torch.no_grad():
387
404
  self._log_sigma.fill_(math.log(sigma_val))
388
405
  else:
389
- self._log_sigma = nn.Parameter(
390
- torch.tensor(sigma_val, device=self.device, dtype=self.dtype).log()
391
- )
406
+ self._log_sigma = log_param(sigma_val, device=self.device, dtype=self.dtype)
392
407
  else:
393
408
  if hasattr(self, '_sigma_buffer'):
394
409
  self._sigma_buffer.fill_(sigma_val)
@@ -467,9 +482,7 @@ class CMAES(Algorithm):
467
482
  self._to_logit(cmu).to(device=self.device, dtype=self.dtype)
468
483
  )
469
484
  # damps is positive, store as log
470
- self._log_damps = nn.Parameter(
471
- torch.tensor(damps, device=self.device, dtype=self.dtype).log()
472
- )
485
+ self._log_damps = log_param(damps, device=self.device, dtype=self.dtype)
473
486
  else:
474
487
  if not hasattr(self, '_cc'):
475
488
  self.register_buffer("_cc", torch.tensor(cc, device=self.device, dtype=self.dtype))
@@ -520,6 +533,37 @@ class CMAES(Algorithm):
520
533
  """Covariance matrix C = L @ L.T."""
521
534
  L = self.L
522
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)
523
567
 
524
568
  @property
525
569
  def p_sigma(self) -> Tensor:
@@ -587,7 +631,7 @@ class CMAES(Algorithm):
587
631
  N, D = self.pop_size, self.n_var
588
632
 
589
633
  # Sample standard normal
590
- z = torch.randn(N, D, device=self.device, dtype=self.dtype)
634
+ z = standard_normal(N, D, device=self.device, dtype=self.dtype)
591
635
 
592
636
  # Transform: y = L @ z
593
637
  L = self.L
@@ -619,6 +663,20 @@ class CMAES(Algorithm):
619
663
  offspring = torch.clamp(offspring, self.xl, self.xu)
620
664
 
621
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()
622
680
 
623
681
  def _advance(self, offspring: Tensor, offspring_fitness: Tensor) -> None:
624
682
  """
@@ -628,30 +686,52 @@ class CMAES(Algorithm):
628
686
  offspring: Offspring population [pop_size, n_var].
629
687
  offspring_fitness: Fitness values [pop_size].
630
688
  """
631
- N, D = self.pop_size, self.n_var
632
- mu = self._mu
633
-
634
- # Sort by fitness (ascending for minimization)
635
- sorted_indices = torch.argsort(offspring_fitness)
636
- selected_indices = sorted_indices[:mu]
637
-
638
- # Get selected y vectors (in transformed space)
639
- y_selected = self._pending_y[selected_indices] # [mu, D]
640
-
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
+
641
707
  # Weighted recombination in y-space
642
- y_w = (self._weights.unsqueeze(-1) * y_selected).sum(dim=0) # [D]
708
+ y_w = (recombination_weights.unsqueeze(-1) * y_selected).sum(dim=0)
643
709
 
644
710
  # Update mean
645
711
  new_mean = self.mean + self.sigma * y_w
646
712
 
647
713
  # Update evolution paths
648
- 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
+ )
649
721
 
650
722
  # Update covariance
651
- 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
+ )
652
732
 
653
733
  # Update step-size
654
- new_sigma = self._update_sigma(new_p_sigma)
734
+ new_sigma = self._update_sigma(new_p_sigma, cs, damps)
655
735
 
656
736
  # Commit updates
657
737
  self._commit_updates(
@@ -685,7 +765,14 @@ class CMAES(Algorithm):
685
765
  del self._pending_z
686
766
  del self._pending_y
687
767
 
688
- 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:
689
776
  """
690
777
  Update evolution paths p_σ and p_c.
691
778
 
@@ -696,7 +783,6 @@ class CMAES(Algorithm):
696
783
  Tuple of (new_p_sigma, new_p_c, h_sigma).
697
784
  """
698
785
  D = self.n_var
699
- mu_eff = self._mu_eff
700
786
 
701
787
  # Compute C^(-1/2) @ y_w using L^(-1) @ y_w
702
788
  L = self.L
@@ -704,8 +790,15 @@ class CMAES(Algorithm):
704
790
  z_w = torch.linalg.solve_triangular(L, y_w.unsqueeze(-1), upper=False).squeeze(-1)
705
791
 
706
792
  # Update p_sigma (conjugate evolution path)
707
- cs = self.cs
708
- 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
709
802
 
710
803
  # Heaviside function h_sigma (smooth approximation)
711
804
  norm_p_sigma = new_p_sigma.norm()
@@ -713,8 +806,7 @@ class CMAES(Algorithm):
713
806
  h_sigma = torch.sigmoid(10 * (threshold - norm_p_sigma / self._chi_n))
714
807
 
715
808
  # Update p_c (evolution path for covariance)
716
- cc = self.cc
717
- 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
718
810
 
719
811
  return new_p_sigma, new_p_c, h_sigma
720
812
 
@@ -723,6 +815,10 @@ class CMAES(Algorithm):
723
815
  y_selected: Tensor,
724
816
  new_p_c: Tensor,
725
817
  h_sigma: Tensor,
818
+ recombination_weights: Tensor,
819
+ cc: Tensor,
820
+ c1: Tensor,
821
+ cmu: Tensor,
726
822
  ) -> Tensor:
727
823
  """
728
824
  Update covariance matrix C.
@@ -736,9 +832,6 @@ class CMAES(Algorithm):
736
832
  New Cholesky factor L.
737
833
  """
738
834
  D = self.n_var
739
- c1 = self.c1
740
- cmu = self.cmu
741
- cc = self.cc
742
835
 
743
836
  # Current covariance
744
837
  L = self.L
@@ -749,7 +842,7 @@ class CMAES(Algorithm):
749
842
 
750
843
  # Rank-mu update
751
844
  rank_mu = (
752
- self._weights.unsqueeze(-1).unsqueeze(-1) *
845
+ recombination_weights.unsqueeze(-1).unsqueeze(-1) *
753
846
  y_selected.unsqueeze(-1) * y_selected.unsqueeze(-2)
754
847
  ).sum(dim=0)
755
848
 
@@ -853,7 +946,12 @@ class CMAES(Algorithm):
853
946
 
854
947
  return L_identity
855
948
 
856
- 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:
857
955
  """
858
956
  Update step-size using CSA (Cumulative Step-size Adaptation).
859
957
 
@@ -863,9 +961,6 @@ class CMAES(Algorithm):
863
961
  Returns:
864
962
  New step-size.
865
963
  """
866
- cs = self.cs
867
- damps = self.damps
868
-
869
964
  # Step-size update factor
870
965
  norm_p_sigma = new_p_sigma.norm()
871
966
  factor = torch.exp((cs / damps) * (norm_p_sigma / self._chi_n - 1))
@@ -1101,6 +1196,10 @@ class CMAES(Algorithm):
1101
1196
  if self.adaptive:
1102
1197
  # Sigma in (1e-10, 1e10)
1103
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
+ )
1104
1203
 
1105
1204
  # Ensure c1 + cmu <= 1
1106
1205
  c1 = torch.sigmoid(self._c1_logit)
@@ -1121,7 +1220,8 @@ class CMAES(Algorithm):
1121
1220
  return {
1122
1221
  'pop_size': self.pop_size,
1123
1222
  'mu': self._mu,
1124
- 'mu_eff': self._mu_eff,
1223
+ 'selection_temperature': float(self.selection_temperature.detach()),
1224
+ 'mu_eff': getattr(self, '_last_mu_eff', self._mu_eff),
1125
1225
  'sigma': float(self.sigma.item()),
1126
1226
  'cc': float(self.cc.item()),
1127
1227
  'cs': float(self.cs.item()),
@@ -1365,4 +1465,4 @@ def cmaes_bipop(
1365
1465
  incpopsize=incpopsize,
1366
1466
  bipop=True,
1367
1467
  **kwargs,
1368
- )
1468
+ )
@@ -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
 
@@ -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: