evograd-diff 0.2.0__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/PKG-INFO +1 -1
  2. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/__init__.py +1 -1
  3. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/algorithms/cmaes.py +4 -7
  4. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/algorithms/de.py +4 -4
  5. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/algorithms/pso.py +3 -2
  6. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/algorithms/shade.py +7 -8
  7. evograd_diff-0.2.1/evograd/benchmarks/functions/cec2017/_base.py +51 -0
  8. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/benchmarks/functions/cec2017/hybrid.py +10 -40
  9. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/benchmarks/functions/cec2017/simple.py +9 -46
  10. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/operators/crossover.py +3 -5
  11. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/operators/mutation.py +6 -8
  12. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/operators/relaxations.py +30 -1
  13. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/operators/repair.py +11 -1
  14. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/operators/sampling.py +5 -7
  15. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/operators/selection.py +2 -4
  16. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/tests/run_all.py +21 -0
  17. evograd_diff-0.2.1/evograd/tests/test_de.py +62 -0
  18. evograd_diff-0.2.1/evograd/tests/test_pso.py +62 -0
  19. evograd_diff-0.2.1/evograd/tests/test_shade.py +64 -0
  20. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/pyproject.toml +1 -1
  21. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/.gitignore +0 -0
  22. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/LICENSE +0 -0
  23. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/README.md +0 -0
  24. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/algorithms/__init__.py +0 -0
  25. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/algorithms/ga.py +0 -0
  26. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/benchmarks/functions/__init__.py +0 -0
  27. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/benchmarks/functions/base.py +0 -0
  28. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/benchmarks/functions/cec2017/__init__.py +0 -0
  29. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/benchmarks/functions/cec2017/basic.py +0 -0
  30. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/benchmarks/functions/cec2017/composition.py +0 -0
  31. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/benchmarks/functions/cec2017/data.pkl +0 -0
  32. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/benchmarks/functions/cec2017/data.py +0 -0
  33. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/benchmarks/functions/classical.py +0 -0
  34. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/benchmarks/functions/smoothed_funnel.py +0 -0
  35. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/benchmarks/functions/transforms.py +0 -0
  36. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/benchmarks/run_benchmark_functions.py +0 -0
  37. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/core/__init__.py +0 -0
  38. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/core/algorithm.py +0 -0
  39. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/core/maximize.py +0 -0
  40. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/core/minimize.py +0 -0
  41. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/core/problem.py +0 -0
  42. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/core/result.py +0 -0
  43. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/core/termination.py +0 -0
  44. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/operators/__init__.py +0 -0
  45. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/operators/survival.py +0 -0
  46. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/tests/__init__.py +0 -0
  47. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/tests/test_cmaes.py +0 -0
  48. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/tests/test_core.py +0 -0
  49. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/tests/test_ga.py +0 -0
  50. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/tests/test_operators.py +0 -0
  51. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/tests/test_per_individual.py +0 -0
  52. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/tests/test_utils.py +0 -0
  53. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/utils/__init__.py +0 -0
  54. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/utils/callbacks.py +0 -0
  55. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/utils/device.py +0 -0
  56. {evograd_diff-0.2.0 → evograd_diff-0.2.1}/evograd/utils/duplicates.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evograd-diff
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: PyTorch-based framework for differentiable evolutionary computation and swarm intelligence
5
5
  Project-URL: Homepage, https://github.com/andreatangherloni/EvoGrad
6
6
  Project-URL: Repository, https://github.com/andreatangherloni/EvoGrad
@@ -57,7 +57,7 @@ Authors
57
57
  Andrea Tangherloni <andrea.tangherloni@unibocconi.it>
58
58
  """
59
59
 
60
- __version__ = "0.2.0"
60
+ __version__ = "0.2.1"
61
61
  __author__ = "Andrea Tangherloni"
62
62
 
63
63
  __all__ = [
@@ -83,6 +83,7 @@ import torch.nn as nn
83
83
  from torch import Tensor
84
84
 
85
85
  from evograd.core.algorithm import Algorithm
86
+ from evograd.operators.relaxations import standard_normal, log_param
86
87
 
87
88
  if TYPE_CHECKING:
88
89
  from evograd.core.problem import Problem
@@ -386,9 +387,7 @@ class CMAES(Algorithm):
386
387
  with torch.no_grad():
387
388
  self._log_sigma.fill_(math.log(sigma_val))
388
389
  else:
389
- self._log_sigma = nn.Parameter(
390
- torch.tensor(sigma_val, device=self.device, dtype=self.dtype).log()
391
- )
390
+ self._log_sigma = log_param(sigma_val, device=self.device, dtype=self.dtype)
392
391
  else:
393
392
  if hasattr(self, '_sigma_buffer'):
394
393
  self._sigma_buffer.fill_(sigma_val)
@@ -467,9 +466,7 @@ class CMAES(Algorithm):
467
466
  self._to_logit(cmu).to(device=self.device, dtype=self.dtype)
468
467
  )
469
468
  # damps is positive, store as log
470
- self._log_damps = nn.Parameter(
471
- torch.tensor(damps, device=self.device, dtype=self.dtype).log()
472
- )
469
+ self._log_damps = log_param(damps, device=self.device, dtype=self.dtype)
473
470
  else:
474
471
  if not hasattr(self, '_cc'):
475
472
  self.register_buffer("_cc", torch.tensor(cc, device=self.device, dtype=self.dtype))
@@ -587,7 +584,7 @@ class CMAES(Algorithm):
587
584
  N, D = self.pop_size, self.n_var
588
585
 
589
586
  # Sample standard normal
590
- z = torch.randn(N, D, device=self.device, dtype=self.dtype)
587
+ z = standard_normal(N, D, device=self.device, dtype=self.dtype)
591
588
 
592
589
  # Transform: y = L @ z
593
590
  L = self.L
@@ -63,6 +63,8 @@ import torch.nn as nn
63
63
  from torch import Tensor
64
64
 
65
65
  from evograd.core.algorithm import Algorithm
66
+ from evograd.operators.repair import clamp_to_bounds
67
+ from evograd.operators.relaxations import log_param
66
68
 
67
69
  if TYPE_CHECKING:
68
70
  from evograd.core.problem import Problem
@@ -308,9 +310,7 @@ class DE(Algorithm):
308
310
  # Setup F parameter
309
311
  if self.adaptive:
310
312
  # Learnable F stored as log(F) for positivity
311
- self._log_F = nn.Parameter(
312
- torch.tensor(self._init_F, device=self.device, dtype=self.dtype).log()
313
- )
313
+ self._log_F = log_param(self._init_F, device=self.device, dtype=self.dtype)
314
314
  else:
315
315
  self.register_buffer(
316
316
  "_F_buffer",
@@ -627,7 +627,7 @@ class DE(Algorithm):
627
627
  trial = self.repair(trial, self.xl, self.xu)
628
628
  else:
629
629
  # Default: clamp to bounds
630
- trial = torch.clamp(trial, self.xl, self.xu)
630
+ trial = clamp_to_bounds(trial, self.xl, self.xu)
631
631
 
632
632
  return trial
633
633
 
@@ -52,6 +52,7 @@ import torch.nn as nn
52
52
  from torch import Tensor
53
53
 
54
54
  from evograd.core.algorithm import Algorithm
55
+ from evograd.operators.repair import clamp_to_bounds
55
56
 
56
57
  if TYPE_CHECKING:
57
58
  from evograd.core.problem import Problem
@@ -357,7 +358,7 @@ class PSO(Algorithm):
357
358
  pos_repaired = torch.where(above, 2 * self.xu - position, pos_repaired)
358
359
 
359
360
  # Clamp to ensure within bounds (in case of large violations)
360
- pos_repaired = torch.clamp(pos_repaired, self.xl, self.xu)
361
+ pos_repaired = clamp_to_bounds(pos_repaired, self.xl, self.xu)
361
362
 
362
363
  # Reverse velocity at boundaries
363
364
  vel_repaired = velocity.clone()
@@ -473,7 +474,7 @@ class PSO(Algorithm):
473
474
  old_pos = new_pos - new_velocity # P₀
474
475
  grad_delta = self._population.data - old_pos # −lr·∇
475
476
  combined = new_pos + grad_delta # P₀ + v − lr·∇
476
- combined = torch.clamp(combined, self.xl, self.xu)
477
+ combined = clamp_to_bounds(combined, self.xl, self.xu)
477
478
  self._population.copy_(combined)
478
479
  else:
479
480
  self._population.copy_(new_pos)
@@ -65,6 +65,7 @@ import torch.nn as nn
65
65
  from torch import Tensor
66
66
 
67
67
  from evograd.core.algorithm import Algorithm
68
+ from evograd.operators.relaxations import standard_normal
68
69
 
69
70
  if TYPE_CHECKING:
70
71
  from evograd.core.problem import Problem
@@ -197,7 +198,7 @@ class SHADEMemory:
197
198
 
198
199
  # Sample from Normal distribution
199
200
  sigma = 0.1 # Standard SHADE scale parameter
200
- CR = mu_CR + sigma * torch.randn(n, device=device, dtype=dtype)
201
+ CR = mu_CR + sigma * standard_normal(n, device=device, dtype=dtype)
201
202
 
202
203
  # Truncate to [0, 1]
203
204
  CR = torch.clamp(CR, min=0.0, max=1.0)
@@ -520,7 +521,7 @@ class SHADE(Algorithm):
520
521
 
521
522
  # Reparameterized Normal for CR
522
523
  sigma = 0.1
523
- eps = torch.randn(N, device=self.device, dtype=self.dtype)
524
+ eps = standard_normal(N, device=self.device, dtype=self.dtype)
524
525
  CR = mu_CR + sigma * eps
525
526
  CR = torch.clamp(CR, min=0.0, max=1.0)
526
527
  else:
@@ -886,8 +887,6 @@ class LSHADE(SHADE):
886
887
  adaptive: bool = False,
887
888
  differentiable: bool = False,
888
889
  selection_temperature: float = 1.0,
889
- seed: Optional[int] = None,
890
- device: Optional[Union[str, torch.device]] = None,
891
890
  dtype: torch.dtype = torch.float32,
892
891
  ) -> None:
893
892
  self.pop_size_init = pop_size_init # Will be set in _setup if None
@@ -909,8 +908,6 @@ class LSHADE(SHADE):
909
908
  differentiable=differentiable,
910
909
  adaptive=adaptive,
911
910
  selection_temperature=selection_temperature,
912
- seed=seed,
913
- device=device,
914
911
  dtype=dtype,
915
912
  )
916
913
 
@@ -919,8 +916,10 @@ class LSHADE(SHADE):
919
916
  # Set default pop_size_init based on problem dimension
920
917
  if self.pop_size_init is None:
921
918
  self.pop_size_init = 18 * self.problem.n_var
922
- self._pop_size = self.pop_size_init
923
-
919
+ # Always initialise the working population size (fixes target_pop_size
920
+ # AttributeError when pop_size_init was provided explicitly).
921
+ self._pop_size = self.pop_size_init
922
+
924
923
  # Create success-history memory with L-SHADE archive size
925
924
  self.memory = SHADEMemory.create(
926
925
  H=self.memory_size,
@@ -0,0 +1,51 @@
1
+ """
2
+ Shared base for CEC 2017 functions.
3
+
4
+ Single source of truth for the bias, shift/rotation loading, default bounds, and
5
+ the shift+rotate transform used by the simple and hybrid CEC 2017 families
6
+ (previously copy-pasted in simple.py and hybrid.py).
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from typing import Optional, Tuple
11
+
12
+ from torch import Tensor
13
+
14
+ from ..base import BenchmarkFunction
15
+ from . import data as cec_data
16
+
17
+
18
+ class CEC2017Base(BenchmarkFunction):
19
+ """Base for shifted+rotated CEC 2017 functions (search space [-100, 100]^D)."""
20
+
21
+ def __init__(
22
+ self,
23
+ func_num: int,
24
+ n_var: int = 10,
25
+ rotation: Optional[Tensor] = None,
26
+ shift: Optional[Tensor] = None,
27
+ seed: Optional[int] = None,
28
+ ):
29
+ super().__init__(n_var=n_var, xl=-100.0, xu=100.0)
30
+
31
+ self.func_num = func_num
32
+ self.bias = func_num * 100.0 # F_i* = i * 100
33
+
34
+ if rotation is not None:
35
+ self.rotation = rotation
36
+ else:
37
+ self.rotation = cec_data.get_rotation(func_num, n_var, seed=seed)
38
+
39
+ if shift is not None:
40
+ self.shift = shift
41
+ else:
42
+ self.shift = cec_data.get_shift(func_num, n_var, seed=seed)
43
+
44
+ def default_bounds(self) -> Tuple[float, float]:
45
+ return (-100.0, 100.0)
46
+
47
+ def _shift_rotate(self, x: Tensor) -> Tensor:
48
+ """Apply the shift + rotation transform (single source of truth)."""
49
+ shift = self.shift.to(x.device, x.dtype)
50
+ rotation = self.rotation.to(x.device, x.dtype)
51
+ return cec_data.shift_rotate(x, shift, rotation)
@@ -26,11 +26,15 @@ from torch import Tensor
26
26
  from ..base import BenchmarkFunction
27
27
  from . import basic
28
28
  from . import data as cec_data
29
+ from ._base import CEC2017Base
29
30
 
30
31
 
31
- class CEC2017HybridFunction(BenchmarkFunction):
32
- """Base class for CEC 2017 hybrid functions."""
33
-
32
+ class CEC2017HybridFunction(CEC2017Base):
33
+ """Base class for CEC 2017 hybrid functions (adds a shuffle permutation).
34
+
35
+ Shared shift/rotation handling is inherited from CEC2017Base (see _base.py).
36
+ """
37
+
34
38
  def __init__(
35
39
  self,
36
40
  func_num: int,
@@ -40,47 +44,13 @@ class CEC2017HybridFunction(BenchmarkFunction):
40
44
  shuffle: Optional[Tensor] = None,
41
45
  seed: Optional[int] = None,
42
46
  ):
43
- """
44
- Initialize CEC 2017 hybrid function.
45
-
46
- Args:
47
- func_num: Function number (11-20).
48
- n_var: Number of variables.
49
- rotation: Optional rotation matrix.
50
- shift: Optional shift vector.
51
- shuffle: Optional shuffle permutation.
52
- seed: Random seed for generating transforms if not provided.
53
- """
54
- super().__init__(n_var=n_var, xl=-100.0, xu=100.0)
55
-
56
- self.func_num = func_num
57
- self.bias = func_num * 100.0
58
-
59
- # Load or generate transforms
60
- if rotation is not None:
61
- self.rotation = rotation
62
- else:
63
- self.rotation = cec_data.get_rotation(func_num, n_var, seed=seed)
64
-
65
- if shift is not None:
66
- self.shift = shift
67
- else:
68
- self.shift = cec_data.get_shift(func_num, n_var, seed=seed)
69
-
47
+ super().__init__(func_num=func_num, n_var=n_var,
48
+ rotation=rotation, shift=shift, seed=seed)
70
49
  if shuffle is not None:
71
50
  self.shuffle = shuffle
72
51
  else:
73
52
  self.shuffle = cec_data.get_shuffle(func_num, n_var, seed=seed)
74
-
75
- def default_bounds(self) -> Tuple[float, float]:
76
- return (-100.0, 100.0)
77
-
78
- def _shift_rotate(self, x: Tensor) -> Tensor:
79
- """Apply shift and rotation transformation."""
80
- shift = self.shift.to(x.device, x.dtype)
81
- rotation = self.rotation.to(x.device, x.dtype)
82
- return cec_data.shift_rotate(x, shift, rotation)
83
-
53
+
84
54
  def _shuffle_and_partition(self, x: Tensor, partitions: List[float]) -> List[Tensor]:
85
55
  """Apply shuffle and partition."""
86
56
  shuffle = self.shuffle.to(x.device)
@@ -26,54 +26,17 @@ from torch import Tensor
26
26
  from ..base import BenchmarkFunction
27
27
  from . import basic
28
28
  from . import data as cec_data
29
+ from ._base import CEC2017Base
29
30
 
30
31
 
31
- class CEC2017Function(BenchmarkFunction):
32
- """Base class for CEC 2017 functions with shift and rotation support."""
33
-
34
- def __init__(
35
- self,
36
- func_num: int,
37
- n_var: int = 10,
38
- rotation: Optional[Tensor] = None,
39
- shift: Optional[Tensor] = None,
40
- seed: Optional[int] = None,
41
- ):
42
- """
43
- Initialize CEC 2017 function.
44
-
45
- Args:
46
- func_num: Function number (1-30).
47
- n_var: Number of variables (2, 10, 20, 30, 50, or 100 for official data).
48
- rotation: Optional rotation matrix. If None, loads from data or generates.
49
- shift: Optional shift vector. If None, loads from data or generates.
50
- seed: Random seed for generating transforms if not provided.
51
- """
52
- super().__init__(n_var=n_var, xl=-100.0, xu=100.0)
53
-
54
- self.func_num = func_num
55
- self.bias = func_num * 100.0 # F_i* = i * 100
56
-
57
- # Load or generate rotation matrix
58
- if rotation is not None:
59
- self.rotation = rotation
60
- else:
61
- self.rotation = cec_data.get_rotation(func_num, n_var, seed=seed)
62
-
63
- # Load or generate shift vector
64
- if shift is not None:
65
- self.shift = shift
66
- else:
67
- self.shift = cec_data.get_shift(func_num, n_var, seed=seed)
68
-
69
- def default_bounds(self) -> Tuple[float, float]:
70
- return (-100.0, 100.0)
71
-
72
- def _shift_rotate(self, x: Tensor) -> Tensor:
73
- """Apply shift and rotation transformation."""
74
- shift = self.shift.to(x.device, x.dtype)
75
- rotation = self.rotation.to(x.device, x.dtype)
76
- return cec_data.shift_rotate(x, shift, rotation)
32
+ class CEC2017Function(CEC2017Base):
33
+ """Base class for CEC 2017 simple functions (shift + rotation).
34
+
35
+ The shared __init__, default_bounds, and _shift_rotate live in
36
+ CEC2017Base (see _base.py).
37
+ """
38
+
39
+ pass
77
40
 
78
41
 
79
42
  class CEC2017_F1(CEC2017Function):
@@ -71,7 +71,7 @@ import torch
71
71
  import torch.nn as nn
72
72
  from torch import Tensor
73
73
 
74
- from evograd.operators.relaxations import binary_concrete, expand_param
74
+ from evograd.operators.relaxations import binary_concrete, expand_param, log_param
75
75
 
76
76
  __all__ = [
77
77
  "Crossover",
@@ -136,9 +136,7 @@ class Crossover(nn.Module, ABC):
136
136
 
137
137
  # Temperature parameter (log for positivity)
138
138
  if learn_temperature and adaptive:
139
- self._log_temperature = nn.Parameter(
140
- torch.tensor(temperature).log()
141
- )
139
+ self._log_temperature = log_param(temperature)
142
140
  else:
143
141
  self.register_buffer(
144
142
  "_log_temperature",
@@ -328,7 +326,7 @@ class SBXCrossover(Crossover):
328
326
 
329
327
  # Eta parameter (log for positivity)
330
328
  if learn_eta and adaptive:
331
- self._log_eta = nn.Parameter(torch.tensor(eta).log())
329
+ self._log_eta = log_param(eta)
332
330
  else:
333
331
  self.register_buffer("_log_eta", torch.tensor(eta).log())
334
332
 
@@ -67,7 +67,7 @@ import torch
67
67
  import torch.nn as nn
68
68
  from torch import Tensor
69
69
 
70
- from evograd.operators.relaxations import binary_concrete, expand_param
70
+ from evograd.operators.relaxations import binary_concrete, expand_param, standard_normal, log_param
71
71
 
72
72
  if TYPE_CHECKING:
73
73
  from evograd.core.problem import Problem
@@ -136,9 +136,7 @@ class Mutation(nn.Module, ABC):
136
136
 
137
137
  # Temperature parameter (log for positivity)
138
138
  if learn_temperature and adaptive:
139
- self._log_temperature = nn.Parameter(
140
- torch.tensor(temperature).log()
141
- )
139
+ self._log_temperature = log_param(temperature)
142
140
  else:
143
141
  self.register_buffer(
144
142
  "_log_temperature",
@@ -366,7 +364,7 @@ class PolynomialMutation(Mutation):
366
364
 
367
365
  # Eta parameter (log for positivity)
368
366
  if learn_eta and adaptive:
369
- self._log_eta = nn.Parameter(torch.tensor(eta).log())
367
+ self._log_eta = log_param(eta)
370
368
  else:
371
369
  self.register_buffer("_log_eta", torch.tensor(eta).log())
372
370
 
@@ -523,7 +521,7 @@ class GaussianMutation(Mutation):
523
521
  # Sigma parameter (log for positivity)
524
522
  sigma_val = sigma if sigma is not None else sigma_frac
525
523
  if learn_sigma and adaptive:
526
- self._log_sigma = nn.Parameter(torch.tensor(sigma_val).log())
524
+ self._log_sigma = log_param(sigma_val)
527
525
  else:
528
526
  self.register_buffer("_log_sigma", torch.tensor(sigma_val).log())
529
527
 
@@ -593,7 +591,7 @@ class GaussianMutation(Mutation):
593
591
  sigma_expanded = expand_param(None, self.sigma, n_pop, n_var, device, dtype)
594
592
 
595
593
  # Gaussian noise (reparameterised)
596
- noise = torch.randn(n_pop, n_var, device=device, dtype=dtype) * sigma_expanded
594
+ noise = standard_normal(n_pop, n_var, device=device, dtype=dtype) * sigma_expanded
597
595
 
598
596
  # Apply mutation with mask
599
597
  y = x + mask * noise
@@ -779,7 +777,7 @@ class NonUniformMutation(Mutation):
779
777
 
780
778
  # B parameter (log for positivity)
781
779
  if learn_b and adaptive:
782
- self._log_b = nn.Parameter(torch.tensor(b).log())
780
+ self._log_b = log_param(b)
783
781
  else:
784
782
  self.register_buffer("_log_b", torch.tensor(b).log())
785
783
 
@@ -13,12 +13,14 @@ Functions:
13
13
 
14
14
  import torch
15
15
  from torch import Tensor
16
- from typing import Union
16
+ from typing import Optional, Union
17
17
 
18
18
  __all__ = [
19
19
  "gumbel_softmax",
20
20
  "binary_concrete",
21
21
  "expand_param",
22
+ "standard_normal",
23
+ "log_param",
22
24
  ]
23
25
 
24
26
 
@@ -173,3 +175,30 @@ def expand_param(
173
175
  )
174
176
  else:
175
177
  raise ValueError(f"param must be 0D, 1D, or 2D, got {val.dim()}D")
178
+
179
+
180
+ def standard_normal(
181
+ *shape: int,
182
+ device: Optional[torch.device] = None,
183
+ dtype: Optional[torch.dtype] = None,
184
+ generator: Optional[torch.Generator] = None,
185
+ ) -> Tensor:
186
+ """Draw standard-normal (pathwise / reparameterization) noise.
187
+
188
+ Single source for the ``torch.randn(..., device=, dtype=)`` reparameterization
189
+ draw, reused by CMA-ES sampling, Gaussian mutation, and the samplers.
190
+ """
191
+ return torch.randn(*shape, device=device, dtype=dtype, generator=generator)
192
+
193
+
194
+ def log_param(
195
+ value: Union[float, Tensor],
196
+ device: Optional[torch.device] = None,
197
+ dtype: Optional[torch.dtype] = None,
198
+ ) -> "torch.nn.Parameter":
199
+ """Learnable positive hyperparameter stored on the log scale.
200
+
201
+ Single source for the repeated ``nn.Parameter(torch.tensor(v).log())`` idiom
202
+ used for sigma, F, eta, temperature, etc. Recover the value via ``.exp()``.
203
+ """
204
+ return torch.nn.Parameter(torch.tensor(value, device=device, dtype=dtype).log())
@@ -50,9 +50,19 @@ __all__ = [
50
50
  "BoundsRepair",
51
51
  "RepairMethod",
52
52
  "NoRepair",
53
+ "clamp_to_bounds",
53
54
  ]
54
55
 
55
56
 
57
+ def clamp_to_bounds(x: Tensor, xl: Tensor, xu: Tensor) -> Tensor:
58
+ """Clamp ``x`` element-wise to the box ``[xl, xu]``.
59
+
60
+ Single source of truth for box-bounds clamping: reused by ClipRepair and by
61
+ the algorithms (PSO/DE) instead of repeating ``torch.clamp`` inline.
62
+ """
63
+ return torch.clamp(x, min=xl, max=xu)
64
+
65
+
56
66
  class RepairMethod(Enum):
57
67
  """Available repair methods."""
58
68
  CLIP = "clip"
@@ -189,7 +199,7 @@ class ClipRepair(Repair):
189
199
  xl: Tensor,
190
200
  xu: Tensor,
191
201
  ) -> Tensor:
192
- return torch.clamp(x, min=xl, max=xu)
202
+ return clamp_to_bounds(x, xl, xu)
193
203
 
194
204
  def __repr__(self) -> str:
195
205
  return "ClipRepair()"
@@ -32,6 +32,8 @@ import torch
32
32
  import torch.nn as nn
33
33
  from torch import Tensor
34
34
 
35
+ from evograd.operators.relaxations import standard_normal
36
+
35
37
  if TYPE_CHECKING:
36
38
  from evograd.core.problem import Problem
37
39
 
@@ -335,16 +337,12 @@ class NormalSampling(Sampling):
335
337
  if generator is not None:
336
338
  # Generator may be on CPU for MPS devices
337
339
  gen_device = generator.device
338
- z = torch.randn(
339
- n_samples, n_var,
340
- device=gen_device,
341
- dtype=dtype,
342
- generator=generator,
343
- )
340
+ z = standard_normal(n_samples, n_var, device=gen_device, dtype=dtype,
341
+ generator=generator)
344
342
  if gen_device != device:
345
343
  z = z.to(device)
346
344
  else:
347
- z = torch.randn(n_samples, n_var, device=device, dtype=dtype)
345
+ z = standard_normal(n_samples, n_var, device=device, dtype=dtype)
348
346
 
349
347
  # Transform to [0, 1] centered at 0.5 with scaled std
350
348
  # mean=0.5, std=sigma_factor * 0.5
@@ -44,7 +44,7 @@ import torch
44
44
  import torch.nn as nn
45
45
  from torch import Tensor
46
46
 
47
- from evograd.operators.relaxations import gumbel_softmax
47
+ from evograd.operators.relaxations import gumbel_softmax, log_param
48
48
 
49
49
  __all__ = [
50
50
  "Selection",
@@ -94,9 +94,7 @@ class Selection(nn.Module, ABC):
94
94
  # Temperature parameter
95
95
  if learn_temperature and adaptive:
96
96
  # Store as log for positivity
97
- self._log_temperature = nn.Parameter(
98
- torch.tensor(temperature).log()
99
- )
97
+ self._log_temperature = log_param(temperature)
100
98
  else:
101
99
  self.register_buffer(
102
100
  "_log_temperature",
@@ -18,6 +18,9 @@ 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
19
  from tests.test_cmaes import run_all_tests as test_cmaes
20
20
  from tests.test_ga import run_all_tests as test_ga
21
+ from tests.test_de import run_all_tests as test_de
22
+ from tests.test_pso import run_all_tests as test_pso
23
+ from tests.test_shade import run_all_tests as test_shade
21
24
  from tests.test_per_individual import main as test_per_individual
22
25
 
23
26
 
@@ -61,6 +64,24 @@ def run_all():
61
64
  print("▶"*60)
62
65
  results['ga'] = test_ga()
63
66
 
67
+ # Run DE tests
68
+ print("\n\n" + "▶"*60)
69
+ print("▶ RUNNING DE TESTS")
70
+ print("▶"*60)
71
+ results['de'] = test_de()
72
+
73
+ # Run PSO tests
74
+ print("\n\n" + "▶"*60)
75
+ print("▶ RUNNING PSO TESTS")
76
+ print("▶"*60)
77
+ results['pso'] = test_pso()
78
+
79
+ # Run SHADE tests
80
+ print("\n\n" + "▶"*60)
81
+ print("▶ RUNNING SHADE TESTS")
82
+ print("▶"*60)
83
+ results['shade'] = test_shade()
84
+
64
85
  # Run per-individual parameter tests
65
86
  print("\n\n" + "▶"*60)
66
87
  print("▶ RUNNING PER-INDIVIDUAL TESTS")
@@ -0,0 +1,62 @@
1
+ """Behavioural tests for Differential Evolution — coverage added before the refactor."""
2
+ import math
3
+ import sys
4
+
5
+ import torch
6
+
7
+ from evograd.core.problem import Problem
8
+ from evograd.core.minimize import minimize
9
+ from evograd.core.termination import MaxEvaluations
10
+ from evograd.algorithms.de import DE
11
+
12
+
13
+ def sphere(x):
14
+ return (x ** 2).sum(dim=-1)
15
+
16
+
17
+ def P(n=10, lo=-5.0, hi=5.0):
18
+ return Problem(objective=sphere, n_var=n, xl=lo, xu=hi)
19
+
20
+
21
+ def test_de_converges():
22
+ print("\n1. DE converges on sphere...")
23
+ r = minimize(P(10), DE(pop_size=30), termination=MaxEvaluations(8000), seed=0, verbose=False)
24
+ assert r.best_fitness < 1.0, f"DE best {r.best_fitness} !< 1.0"
25
+
26
+
27
+ def test_de_variants():
28
+ print("2. DE variants run, finite fitness...")
29
+ for v in ("DE/rand/1/bin", "DE/best/1/bin", "DE/current-to-best/1/bin", "DE/rand/2/bin"):
30
+ r = minimize(P(8), DE(pop_size=20, variant=v), termination=MaxEvaluations(1500), seed=1, verbose=False)
31
+ assert math.isfinite(r.best_fitness), f"DE {v} non-finite fitness"
32
+
33
+
34
+ def test_de_four_modes():
35
+ print("3. DE four modes run, finite fitness...")
36
+ for kw in ({}, dict(adaptive=True), dict(differentiable=True), dict(adaptive=True, differentiable=True)):
37
+ r = minimize(P(8), DE(pop_size=20, **kw), termination=MaxEvaluations(800), seed=2, verbose=False)
38
+ assert math.isfinite(r.best_fitness), f"DE {kw} non-finite fitness"
39
+
40
+
41
+ def test_de_respects_bounds():
42
+ print("4. DE respects bounds...")
43
+ r = minimize(P(6, -2.0, 2.0), DE(pop_size=20), termination=MaxEvaluations(1500), seed=3, verbose=False)
44
+ x = r.best_solution
45
+ assert (x >= -2.0 - 1e-3).all() and (x <= 2.0 + 1e-3).all(), "DE violated bounds"
46
+
47
+
48
+ def run_all_tests():
49
+ ok = True
50
+ for t in (test_de_converges, test_de_variants, test_de_four_modes, test_de_respects_bounds):
51
+ try:
52
+ t()
53
+ print(" PASS")
54
+ except Exception:
55
+ ok = False
56
+ import traceback
57
+ traceback.print_exc()
58
+ return ok
59
+
60
+
61
+ if __name__ == "__main__":
62
+ sys.exit(0 if run_all_tests() else 1)
@@ -0,0 +1,62 @@
1
+ """Behavioural tests for Particle Swarm Optimisation — coverage added before the refactor."""
2
+ import math
3
+ import sys
4
+
5
+ import torch
6
+
7
+ from evograd.core.problem import Problem
8
+ from evograd.core.minimize import minimize
9
+ from evograd.core.termination import MaxEvaluations
10
+ from evograd.algorithms.pso import PSO
11
+
12
+
13
+ def sphere(x):
14
+ return (x ** 2).sum(dim=-1)
15
+
16
+
17
+ def P(n=10, lo=-5.0, hi=5.0):
18
+ return Problem(objective=sphere, n_var=n, xl=lo, xu=hi)
19
+
20
+
21
+ def test_pso_converges():
22
+ print("\n1. PSO converges on sphere...")
23
+ r = minimize(P(10), PSO(pop_size=30), termination=MaxEvaluations(8000), seed=0, verbose=False)
24
+ assert r.best_fitness < 1.0, f"PSO best {r.best_fitness} !< 1.0"
25
+
26
+
27
+ def test_pso_four_modes():
28
+ print("2. PSO four modes run, finite fitness...")
29
+ for kw in ({}, dict(adaptive=True), dict(differentiable=True), dict(adaptive=True, differentiable=True)):
30
+ r = minimize(P(8), PSO(pop_size=20, **kw), termination=MaxEvaluations(800), seed=1, verbose=False)
31
+ assert math.isfinite(r.best_fitness), f"PSO {kw} non-finite fitness"
32
+
33
+
34
+ def test_pso_per_particle_coeffs():
35
+ print("3. PSO per-particle adaptive coeffs run...")
36
+ r = minimize(P(8), PSO(pop_size=20, adaptive=True, per_particle_coeffs=True),
37
+ termination=MaxEvaluations(800), seed=2, verbose=False)
38
+ assert math.isfinite(r.best_fitness), "PSO per-particle non-finite fitness"
39
+
40
+
41
+ def test_pso_respects_bounds():
42
+ print("4. PSO respects bounds...")
43
+ r = minimize(P(6, -2.0, 2.0), PSO(pop_size=20), termination=MaxEvaluations(1500), seed=3, verbose=False)
44
+ x = r.best_solution
45
+ assert (x >= -2.0 - 1e-3).all() and (x <= 2.0 + 1e-3).all(), "PSO violated bounds"
46
+
47
+
48
+ def run_all_tests():
49
+ ok = True
50
+ for t in (test_pso_converges, test_pso_four_modes, test_pso_per_particle_coeffs, test_pso_respects_bounds):
51
+ try:
52
+ t()
53
+ print(" PASS")
54
+ except Exception:
55
+ ok = False
56
+ import traceback
57
+ traceback.print_exc()
58
+ return ok
59
+
60
+
61
+ if __name__ == "__main__":
62
+ sys.exit(0 if run_all_tests() else 1)
@@ -0,0 +1,64 @@
1
+ """Behavioural tests for SHADE (and LSHADE) — coverage added before the refactor."""
2
+ import math
3
+ import sys
4
+
5
+ import torch
6
+
7
+ from evograd.core.problem import Problem
8
+ from evograd.core.minimize import minimize
9
+ from evograd.core.termination import MaxEvaluations
10
+ from evograd.algorithms.shade import SHADE, LSHADE
11
+
12
+
13
+ def sphere(x):
14
+ return (x ** 2).sum(dim=-1)
15
+
16
+
17
+ def P(n=10, lo=-5.0, hi=5.0):
18
+ return Problem(objective=sphere, n_var=n, xl=lo, xu=hi)
19
+
20
+
21
+ def test_shade_converges():
22
+ print("\n1. SHADE converges on sphere...")
23
+ r = minimize(P(10), SHADE(pop_size=30), termination=MaxEvaluations(3000), seed=0, verbose=False)
24
+ assert r.best_fitness < 1.0, f"SHADE best {r.best_fitness} !< 1.0"
25
+
26
+
27
+ def test_shade_four_modes():
28
+ print("2. SHADE four modes run, finite fitness...")
29
+ for kw in ({}, dict(adaptive=True), dict(differentiable=True), dict(adaptive=True, differentiable=True)):
30
+ r = minimize(P(8), SHADE(pop_size=20, **kw), termination=MaxEvaluations(800), seed=1, verbose=False)
31
+ assert math.isfinite(r.best_fitness), f"SHADE {kw} non-finite fitness"
32
+
33
+
34
+ def test_shade_respects_bounds():
35
+ print("3. SHADE respects bounds...")
36
+ r = minimize(P(6, -2.0, 2.0), SHADE(pop_size=20), termination=MaxEvaluations(1200), seed=2, verbose=False)
37
+ x = r.best_solution
38
+ assert (x >= -2.0 - 1e-3).all() and (x <= 2.0 + 1e-3).all(), "SHADE violated bounds"
39
+
40
+
41
+ def test_lshade_runs():
42
+ print("4. LSHADE runs, finite fitness, respects bounds (fixed)...")
43
+ r = minimize(P(10), LSHADE(pop_size_init=40, pop_size_min=4),
44
+ termination=MaxEvaluations(2000), seed=3, verbose=False)
45
+ assert math.isfinite(r.best_fitness), "LSHADE non-finite fitness"
46
+ x = r.best_solution
47
+ assert (x >= -5.0 - 1e-3).all() and (x <= 5.0 + 1e-3).all(), "LSHADE violated bounds"
48
+
49
+
50
+ def run_all_tests():
51
+ ok = True
52
+ for t in (test_shade_converges, test_shade_four_modes, test_shade_respects_bounds, test_lshade_runs):
53
+ try:
54
+ t()
55
+ print(" PASS")
56
+ except Exception:
57
+ ok = False
58
+ import traceback
59
+ traceback.print_exc()
60
+ return ok
61
+
62
+
63
+ if __name__ == "__main__":
64
+ sys.exit(0 if run_all_tests() else 1)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "evograd-diff"
7
- version = "0.2.0"
7
+ version = "0.2.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" }
File without changes
File without changes
File without changes