evograd-diff 0.1.0__tar.gz → 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/.claude/settings.local.json +5 -1
  2. evograd_diff-0.1.1/.gitignore +15 -0
  3. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/PKG-INFO +19 -12
  4. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/README.md +18 -11
  5. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/algorithms/cmaes.py +8 -5
  6. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/core/algorithm.py +20 -4
  7. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/tests/run_all.py +22 -1
  8. evograd_diff-0.1.1/evograd/tests/test_cmaes.py +143 -0
  9. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/tests/test_ga.py +21 -40
  10. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/tests/test_operators.py +8 -8
  11. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/tests/test_per_individual.py +26 -18
  12. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/tests/test_utils.py +4 -2
  13. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/pyproject.toml +1 -1
  14. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/uv.lock +2 -2
  15. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/.python-version +0 -0
  16. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/LICENSE +0 -0
  17. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/Test_new_evograd.ipynb +0 -0
  18. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/__init__.py +0 -0
  19. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/algorithms/__init__.py +0 -0
  20. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/algorithms/de.py +0 -0
  21. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/algorithms/ga.py +0 -0
  22. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/algorithms/pso.py +0 -0
  23. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/algorithms/shade.py +0 -0
  24. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/benchmarks/functions/__init__.py +0 -0
  25. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/benchmarks/functions/base.py +0 -0
  26. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/benchmarks/functions/cec2017/__init__.py +0 -0
  27. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/benchmarks/functions/cec2017/basic.py +0 -0
  28. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/benchmarks/functions/cec2017/composition.py +0 -0
  29. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/benchmarks/functions/cec2017/data.pkl +0 -0
  30. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/benchmarks/functions/cec2017/data.py +0 -0
  31. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/benchmarks/functions/cec2017/hybrid.py +0 -0
  32. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/benchmarks/functions/cec2017/simple.py +0 -0
  33. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/benchmarks/functions/classical.py +0 -0
  34. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/benchmarks/functions/smoothed_funnel.py +0 -0
  35. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/benchmarks/functions/transforms.py +0 -0
  36. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/benchmarks/run_benchmark_functions.py +0 -0
  37. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/core/__init__.py +0 -0
  38. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/core/maximize.py +0 -0
  39. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/core/minimize.py +0 -0
  40. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/core/problem.py +0 -0
  41. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/core/result.py +0 -0
  42. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/core/termination.py +0 -0
  43. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/operators/__init__.py +0 -0
  44. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/operators/crossover.py +0 -0
  45. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/operators/mutation.py +0 -0
  46. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/operators/relaxations.py +0 -0
  47. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/operators/repair.py +0 -0
  48. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/operators/sampling.py +0 -0
  49. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/operators/selection.py +0 -0
  50. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/operators/survival.py +0 -0
  51. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/tests/__init__.py +0 -0
  52. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/tests/test_core.py +0 -0
  53. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/utils/__init__.py +0 -0
  54. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/utils/callbacks.py +0 -0
  55. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/utils/device.py +0 -0
  56. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/evograd/utils/duplicates.py +0 -0
  57. {evograd_diff-0.1.0 → evograd_diff-0.1.1}/plot_benchmarks.py +0 -0
@@ -6,7 +6,11 @@
6
6
  "Bash(git push *)",
7
7
  "Bash(uv build *)",
8
8
  "Bash(python -m zipfile -l dist/evograd-0.1.0-py3-none-any.whl)",
9
- "WebFetch(domain:pypi.org)"
9
+ "WebFetch(domain:pypi.org)",
10
+ "Bash(uv run *)",
11
+ "Bash(git check-ignore *)",
12
+ "Bash(uv lock *)",
13
+ "Bash(git stash *)"
10
14
  ]
11
15
  }
12
16
  }
@@ -0,0 +1,15 @@
1
+ # Build artifacts
2
+ dist/
3
+ build/
4
+ *.egg-info/
5
+
6
+ # Python caches
7
+ __pycache__/
8
+ *.py[cod]
9
+
10
+ # Virtual environments
11
+ .venv/
12
+ venv/
13
+
14
+ # macOS
15
+ .DS_Store
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evograd-diff
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: PyTorch-based framework for differentiable evolutionary computation and swarm intelligence
5
5
  Project-URL: Homepage, https://github.com/andreatangherloni/EvoGrad
6
6
  Project-URL: Repository, https://github.com/andreatangherloni/EvoGrad
@@ -47,14 +47,21 @@ Description-Content-Type: text/markdown
47
47
  ## 📦 Installation
48
48
 
49
49
  ```bash
50
- # Clone the repository
51
- git clone https://github.com/YOUR_USERNAME/evograd.git
52
- cd evograd
50
+ # From PyPI (the import name is `evograd`)
51
+ pip install evograd-diff
52
+ ```
53
+
54
+ Or install directly from the repository:
55
+
56
+ ```bash
57
+ pip install "git+https://github.com/andreatangherloni/EvoGrad.git"
58
+ ```
53
59
 
54
- # Install dependencies
55
- pip install torch numpy
60
+ For local development:
56
61
 
57
- # Install in development mode
62
+ ```bash
63
+ git clone https://github.com/andreatangherloni/EvoGrad.git
64
+ cd EvoGrad
58
65
  pip install -e .
59
66
  ```
60
67
 
@@ -62,7 +69,7 @@ pip install -e .
62
69
 
63
70
  ```python
64
71
  import torch
65
- from evograd.core import Problem, minimize
72
+ from evograd.core import Problem, minimize, MaxEvaluations
66
73
  from evograd.algorithms import GA, DE, PSO, CMAES
67
74
 
68
75
  # Define an optimisation problem
@@ -75,22 +82,22 @@ problem = Problem(
75
82
 
76
83
  # Run with Genetic Algorithm
77
84
  ga = GA(pop_size=100, differentiable=True)
78
- result = minimize(problem, ga, max_evals=10000, seed=42)
85
+ result = minimize(problem, ga, termination=MaxEvaluations(10000), seed=42)
79
86
  print(f"GA Best: {result.best_fitness:.6f}")
80
87
 
81
88
  # Run with Differential Evolution
82
89
  de = DE(pop_size=100, variant="DE/rand/1/bin", adaptive=True)
83
- result = minimize(problem, de, max_evals=10000, seed=42)
90
+ result = minimize(problem, de, termination=MaxEvaluations(10000), seed=42)
84
91
  print(f"DE Best: {result.best_fitness:.6f}")
85
92
 
86
93
  # Run with Particle Swarm Optimisation
87
94
  pso = PSO(pop_size=100, adaptive=True, differentiable=True)
88
- result = minimize(problem, pso, max_evals=10000, seed=42)
95
+ result = minimize(problem, pso, termination=MaxEvaluations(10000), seed=42)
89
96
  print(f"PSO Best: {result.best_fitness:.6f}")
90
97
 
91
98
  # Run with CMA-ES
92
99
  cmaes = CMAES(sigma=0.5, adaptive=True)
93
- result = minimize(problem, cmaes, max_evals=10000, seed=42)
100
+ result = minimize(problem, cmaes, termination=MaxEvaluations(10000), seed=42)
94
101
  print(f"CMA-ES Best: {result.best_fitness:.6f}")
95
102
  ```
96
103
 
@@ -20,14 +20,21 @@
20
20
  ## 📦 Installation
21
21
 
22
22
  ```bash
23
- # Clone the repository
24
- git clone https://github.com/YOUR_USERNAME/evograd.git
25
- cd evograd
23
+ # From PyPI (the import name is `evograd`)
24
+ pip install evograd-diff
25
+ ```
26
+
27
+ Or install directly from the repository:
28
+
29
+ ```bash
30
+ pip install "git+https://github.com/andreatangherloni/EvoGrad.git"
31
+ ```
26
32
 
27
- # Install dependencies
28
- pip install torch numpy
33
+ For local development:
29
34
 
30
- # Install in development mode
35
+ ```bash
36
+ git clone https://github.com/andreatangherloni/EvoGrad.git
37
+ cd EvoGrad
31
38
  pip install -e .
32
39
  ```
33
40
 
@@ -35,7 +42,7 @@ pip install -e .
35
42
 
36
43
  ```python
37
44
  import torch
38
- from evograd.core import Problem, minimize
45
+ from evograd.core import Problem, minimize, MaxEvaluations
39
46
  from evograd.algorithms import GA, DE, PSO, CMAES
40
47
 
41
48
  # Define an optimisation problem
@@ -48,22 +55,22 @@ problem = Problem(
48
55
 
49
56
  # Run with Genetic Algorithm
50
57
  ga = GA(pop_size=100, differentiable=True)
51
- result = minimize(problem, ga, max_evals=10000, seed=42)
58
+ result = minimize(problem, ga, termination=MaxEvaluations(10000), seed=42)
52
59
  print(f"GA Best: {result.best_fitness:.6f}")
53
60
 
54
61
  # Run with Differential Evolution
55
62
  de = DE(pop_size=100, variant="DE/rand/1/bin", adaptive=True)
56
- result = minimize(problem, de, max_evals=10000, seed=42)
63
+ result = minimize(problem, de, termination=MaxEvaluations(10000), seed=42)
57
64
  print(f"DE Best: {result.best_fitness:.6f}")
58
65
 
59
66
  # Run with Particle Swarm Optimisation
60
67
  pso = PSO(pop_size=100, adaptive=True, differentiable=True)
61
- result = minimize(problem, pso, max_evals=10000, seed=42)
68
+ result = minimize(problem, pso, termination=MaxEvaluations(10000), seed=42)
62
69
  print(f"PSO Best: {result.best_fitness:.6f}")
63
70
 
64
71
  # Run with CMA-ES
65
72
  cmaes = CMAES(sigma=0.5, adaptive=True)
66
- result = minimize(problem, cmaes, max_evals=10000, seed=42)
73
+ result = minimize(problem, cmaes, termination=MaxEvaluations(10000), seed=42)
67
74
  print(f"CMA-ES Best: {result.best_fitness:.6f}")
68
75
  ```
69
76
 
@@ -281,15 +281,18 @@ class CMAES(Algorithm):
281
281
  # Setup
282
282
  # =========================================================================
283
283
 
284
- def _setup(self) -> None:
285
- """CMA-ES specific setup after initialization."""
286
- n_var = self.problem.n_var
287
-
284
+ def _setup_pop_size(self) -> None:
285
+ """Compute the default CMA-ES population size (lambda) before sampling."""
288
286
  # Compute default population size if not provided
289
287
  if self._requested_pop_size is None:
288
+ n_var = self.problem.n_var
290
289
  self.pop_size = 4 + int(3 * math.log(n_var))
291
290
  self.n_offsprings = self.pop_size
292
-
291
+
292
+ def _setup(self) -> None:
293
+ """CMA-ES specific setup after initialization."""
294
+ n_var = self.problem.n_var
295
+
293
296
  # Initialize restart state
294
297
  self.restart_state.initial_pop_size = self.pop_size
295
298
  self.restart_state.current_pop_size = self.pop_size
@@ -322,10 +322,21 @@ class Algorithm(nn.Module, ABC):
322
322
  # Optional Hooks (can be overridden)
323
323
  # =========================================================================
324
324
 
325
+ def _setup_pop_size(self) -> None:
326
+ """
327
+ Resolve the population size before the initial population is sampled.
328
+
329
+ Override for algorithms that derive pop_size from the problem
330
+ (e.g. CMA-ES default lambda = 4 + floor(3*ln(n))). Called near the
331
+ start of initialize(), after the problem is attached but before
332
+ sampling, so the population is allocated at the final size.
333
+ """
334
+ pass
335
+
325
336
  def _setup(self) -> None:
326
337
  """
327
338
  One-time setup after initialisation.
328
-
339
+
329
340
  Override to perform algorithm-specific setup that requires
330
341
  the problem and population to be initialized.
331
342
  Called at the end of initialize().
@@ -370,14 +381,17 @@ class Algorithm(nn.Module, ABC):
370
381
 
371
382
  # Move problem bounds to device
372
383
  self.register_buffer(
373
- "xl",
384
+ "xl",
374
385
  problem.xl.to(device=self.device, dtype=self.dtype)
375
386
  )
376
387
  self.register_buffer(
377
388
  "xu",
378
389
  problem.xu.to(device=self.device, dtype=self.dtype)
379
390
  )
380
-
391
+
392
+ # Resolve final population size (e.g. CMA-ES auto lambda) before sampling
393
+ self._setup_pop_size()
394
+
381
395
  # Create initial population using sampling operator
382
396
  population = self.sampling(self.pop_size, problem)
383
397
 
@@ -686,13 +700,15 @@ class Algorithm(nn.Module, ABC):
686
700
  mode = "differentiable" if self.differentiable else "classical"
687
701
  status = "initialized" if self._is_initialized else "not initialized"
688
702
  n_var = self.n_var if self.problem else "?"
703
+ # device is only assigned in initialize(); fall back before then.
704
+ device = getattr(self, "device", "?")
689
705
  return (
690
706
  f"{self.__class__.__name__}("
691
707
  f"pop_size={self.pop_size}, "
692
708
  f"n_var={n_var}, "
693
709
  f"mode={mode}, "
694
710
  f"status={status}, "
695
- f"device={self.device})"
711
+ f"device={device})"
696
712
  )
697
713
 
698
714
  def summary(self) -> str:
@@ -16,6 +16,9 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
16
16
  from tests.test_utils import run_all_tests as test_utils
17
17
  from tests.test_core import run_all_tests as test_core
18
18
  from tests.test_operators import run_all_tests as test_operators
19
+ from tests.test_cmaes import run_all_tests as test_cmaes
20
+ from tests.test_ga import run_all_tests as test_ga
21
+ from tests.test_per_individual import main as test_per_individual
19
22
 
20
23
 
21
24
  def run_all():
@@ -45,7 +48,25 @@ def run_all():
45
48
  print("▶ RUNNING OPERATORS TESTS")
46
49
  print("▶"*60)
47
50
  results['operators'] = test_operators()
48
-
51
+
52
+ # Run CMA-ES tests
53
+ print("\n\n" + "▶"*60)
54
+ print("▶ RUNNING CMA-ES TESTS")
55
+ print("▶"*60)
56
+ results['cmaes'] = test_cmaes()
57
+
58
+ # Run GA tests
59
+ print("\n\n" + "▶"*60)
60
+ print("▶ RUNNING GA TESTS")
61
+ print("▶"*60)
62
+ results['ga'] = test_ga()
63
+
64
+ # Run per-individual parameter tests
65
+ print("\n\n" + "▶"*60)
66
+ print("▶ RUNNING PER-INDIVIDUAL TESTS")
67
+ print("▶"*60)
68
+ results['per_individual'] = test_per_individual()
69
+
49
70
  # Summary
50
71
  print("\n\n" + "█"*60)
51
72
  print("█" + " "*58 + "█")
@@ -0,0 +1,143 @@
1
+ """
2
+ Test script for EvoGrad CMA-ES implementation.
3
+
4
+ Tests:
5
+ - Default (auto) population size: lambda = 4 + floor(3*ln(n))
6
+ - Explicit population size
7
+ - Population tensor is allocated at the resolved pop_size
8
+ - End-to-end minimize() runs and converges
9
+
10
+ Regression:
11
+ With no pop_size, CMA-ES used to allocate the population at a
12
+ placeholder size (10) and only recompute lambda afterwards, causing
13
+ a tensor-size mismatch on the first update for any n_var where
14
+ 4 + floor(3*ln(n)) != 10. See _setup_pop_size().
15
+
16
+ Usage:
17
+ cd evograd && python tests/test_cmaes.py
18
+ """
19
+
20
+ import sys
21
+ import os
22
+ import math
23
+ import torch
24
+
25
+ # Add parent directory to path for imports
26
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
27
+
28
+ from evograd.core.problem import Problem
29
+ from evograd.core.minimize import minimize
30
+ from evograd.core.termination import MaxEvaluations
31
+ from evograd.algorithms.cmaes import CMAES
32
+
33
+
34
+ def sphere(x):
35
+ """Sphere function: sum of squares. Global optimum at origin."""
36
+ return (x ** 2).sum(dim=-1)
37
+
38
+
39
+ def _expected_lambda(n_var):
40
+ """CMA-ES default population size (lambda)."""
41
+ return 4 + int(3 * math.log(n_var))
42
+
43
+
44
+ def make_problem(n_var):
45
+ return Problem(objective=sphere, n_var=n_var, xl=-5.0, xu=5.0)
46
+
47
+
48
+ # =============================================================================
49
+ # Tests
50
+ # =============================================================================
51
+
52
+ def test_auto_pop_size():
53
+ """Default pop_size must resolve to lambda and size the population."""
54
+ print("\n1. Testing auto pop_size (no pop_size given)...")
55
+
56
+ # Cover sizes where the auto value differs from the old placeholder (10).
57
+ for n_var in (2, 30, 50):
58
+ expected = _expected_lambda(n_var)
59
+
60
+ cmaes = CMAES(sigma=0.5)
61
+ cmaes.initialize(make_problem(n_var))
62
+
63
+ assert cmaes.pop_size == expected, (
64
+ f"n_var={n_var}: pop_size {cmaes.pop_size} != expected {expected}"
65
+ )
66
+ assert cmaes.n_offsprings == expected, (
67
+ f"n_var={n_var}: n_offsprings {cmaes.n_offsprings} != {expected}"
68
+ )
69
+ assert tuple(cmaes.population.shape) == (expected, n_var), (
70
+ f"n_var={n_var}: population shape {tuple(cmaes.population.shape)} "
71
+ f"!= {(expected, n_var)}"
72
+ )
73
+ print(f" n_var={n_var:>3}: pop_size={cmaes.pop_size} "
74
+ f"population={tuple(cmaes.population.shape)} ✓")
75
+
76
+
77
+ def test_explicit_pop_size():
78
+ """Explicit pop_size must be honoured exactly."""
79
+ print("\n2. Testing explicit pop_size...")
80
+
81
+ cmaes = CMAES(pop_size=50, sigma=0.5)
82
+ cmaes.initialize(make_problem(30))
83
+
84
+ assert cmaes.pop_size == 50, f"pop_size {cmaes.pop_size} != 50"
85
+ assert tuple(cmaes.population.shape) == (50, 30)
86
+ print(f" pop_size={cmaes.pop_size} "
87
+ f"population={tuple(cmaes.population.shape)} ✓")
88
+
89
+
90
+ def test_step_no_crash():
91
+ """The first update must not raise a size mismatch (the regression)."""
92
+ print("\n3. Testing step() with auto pop_size (regression)...")
93
+
94
+ cmaes = CMAES(sigma=0.5)
95
+ cmaes.initialize(make_problem(30))
96
+ for _ in range(3):
97
+ cmaes.step()
98
+ print(f" 3 steps ran; generation={cmaes.generation}, "
99
+ f"pop_size={cmaes.pop_size} ✓")
100
+
101
+
102
+ def test_convergence():
103
+ """End-to-end minimize() should converge on the sphere function."""
104
+ print("\n4. Testing convergence via minimize()...")
105
+
106
+ problem = make_problem(10)
107
+ cmaes = CMAES(sigma=0.5) # auto pop_size
108
+ result = minimize(
109
+ problem, cmaes, termination=MaxEvaluations(5000),
110
+ seed=42, verbose=False,
111
+ )
112
+ assert result.best_fitness < 1.0, (
113
+ f"did not converge: best_fitness={result.best_fitness}"
114
+ )
115
+ print(f" best_fitness={result.best_fitness:.6f} (< 1.0) ✓")
116
+
117
+
118
+ def run_all_tests():
119
+ """Run all CMA-ES tests."""
120
+ print("\n" + "#"*60)
121
+ print("# EvoGrad CMA-ES Tests")
122
+ print("#"*60)
123
+
124
+ try:
125
+ test_auto_pop_size()
126
+ test_explicit_pop_size()
127
+ test_step_no_crash()
128
+ test_convergence()
129
+
130
+ print("\n" + "="*60)
131
+ print("✓ ALL CMA-ES TESTS PASSED!")
132
+ print("="*60)
133
+ return True
134
+ except Exception as e:
135
+ print(f"\n✗ TEST FAILED: {e}")
136
+ import traceback
137
+ traceback.print_exc()
138
+ return False
139
+
140
+
141
+ if __name__ == "__main__":
142
+ success = run_all_tests()
143
+ sys.exit(0 if success else 1)
@@ -85,10 +85,10 @@ def test_ga_creation():
85
85
  print("\n2. Testing GA with custom operators...")
86
86
  ga = GA(
87
87
  pop_size=30,
88
- selection=RouletteSelection(differentiable=True),
89
- crossover=BlendCrossover(alpha=0.5, differentiable=True),
90
- mutation=GaussianMutation(sigma=0.1, differentiable=True),
91
- survival=MergeSurvival(elitism=True, n_elite=2, differentiable=True),
88
+ selection=RouletteSelection(adaptive=True),
89
+ crossover=BlendCrossover(alpha=0.5, adaptive=True),
90
+ mutation=GaussianMutation(sigma=0.1, adaptive=True),
91
+ survival=MergeSurvival(elitism=True, n_elite=2, adaptive=True),
92
92
  )
93
93
  print(f" Created: {ga}")
94
94
  print(f" Survival: {ga.survival}")
@@ -165,10 +165,10 @@ def test_ga_step():
165
165
  ga = GA(
166
166
  pop_size=30,
167
167
  sampling=UniformSampling(seed=42),
168
- selection=TournamentSelection(tournament_size=3, differentiable=True),
169
- crossover=SBXCrossover(eta=15, prob=0.9, differentiable=True),
170
- mutation=PolynomialMutation(eta=20, differentiable=True),
171
- survival=MergeSurvival(n_survive=30, elitism=True, n_elite=1, differentiable=True),
168
+ selection=TournamentSelection(tournament_size=3, adaptive=True),
169
+ crossover=SBXCrossover(eta=15, prob=0.9, adaptive=True),
170
+ mutation=PolynomialMutation(eta=20, adaptive=True),
171
+ survival=MergeSurvival(n_survive=30, elitism=True, n_elite=1, adaptive=True),
172
172
  differentiable=True,
173
173
  )
174
174
  ga.initialize(problem)
@@ -212,9 +212,7 @@ def test_survival_strategies():
212
212
  print("\n1. Testing MergeSurvival (mu+lambda)...")
213
213
  ga_plus = GA(
214
214
  pop_size=20,
215
- survival=MergeSurvival(n_survive=20, elitism=True, n_elite=1),
216
- seed=42,
217
- )
215
+ survival=MergeSurvival(n_survive=20, elitism=True, n_elite=1), )
218
216
  ga_plus.initialize(problem)
219
217
  for _ in range(5):
220
218
  ga_plus.step()
@@ -225,9 +223,7 @@ def test_survival_strategies():
225
223
  ga_comma_inst = GA(
226
224
  pop_size=20,
227
225
  n_offsprings=40, # Must be >= pop_size
228
- survival=CommaSurvival(n_survive=20, elitism=True, n_elite=1),
229
- seed=42,
230
- )
226
+ survival=CommaSurvival(n_survive=20, elitism=True, n_elite=1), )
231
227
  ga_comma_inst.initialize(problem)
232
228
  for _ in range(5):
233
229
  ga_comma_inst.step()
@@ -238,9 +234,7 @@ def test_survival_strategies():
238
234
  ga_replace = GA(
239
235
  pop_size=20,
240
236
  n_offsprings=5,
241
- survival=ReplaceWorstSurvival(n_survive=20, elitism=True, n_elite=1),
242
- seed=42,
243
- )
237
+ survival=ReplaceWorstSurvival(n_survive=20, elitism=True, n_elite=1), )
244
238
  ga_replace.initialize(problem)
245
239
  for _ in range(20): # More generations since fewer offspring per gen
246
240
  ga_replace.step()
@@ -250,9 +244,7 @@ def test_survival_strategies():
250
244
  print("\n4. Testing FitnessSurvival (pure truncation)...")
251
245
  ga_fitness = GA(
252
246
  pop_size=20,
253
- survival=FitnessSurvival(n_survive=20),
254
- seed=42,
255
- )
247
+ survival=FitnessSurvival(n_survive=20), )
256
248
  ga_fitness.initialize(problem)
257
249
  for _ in range(5):
258
250
  ga_fitness.step()
@@ -278,9 +270,7 @@ def test_elitism():
278
270
  print("\n1. Testing GA with elitism...")
279
271
  ga_elite = GA(
280
272
  pop_size=20,
281
- survival=MergeSurvival(n_survive=20, elitism=True, n_elite=1),
282
- seed=42,
283
- )
273
+ survival=MergeSurvival(n_survive=20, elitism=True, n_elite=1), )
284
274
  ga_elite.initialize(problem)
285
275
 
286
276
  best_values = [ga_elite.best_fitness]
@@ -299,7 +289,6 @@ def test_elitism():
299
289
  ga_no_elite = GA(
300
290
  pop_size=20,
301
291
  survival=FitnessSurvival(n_survive=20), # No elitism
302
- seed=42,
303
292
  )
304
293
  ga_no_elite.initialize(problem)
305
294
 
@@ -325,9 +314,7 @@ def test_differentiable_mode():
325
314
 
326
315
  ga = GA(
327
316
  pop_size=20,
328
- differentiable=True,
329
- seed=42,
330
- )
317
+ differentiable=True, )
331
318
  ga.initialize(problem)
332
319
 
333
320
  # Create optimizer for learnable parameters
@@ -386,9 +373,7 @@ def test_state_persistence():
386
373
  sampling=UniformSampling(seed=42),
387
374
  selection=TournamentSelection(tournament_size=3),
388
375
  crossover=SBXCrossover(eta=15, prob=0.9),
389
- mutation=PolynomialMutation(eta=20),
390
- seed=42,
391
- )
376
+ mutation=PolynomialMutation(eta=20), )
392
377
  ga1.initialize(problem)
393
378
 
394
379
  for _ in range(10):
@@ -408,9 +393,7 @@ def test_state_persistence():
408
393
  sampling=UniformSampling(seed=42),
409
394
  selection=TournamentSelection(tournament_size=3),
410
395
  crossover=SBXCrossover(eta=15, prob=0.9),
411
- mutation=PolynomialMutation(eta=20),
412
- seed=0, # Different seed
413
- )
396
+ mutation=PolynomialMutation(eta=20), )
414
397
  ga2.initialize(problem)
415
398
 
416
399
  print(f"\n2. New GA before load:")
@@ -475,9 +458,7 @@ def test_convergence():
475
458
  selection=TournamentSelection(tournament_size=3),
476
459
  crossover=SBXCrossover(eta=15, prob=0.9),
477
460
  mutation=PolynomialMutation(eta=20),
478
- survival=MergeSurvival(n_survive=50, elitism=True, n_elite=1),
479
- seed=42,
480
- )
461
+ survival=MergeSurvival(n_survive=50, elitism=True, n_elite=1), )
481
462
  ga.initialize(problem)
482
463
 
483
464
  print(f"\n1. Running GA for 100 generations...")
@@ -513,10 +494,10 @@ def test_hyperparams():
513
494
 
514
495
  ga = GA(
515
496
  pop_size=20,
516
- selection=TournamentSelection(tournament_size=3, differentiable=True),
517
- crossover=SBXCrossover(eta=15, prob=0.9, differentiable=True),
518
- mutation=PolynomialMutation(eta=20, differentiable=True),
519
- survival=MergeSurvival(n_survive=20, elitism=True, n_elite=2, differentiable=True),
497
+ selection=TournamentSelection(tournament_size=3, adaptive=True),
498
+ crossover=SBXCrossover(eta=15, prob=0.9, adaptive=True),
499
+ mutation=PolynomialMutation(eta=20, adaptive=True),
500
+ survival=MergeSurvival(n_survive=20, elitism=True, n_elite=2, adaptive=True),
520
501
  )
521
502
  ga.initialize(problem)
522
503
  ga.step()
@@ -170,7 +170,7 @@ def test_selection():
170
170
  print("\n2. Testing TournamentSelection (differentiable)...")
171
171
  tournament_diff = TournamentSelection(
172
172
  tournament_size=3,
173
- differentiable=True,
173
+ adaptive=True,
174
174
  temperature=1.0,
175
175
  learn_temperature=True,
176
176
  )
@@ -247,7 +247,7 @@ def test_crossover():
247
247
  sbx_diff = SBXCrossover(
248
248
  eta=15,
249
249
  prob=0.9,
250
- differentiable=True,
250
+ adaptive=True,
251
251
  learn_eta=True,
252
252
  learn_prob=True,
253
253
  )
@@ -274,7 +274,7 @@ def test_crossover():
274
274
  print(f" Offspring shape: {offspring_bin.shape}")
275
275
 
276
276
  # Test differentiable binomial
277
- binomial_diff = BinomialCrossover(cr=0.9, differentiable=True, learn_cr=True)
277
+ binomial_diff = BinomialCrossover(cr=0.9, adaptive=True, learn_cr=True)
278
278
  p1 = torch.nn.Parameter(parent1.clone())
279
279
  offspring_bin_diff = binomial_diff(p1, parent2)
280
280
  loss = offspring_bin_diff.sum()
@@ -343,7 +343,7 @@ def test_mutation():
343
343
  poly_diff = PolynomialMutation(
344
344
  eta=20,
345
345
  prob=0.1,
346
- differentiable=True,
346
+ adaptive=True,
347
347
  learn_eta=True,
348
348
  learn_prob=True,
349
349
  )
@@ -367,7 +367,7 @@ def test_mutation():
367
367
  print(f" With sigma_frac: ✓")
368
368
 
369
369
  # Test differentiable Gaussian
370
- gauss_diff = GaussianMutation(sigma=0.1, differentiable=True, learn_sigma=True)
370
+ gauss_diff = GaussianMutation(sigma=0.1, adaptive=True, learn_sigma=True)
371
371
  pop_param = torch.nn.Parameter(population.clone())
372
372
  mutated_gauss_diff = gauss_diff(pop_param, xl, xu)
373
373
  loss = mutated_gauss_diff.sum()
@@ -592,18 +592,18 @@ def test_operator_integration():
592
592
  # Create differentiable operators
593
593
  selection_diff = TournamentSelection(
594
594
  tournament_size=3,
595
- differentiable=True,
595
+ adaptive=True,
596
596
  temperature=1.0,
597
597
  )
598
598
  crossover_diff = SBXCrossover(
599
599
  eta=15,
600
600
  prob=0.9,
601
- differentiable=True,
601
+ adaptive=True,
602
602
  learn_eta=True,
603
603
  )
604
604
  mutation_diff = GaussianMutation(
605
605
  sigma=0.1,
606
- differentiable=True,
606
+ adaptive=True,
607
607
  learn_sigma=True,
608
608
  )
609
609
 
@@ -266,7 +266,7 @@ def test_differentiable_mode():
266
266
 
267
267
  # Differentiable operators
268
268
  print("\n1. SBXCrossover with gradient flow:")
269
- sbx = SBXCrossover(eta=15, prob=0.9, differentiable=True, learn_eta=True)
269
+ sbx = SBXCrossover(eta=15, prob=0.9, adaptive=True, learn_eta=True)
270
270
 
271
271
  p1 = torch.nn.Parameter(torch.randn(N, D))
272
272
  p2 = torch.randn(N, D)
@@ -282,7 +282,7 @@ def test_differentiable_mode():
282
282
 
283
283
  # PolynomialMutation with gradient flow
284
284
  print("\n2. PolynomialMutation with gradient flow:")
285
- mutation = PolynomialMutation(eta=20, prob=0.1, differentiable=True, learn_eta=True)
285
+ mutation = PolynomialMutation(eta=20, prob=0.1, adaptive=True, learn_eta=True)
286
286
 
287
287
  x = torch.nn.Parameter(torch.randn(N, D))
288
288
  xl = torch.zeros(D)
@@ -305,22 +305,30 @@ def main():
305
305
  print("\n" + "#" * 70)
306
306
  print("# Per-Individual/Per-Gene Parameter Support Tests")
307
307
  print("#" * 70)
308
-
309
- test_crossover_configurations()
310
- test_mutation_configurations()
311
- test_shade_style_usage()
312
- test_differentiable_mode()
313
-
314
- print("\n" + "=" * 70)
315
- print("ALL TESTS PASSED! ✓")
316
- print("=" * 70)
317
-
318
- print("\nSummary of Four Configurations:")
319
- print(" 1. Fixed (scalar) - Same value for all")
320
- print(" 2. Per-gene [D] - Different per variable")
321
- print(" 3. Per-individual [N] - Different per individual (SHADE needs this!)")
322
- print(" 4. Full matrix [N, D] - Maximum flexibility")
308
+
309
+ try:
310
+ test_crossover_configurations()
311
+ test_mutation_configurations()
312
+ test_shade_style_usage()
313
+ test_differentiable_mode()
314
+
315
+ print("\n" + "=" * 70)
316
+ print("ALL TESTS PASSED! ✓")
317
+ print("=" * 70)
318
+
319
+ print("\nSummary of Four Configurations:")
320
+ print(" 1. Fixed (scalar) - Same value for all")
321
+ print(" 2. Per-gene [D] - Different per variable")
322
+ print(" 3. Per-individual [N] - Different per individual (SHADE needs this!)")
323
+ print(" 4. Full matrix [N, D] - Maximum flexibility")
324
+ return True
325
+ except Exception as e:
326
+ print(f"\n✗ TEST FAILED: {e}")
327
+ import traceback
328
+ traceback.print_exc()
329
+ return False
323
330
 
324
331
 
325
332
  if __name__ == "__main__":
326
- main()
333
+ success = main()
334
+ sys.exit(0 if success else 1)
@@ -183,8 +183,10 @@ def test_callbacks():
183
183
 
184
184
  print(f" Tracked generations: {len(history_cb.generations)}")
185
185
  print(f" Best fitness history: {history_cb.best_fitness[:3]}...")
186
- assert len(history_cb.generations) == 5
187
- assert len(history_cb.best_fitness) == 5
186
+ # on_optimisation_start records the initial state, then each of the 5
187
+ # on_generation_end calls records one more -> 1 + 5 = 6 entries.
188
+ assert len(history_cb.generations) == 6
189
+ assert len(history_cb.best_fitness) == 6
188
190
 
189
191
  # Test PrintCallback
190
192
  print("\n2. Testing PrintCallback...")
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "evograd-diff"
7
- version = "0.1.0"
7
+ version = "0.1.1"
8
8
  description = "PyTorch-based framework for differentiable evolutionary computation and swarm intelligence"
9
9
  readme = "README.md"
10
10
  license = { text = "Apache-2.0" }
@@ -155,8 +155,8 @@ wheels = [
155
155
  ]
156
156
 
157
157
  [[package]]
158
- name = "evograd"
159
- version = "0.1.0"
158
+ name = "evograd-diff"
159
+ version = "0.1.1"
160
160
  source = { editable = "." }
161
161
  dependencies = [
162
162
  { name = "matplotlib" },
File without changes