evograd-diff 0.1.0__py3-none-any.whl

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 (50) hide show
  1. evograd/__init__.py +67 -0
  2. evograd/algorithms/__init__.py +138 -0
  3. evograd/algorithms/cmaes.py +1365 -0
  4. evograd/algorithms/de.py +895 -0
  5. evograd/algorithms/ga.py +532 -0
  6. evograd/algorithms/pso.py +648 -0
  7. evograd/algorithms/shade.py +1165 -0
  8. evograd/benchmarks/functions/__init__.py +229 -0
  9. evograd/benchmarks/functions/base.py +217 -0
  10. evograd/benchmarks/functions/cec2017/__init__.py +250 -0
  11. evograd/benchmarks/functions/cec2017/basic.py +413 -0
  12. evograd/benchmarks/functions/cec2017/composition.py +580 -0
  13. evograd/benchmarks/functions/cec2017/data.pkl +0 -0
  14. evograd/benchmarks/functions/cec2017/data.py +350 -0
  15. evograd/benchmarks/functions/cec2017/hybrid.py +406 -0
  16. evograd/benchmarks/functions/cec2017/simple.py +326 -0
  17. evograd/benchmarks/functions/classical.py +649 -0
  18. evograd/benchmarks/functions/smoothed_funnel.py +476 -0
  19. evograd/benchmarks/functions/transforms.py +463 -0
  20. evograd/benchmarks/run_benchmark_functions.py +1208 -0
  21. evograd/core/__init__.py +73 -0
  22. evograd/core/algorithm.py +778 -0
  23. evograd/core/maximize.py +269 -0
  24. evograd/core/minimize.py +740 -0
  25. evograd/core/problem.py +444 -0
  26. evograd/core/result.py +571 -0
  27. evograd/core/termination.py +602 -0
  28. evograd/operators/__init__.py +178 -0
  29. evograd/operators/crossover.py +1117 -0
  30. evograd/operators/mutation.py +1098 -0
  31. evograd/operators/relaxations.py +175 -0
  32. evograd/operators/repair.py +601 -0
  33. evograd/operators/sampling.py +577 -0
  34. evograd/operators/selection.py +981 -0
  35. evograd/operators/survival.py +1000 -0
  36. evograd/tests/__init__.py +11 -0
  37. evograd/tests/run_all.py +78 -0
  38. evograd/tests/test_core.py +528 -0
  39. evograd/tests/test_ga.py +572 -0
  40. evograd/tests/test_operators.py +662 -0
  41. evograd/tests/test_per_individual.py +326 -0
  42. evograd/tests/test_utils.py +328 -0
  43. evograd/utils/__init__.py +97 -0
  44. evograd/utils/callbacks.py +926 -0
  45. evograd/utils/device.py +502 -0
  46. evograd/utils/duplicates.py +421 -0
  47. evograd_diff-0.1.0.dist-info/METADATA +439 -0
  48. evograd_diff-0.1.0.dist-info/RECORD +50 -0
  49. evograd_diff-0.1.0.dist-info/WHEEL +4 -0
  50. evograd_diff-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,532 @@
1
+ """
2
+ Genetic Algorithm (GA) implementation for EvoGrad.
3
+
4
+ This module provides a fully differentiable Genetic Algorithm that
5
+ supports both classical and gradient-enabled optimisation modes.
6
+
7
+ The GA follows the standard evolutionary cycle:
8
+ 1. Selection: Choose parents based on fitness
9
+ 2. Crossover: Recombine parents to create offspring
10
+ 3. Mutation: Introduce random variation
11
+ 4. Survival: Select individuals for next generation
12
+
13
+ All operators are pluggable via dependency injection (pymoo-style),
14
+ allowing flexible customisation of the algorithm behavior.
15
+
16
+ Differentiable Mode:
17
+ When `differentiable=True`, the population is stored as nn.Parameter,
18
+ enabling gradient flow through the entire evolutionary cycle.
19
+
20
+ When operators have `adaptive=True`, their internal parameters
21
+ (temperature, eta, prob, etc.) are also learnable nn.Parameters.
22
+
23
+ Example:
24
+ >>> from evograd.algorithms import GA
25
+ >>> from evograd.core import Problem, minimize
26
+ >>> from evograd.operators import (
27
+ ... FloatRandomSampling,
28
+ ... TournamentSelection,
29
+ ... SBXCrossover,
30
+ ... PolynomialMutation,
31
+ ... MergeSurvival,
32
+ ... )
33
+ >>>
34
+ >>> # Define problem
35
+ >>> problem = Problem(
36
+ ... objective=lambda x: (x**2).sum(dim=-1),
37
+ ... n_var=30,
38
+ ... xl=-100.0,
39
+ ... xu=100.0,
40
+ ... )
41
+ >>>
42
+ >>> # Create GA with operators
43
+ >>> ga = GA(
44
+ ... pop_size=100,
45
+ ... sampling=FloatRandomSampling(),
46
+ ... selection=TournamentSelection(tournament_size=3),
47
+ ... crossover=SBXCrossover(eta=15, prob=0.9),
48
+ ... mutation=PolynomialMutation(eta=20),
49
+ ... survival=MergeSurvival(elitism=True, n_elite=1),
50
+ ... differentiable=True,
51
+ ... )
52
+ >>>
53
+ >>> # Run optimization (pymoo-style)
54
+ >>> result = minimize(problem, ga, max_evals=10000, seed=42)
55
+ >>> print(f"Best fitness: {result.best_fitness}")
56
+
57
+ Reference:
58
+ Holland, J. H. (1992). Genetic Algorithms. Scientific American.
59
+ Goldberg, D. E. (1989). Genetic Algorithms in Search, Optimization,
60
+ and Machine Learning.
61
+ """
62
+
63
+ from __future__ import annotations
64
+
65
+ from typing import TYPE_CHECKING, Any, Dict, Optional, Union
66
+
67
+ import torch
68
+ import torch.nn as nn
69
+ from torch import Tensor
70
+
71
+ from evograd.core.algorithm import Algorithm
72
+
73
+ if TYPE_CHECKING:
74
+ from evograd.core.problem import Problem
75
+
76
+ __all__ = ["GA", "ga_default", "ga_steady_state", "ga_comma"]
77
+
78
+
79
+ class GA(Algorithm):
80
+ """
81
+ Genetic Algorithm (GA) for continuous optimisation.
82
+
83
+ A generational evolutionary algorithm that evolves a population
84
+ of candidate solutions through selection, crossover, mutation,
85
+ and survival selection. Supports both classical and differentiable
86
+ operation modes.
87
+
88
+ All operators are injected via constructor (pymoo-style), enabling
89
+ flexible algorithm configuration.
90
+
91
+ Args:
92
+ pop_size: Population size.
93
+ sampling: Operator for initial population generation.
94
+ Default: FloatRandomSampling()
95
+ selection: Parent selection operator.
96
+ Default: TournamentSelection(tournament_size=3)
97
+ crossover: Crossover/recombination operator.
98
+ Default: SBXCrossover(eta=15, prob=0.9)
99
+ mutation: Mutation operator.
100
+ Default: PolynomialMutation(eta=20)
101
+ survival: Survival selection operator.
102
+ Default: MergeSurvival(elitism=True, n_elite=1)
103
+ repair: Repair operator for constraint handling.
104
+ n_offsprings: Number of offspring per generation.
105
+ Default: pop_size (generational GA).
106
+ eliminate_duplicates: Duplicate handling strategy.
107
+ differentiable: Enable gradient flow through population.
108
+ adaptive: Enable learnable operator parameters.
109
+ dtype: Tensor dtype (default: torch.float32).
110
+
111
+ Attributes:
112
+ pop_size: Population size.
113
+ n_offsprings: Number of offspring per generation.
114
+ pop: Current population (property).
115
+ fitness: Current fitness values (property).
116
+ best_fitness: Best fitness found so far.
117
+ best_solution: Best solution found so far.
118
+ generation: Current generation number.
119
+ n_evals: Total fitness evaluations.
120
+
121
+ Example:
122
+ >>> # Minimal setup with defaults
123
+ >>> ga = GA(pop_size=50)
124
+ >>> ga.initialize(problem)
125
+ >>>
126
+ >>> # With custom operators
127
+ >>> from evograd.operators import (
128
+ ... RouletteSelection, BlendCrossover,
129
+ ... GaussianMutation, CommaSurvival,
130
+ ... )
131
+ >>> ga = GA(
132
+ ... pop_size=100,
133
+ ... selection=RouletteSelection(),
134
+ ... crossover=BlendCrossover(alpha=0.5),
135
+ ... mutation=GaussianMutation(sigma=0.1),
136
+ ... survival=CommaSurvival(elitism=True, n_elite=2),
137
+ ... n_offsprings=200, # Required: >= pop_size for comma
138
+ ... )
139
+ """
140
+
141
+ def __init__(
142
+ self,
143
+ pop_size: int = 100,
144
+ sampling: Optional[nn.Module] = None,
145
+ selection: Optional[nn.Module] = None,
146
+ crossover: Optional[nn.Module] = None,
147
+ mutation: Optional[nn.Module] = None,
148
+ survival: Optional[nn.Module] = None,
149
+ repair: Optional[nn.Module] = None,
150
+ n_offsprings: Optional[int] = None,
151
+ eliminate_duplicates: bool = True,
152
+ differentiable: bool = True,
153
+ adaptive: bool = True,
154
+ dtype: torch.dtype = torch.float32,
155
+ ) -> None:
156
+ # Create default operators if not provided
157
+ if selection is None:
158
+ from evograd.operators.selection import TournamentSelection
159
+ selection = TournamentSelection(
160
+ tournament_size=3,
161
+ adaptive=adaptive,
162
+ )
163
+
164
+ if crossover is None:
165
+ from evograd.operators.crossover import SBXCrossover
166
+ crossover = SBXCrossover(
167
+ eta=15,
168
+ prob=0.9,
169
+ adaptive=adaptive,
170
+ )
171
+
172
+ if mutation is None:
173
+ from evograd.operators.mutation import PolynomialMutation
174
+ mutation = PolynomialMutation(
175
+ eta=20,
176
+ prob=None, # Default: 1/n_var
177
+ adaptive=adaptive,
178
+ )
179
+
180
+ if survival is None:
181
+ from evograd.operators.survival import MergeSurvival
182
+ survival = MergeSurvival(
183
+ n_survive=pop_size,
184
+ elitism=True,
185
+ n_elite=1,
186
+ adaptive=adaptive,
187
+ )
188
+
189
+ is_adaptive = selection.adaptive or crossover.adaptive or mutation.adaptive
190
+
191
+ if not is_adaptive and survival.adaptive:
192
+ adaptive = False
193
+ survival.adaptive = False
194
+ for p in survival.parameters():
195
+ p.requires_grad = False
196
+
197
+
198
+ # Call super().__init__() first before any attribute assignments
199
+ super().__init__(
200
+ pop_size=pop_size,
201
+ sampling=sampling,
202
+ selection=selection,
203
+ crossover=crossover,
204
+ mutation=mutation,
205
+ survival=survival,
206
+ repair=repair,
207
+ eliminate_duplicates=eliminate_duplicates,
208
+ n_offsprings=n_offsprings,
209
+ differentiable=differentiable,
210
+ adaptive=adaptive,
211
+ dtype=dtype,
212
+ )
213
+
214
+ # =========================================================================
215
+ # Properties
216
+ # =========================================================================
217
+
218
+ @property
219
+ def population(self) -> Tensor:
220
+ """Current population."""
221
+ return self._population
222
+
223
+ @property
224
+ def fitness(self) -> Tensor:
225
+ """Current fitness values."""
226
+ return self.state.fitness
227
+
228
+ # =========================================================================
229
+ # Core GA Methods
230
+ # =========================================================================
231
+
232
+ def _setup(self) -> None:
233
+ """
234
+ GA-specific setup after initialization.
235
+
236
+ Called by initialize() after population is created.
237
+ """
238
+ # Update survival operator's n_survive if not set
239
+ if hasattr(self.survival, 'n_survive'):
240
+ if self.survival.n_survive is None:
241
+ self.survival.n_survive = self.pop_size
242
+
243
+ def _infill(self) -> Tensor:
244
+ """
245
+ Generate offspring through selection, crossover, and mutation.
246
+
247
+ This implements the main GA variation operators:
248
+ 1. Select parents based on fitness
249
+ 2. Apply crossover to create offspring
250
+ 3. Apply mutation to introduce variation
251
+
252
+ Returns:
253
+ Offspring population tensor [n_offsprings, n_var].
254
+ """
255
+ n_offspring = self.n_offsprings
256
+
257
+ # Number of parent pairs needed
258
+ n_pairs = n_offspring
259
+
260
+ # 1. SELECTION: Choose parents
261
+ # Select 2 * n_pairs parents (to form pairs)
262
+ parents = self.selection(
263
+ self.population,
264
+ self.fitness,
265
+ n_select=2 * n_pairs,
266
+ )
267
+
268
+ # Split into parent pairs
269
+ parent1 = parents[:n_pairs]
270
+ parent2 = parents[n_pairs:2*n_pairs]
271
+
272
+ # 2. CROSSOVER: Recombine parents
273
+ offspring = self.crossover(parent1, parent2)
274
+
275
+ # 3. MUTATION: Introduce variation
276
+ offspring = self.mutation(offspring, self.xl, self.xu)
277
+
278
+ return offspring
279
+
280
+ def _advance(self, offspring: Tensor, offspring_fitness: Tensor) -> None:
281
+ """
282
+ Update population using survival selection.
283
+
284
+ Delegates to the survival operator to select which individuals
285
+ survive to the next generation.
286
+
287
+ Args:
288
+ offspring: Offspring population tensor.
289
+ offspring_fitness: Fitness values of offspring.
290
+ """
291
+ # Use survival operator
292
+ survivors, survivor_fitness = self.survival(
293
+ self.population,
294
+ self.fitness,
295
+ offspring,
296
+ offspring_fitness,
297
+ n_survive=self.pop_size,
298
+ )
299
+
300
+ # Update population
301
+ self._update_population(survivors, survivor_fitness)
302
+
303
+ # Update best solution tracking
304
+ self.state.update_best(self.population, self.state.fitness)
305
+
306
+ def _update_population(self, new_population: Tensor, new_fit: Tensor) -> None:
307
+ """
308
+ Update population and fitness tensors.
309
+
310
+ Args:
311
+ new_pop: New population tensor.
312
+ new_fit: New fitness tensor.
313
+ """
314
+ with torch.no_grad():
315
+ self._population.copy_(new_population)
316
+ self.state.fitness = new_fit
317
+ self.state.population = self._population
318
+
319
+ # =========================================================================
320
+ # Hyperparameter Access
321
+ # =========================================================================
322
+
323
+ def _get_hyperparams(self) -> Dict[str, Any]:
324
+ """
325
+ Return current hyperparameter values.
326
+
327
+ Collects hyperparameters from all operators for logging.
328
+
329
+ Returns:
330
+ Dictionary of hyperparameter names to values.
331
+ """
332
+ params = {
333
+ 'pop_size': self.pop_size,
334
+ 'n_offsprings': self.n_offsprings,
335
+ }
336
+
337
+ # Add selection parameters
338
+ if hasattr(self.selection, 'temperature'):
339
+ params['selection_temperature'] = self.selection.temperature.item()
340
+ if hasattr(self.selection, 'tournament_size'):
341
+ params['tournament_size'] = self.selection.tournament_size
342
+
343
+ # Add crossover parameters
344
+ if hasattr(self.crossover, 'eta'):
345
+ params['crossover_eta'] = self.crossover.eta.item()
346
+ if hasattr(self.crossover, 'prob'):
347
+ prob = self.crossover.prob
348
+ if isinstance(prob, Tensor):
349
+ params['crossover_prob'] = prob.mean().item()
350
+ else:
351
+ params['crossover_prob'] = prob
352
+ if hasattr(self.crossover, 'temperature'):
353
+ params['crossover_temperature'] = self.crossover.temperature.item()
354
+
355
+ # Add mutation parameters
356
+ if hasattr(self.mutation, 'eta'):
357
+ params['mutation_eta'] = self.mutation.eta.item()
358
+ if hasattr(self.mutation, 'prob'):
359
+ prob = self.mutation.prob
360
+ if prob is not None:
361
+ if isinstance(prob, Tensor):
362
+ params['mutation_prob'] = prob.mean().item()
363
+ else:
364
+ params['mutation_prob'] = prob
365
+ if hasattr(self.mutation, 'temperature'):
366
+ params['mutation_temperature'] = self.mutation.temperature.item()
367
+
368
+ # Add survival parameters
369
+ if hasattr(self.survival, 'elitism'):
370
+ params['elitism'] = self.survival.elitism
371
+ if hasattr(self.survival, 'n_elite'):
372
+ params['n_elite'] = self.survival.n_elite
373
+ if hasattr(self.survival, 'temperature'):
374
+ params['survival_temperature'] = self.survival.temperature.item()
375
+
376
+ return params
377
+
378
+ # =========================================================================
379
+ # String Representation
380
+ # =========================================================================
381
+
382
+ def __repr__(self) -> str:
383
+ survival_name = type(self.survival).__name__
384
+ return (
385
+ f"GA(pop_size={self.pop_size}, "
386
+ f"n_offsprings={self.n_offsprings}, "
387
+ f"survival={survival_name}, "
388
+ f"differentiable={self.differentiable})"
389
+ )
390
+
391
+
392
+ # =============================================================================
393
+ # Convenience Factory Functions
394
+ # =============================================================================
395
+
396
+ def ga_default(
397
+ pop_size: int = 100,
398
+ differentiable: bool = True,
399
+ adaptive: bool = True,
400
+ **kwargs,
401
+ ) -> GA:
402
+ """
403
+ Create a GA with sensible default operators.
404
+
405
+ Uses:
406
+ - TournamentSelection (k=3)
407
+ - SBXCrossover (eta=15, prob=0.9)
408
+ - PolynomialMutation (eta=20)
409
+ - MergeSurvival with elitism (n_elite=1)
410
+
411
+ Args:
412
+ pop_size: Population size.
413
+ differentiable: Enable gradient flow.
414
+ adaptive: Enable learnable operator parameters.
415
+ **kwargs: Additional arguments passed to GA.
416
+
417
+ Returns:
418
+ Configured GA instance.
419
+
420
+ Example:
421
+ >>> ga = ga_default(pop_size=50)
422
+ >>> result = minimize(problem, ga)
423
+ """
424
+ from evograd.operators.survival import MergeSurvival
425
+
426
+ return GA(
427
+ pop_size=pop_size,
428
+ survival=MergeSurvival(
429
+ n_survive=pop_size,
430
+ elitism=True,
431
+ n_elite=1,
432
+ adaptive=adaptive,
433
+ ),
434
+ differentiable=differentiable,
435
+ adaptive=adaptive,
436
+ **kwargs,
437
+ )
438
+
439
+
440
+ def ga_steady_state(
441
+ pop_size: int = 100,
442
+ n_offsprings: int = 2,
443
+ differentiable: bool = True,
444
+ adaptive: bool = True,
445
+ **kwargs,
446
+ ) -> GA:
447
+ """
448
+ Create a steady-state GA.
449
+
450
+ In steady-state GA, only a few offspring are created per
451
+ generation and they replace the worst individuals.
452
+
453
+ Args:
454
+ pop_size: Population size.
455
+ n_offsprings: Number of offspring per generation.
456
+ differentiable: Enable gradient flow.
457
+ adaptive: Enable learnable operator parameters.
458
+ **kwargs: Additional arguments passed to GA.
459
+
460
+ Returns:
461
+ Configured GA instance.
462
+
463
+ Example:
464
+ >>> ga = ga_steady_state(pop_size=50, n_offsprings=2)
465
+ >>> result = minimize(problem, ga)
466
+ """
467
+ from evograd.operators.survival import ReplaceWorstSurvival
468
+
469
+ return GA(
470
+ pop_size=pop_size,
471
+ n_offsprings=n_offsprings,
472
+ survival=ReplaceWorstSurvival(
473
+ n_survive=pop_size,
474
+ elitism=True,
475
+ n_elite=1,
476
+ adaptive=adaptive,
477
+ ),
478
+ differentiable=differentiable,
479
+ adaptive=adaptive,
480
+ **kwargs,
481
+ )
482
+
483
+
484
+ def ga_comma(
485
+ pop_size: int = 50,
486
+ n_offsprings: int = 100,
487
+ differentiable: bool = True,
488
+ adaptive: bool = True,
489
+ **kwargs,
490
+ ) -> GA:
491
+ """
492
+ Create a (μ, λ) style GA.
493
+
494
+ In (μ, λ) selection, parents are discarded and the next
495
+ generation is selected only from offspring. This can help
496
+ escape local optima but requires n_offsprings >= pop_size.
497
+
498
+ Args:
499
+ pop_size: Population size (μ).
500
+ n_offsprings: Number of offspring (λ).
501
+ differentiable: Enable gradient flow.
502
+ adaptive: Enable learnable operator parameters.
503
+ **kwargs: Additional arguments passed to GA.
504
+
505
+ Returns:
506
+ Configured GA instance.
507
+
508
+ Example:
509
+ >>> ga = ga_comma(pop_size=50, n_offsprings=100)
510
+ >>> result = minimize(problem, ga)
511
+ """
512
+ from evograd.operators.survival import CommaSurvival
513
+
514
+ if n_offsprings < pop_size:
515
+ raise ValueError(
516
+ f"For (μ,λ) GA, n_offsprings ({n_offsprings}) must be >= "
517
+ f"pop_size ({pop_size})"
518
+ )
519
+
520
+ return GA(
521
+ pop_size=pop_size,
522
+ n_offsprings=n_offsprings,
523
+ survival=CommaSurvival(
524
+ n_survive=pop_size,
525
+ elitism=True, # Still keep elitism to preserve best
526
+ n_elite=1,
527
+ adaptive=adaptive,
528
+ ),
529
+ differentiable=differentiable,
530
+ adaptive=adaptive,
531
+ **kwargs,
532
+ )