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,601 @@
1
+ """
2
+ Repair operators for bounds handling.
3
+
4
+ This module provides repair operators that handle constraint
5
+ violations, primarily keeping solutions within variable bounds.
6
+ All methods are differentiable-friendly.
7
+
8
+ Available repair methods:
9
+ - ClipRepair: Clamp values to bounds (simplest)
10
+ - ReflectRepair: Bounce off boundaries (preserves momentum)
11
+ - WrapRepair: Periodic/toroidal wrapping
12
+ - RandomRepair: Reset violating genes randomly
13
+ - BoundsRepair: Configurable repair with method selection
14
+
15
+ Differentiable Considerations:
16
+ - ClipRepair: Gradient is zero at boundaries (can cause issues)
17
+ - ReflectRepair: Gradient flows through reflection
18
+ - WrapRepair: Gradient flows through modulo (discontinuous)
19
+ - For differentiable mode, ReflectRepair is generally recommended
20
+
21
+ Example:
22
+ >>> from evograd.operators import BoundsRepair
23
+ >>>
24
+ >>> # Using method string
25
+ >>> repair = BoundsRepair(method='reflect')
26
+ >>> repaired = repair(population, xl, xu)
27
+ >>>
28
+ >>> # Or use specific class
29
+ >>> from evograd.operators import ReflectRepair
30
+ >>> repair = ReflectRepair()
31
+ >>> repaired = repair(population, xl, xu)
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ from abc import ABC, abstractmethod
37
+ from enum import Enum
38
+ from typing import Optional, Union
39
+
40
+ import torch
41
+ import torch.nn as nn
42
+ from torch import Tensor
43
+
44
+ __all__ = [
45
+ "Repair",
46
+ "ClipRepair",
47
+ "ReflectRepair",
48
+ "WrapRepair",
49
+ "RandomRepair",
50
+ "BoundsRepair",
51
+ "RepairMethod",
52
+ "NoRepair",
53
+ ]
54
+
55
+
56
+ class RepairMethod(Enum):
57
+ """Available repair methods."""
58
+ CLIP = "clip"
59
+ REFLECT = "reflect"
60
+ WRAP = "wrap"
61
+ RANDOM = "random"
62
+ NONE = "none"
63
+
64
+
65
+ # =============================================================================
66
+ # Base Repair Class
67
+ # =============================================================================
68
+
69
+ class Repair(nn.Module, ABC):
70
+ """
71
+ Abstract base class for repair operators.
72
+
73
+ Subclasses must implement:
74
+ - _repair(): Apply repair to bring solutions within bounds
75
+ """
76
+
77
+ def __init__(self) -> None:
78
+ super().__init__()
79
+
80
+ @abstractmethod
81
+ def _repair(
82
+ self,
83
+ x: Tensor,
84
+ xl: Tensor,
85
+ xu: Tensor,
86
+ ) -> Tensor:
87
+ """
88
+ Repair solutions to satisfy bounds.
89
+
90
+ Args:
91
+ x: Solutions to repair [n_pop, n_var].
92
+ xl: Lower bounds [n_var] or scalar.
93
+ xu: Upper bounds [n_var] or scalar.
94
+
95
+ Returns:
96
+ Repaired solutions [n_pop, n_var].
97
+ """
98
+ pass
99
+
100
+ def forward(
101
+ self,
102
+ x: Tensor,
103
+ xl: Optional[Tensor] = None,
104
+ xu: Optional[Tensor] = None,
105
+ problem: Optional["Problem"] = None,
106
+ ) -> Tensor:
107
+ """
108
+ Apply repair.
109
+
110
+ Args:
111
+ x: Solutions to repair [n_pop, n_var].
112
+ xl: Lower bounds (or provide problem).
113
+ xu: Upper bounds (or provide problem).
114
+ problem: Problem instance with bounds.
115
+
116
+ Returns:
117
+ Repaired solutions [n_pop, n_var].
118
+ """
119
+ # Get bounds from problem if provided
120
+ if problem is not None:
121
+ xl = problem.xl
122
+ xu = problem.xu
123
+
124
+ # Ensure bounds are provided
125
+ if xl is None or xu is None:
126
+ raise ValueError("Bounds must be provided via xl/xu or problem")
127
+
128
+ # Ensure bounds have correct shape
129
+ n_var = x.shape[-1]
130
+ if xl.dim() == 0:
131
+ xl = xl.expand(n_var)
132
+ if xu.dim() == 0:
133
+ xu = xu.expand(n_var)
134
+
135
+ return self._repair(x, xl, xu)
136
+
137
+ # Note: Do NOT override __call__. nn.Module.__call__ dispatches to
138
+ # forward() and fires registered hooks (forward_pre_hooks, forward_hooks,
139
+ # and the autograd profiler). Overriding __call__ would bypass all of these.
140
+
141
+ def is_within_bounds(
142
+ self,
143
+ x: Tensor,
144
+ xl: Tensor,
145
+ xu: Tensor,
146
+ tol: float = 1e-8,
147
+ ) -> Tensor:
148
+ """
149
+ Check if solutions are within bounds.
150
+
151
+ Args:
152
+ x: Solutions to check [n_pop, n_var].
153
+ xl: Lower bounds.
154
+ xu: Upper bounds.
155
+ tol: Tolerance for boundary check.
156
+
157
+ Returns:
158
+ Boolean tensor [n_pop] indicating feasibility.
159
+ """
160
+ within_lower = (x >= xl - tol).all(dim=-1)
161
+ within_upper = (x <= xu + tol).all(dim=-1)
162
+ return within_lower & within_upper
163
+
164
+
165
+ # =============================================================================
166
+ # Clip (Clamp) Repair
167
+ # =============================================================================
168
+
169
+ class ClipRepair(Repair):
170
+ """
171
+ Clip repair (clamping to bounds).
172
+
173
+ Simply clamps values to [xl, xu]. This is the simplest and
174
+ most common repair method.
175
+
176
+ Note:
177
+ Gradient is zero when values are clipped, which can cause
178
+ issues in differentiable mode. Consider ReflectRepair for
179
+ better gradient flow.
180
+
181
+ Example:
182
+ >>> repair = ClipRepair()
183
+ >>> repaired = repair(population, xl, xu)
184
+ """
185
+
186
+ def _repair(
187
+ self,
188
+ x: Tensor,
189
+ xl: Tensor,
190
+ xu: Tensor,
191
+ ) -> Tensor:
192
+ return torch.clamp(x, min=xl, max=xu)
193
+
194
+ def __repr__(self) -> str:
195
+ return "ClipRepair()"
196
+
197
+
198
+ # =============================================================================
199
+ # Reflect Repair
200
+ # =============================================================================
201
+
202
+ class ReflectRepair(Repair):
203
+ """
204
+ Reflection repair (bounce off boundaries).
205
+
206
+ When a value exceeds a bound, it bounces back into the
207
+ feasible region. This preserves the "momentum" of the
208
+ search and provides better gradient flow than clipping.
209
+
210
+ The reflection is computed as:
211
+ x' = xl + |x - xl| mod (2 * range)
212
+ if x' > xu: x' = 2*xu - x'
213
+
214
+ Args:
215
+ max_iterations: Maximum reflection iterations (prevents
216
+ infinite loops for extreme violations).
217
+
218
+ Example:
219
+ >>> repair = ReflectRepair()
220
+ >>> repaired = repair(population, xl, xu)
221
+
222
+ Note:
223
+ This is the recommended repair method for differentiable
224
+ mode as gradients flow through the reflection operation.
225
+ """
226
+
227
+ def __init__(self, max_iterations: int = 100) -> None:
228
+ super().__init__()
229
+ self.max_iterations = max_iterations
230
+
231
+ def _repair(
232
+ self,
233
+ x: Tensor,
234
+ xl: Tensor,
235
+ xu: Tensor,
236
+ ) -> Tensor:
237
+ # Compute range
238
+ span = xu - xl
239
+
240
+ # Handle zero span (fixed variables)
241
+ span = torch.where(span > 0, span, torch.ones_like(span))
242
+
243
+ # Normalise to [0, 2*span] then fold
244
+ x_shifted = x - xl
245
+ x_mod = torch.remainder(x_shifted, 2 * span)
246
+
247
+ # Fold back: if > span, reflect from upper bound
248
+ x_folded = torch.where(x_mod > span, 2 * span - x_mod, x_mod)
249
+
250
+ # Shift back to original space
251
+ x_repaired = xl + x_folded
252
+
253
+ return x_repaired
254
+
255
+ def __repr__(self) -> str:
256
+ return f"ReflectRepair(max_iterations={self.max_iterations})"
257
+
258
+
259
+ # =============================================================================
260
+ # Wrap (Periodic) Repair
261
+ # =============================================================================
262
+
263
+ class WrapRepair(Repair):
264
+ """
265
+ Wrap repair (periodic/toroidal boundaries).
266
+
267
+ Values that exceed bounds wrap around to the other side,
268
+ treating the search space as a torus. Useful for periodic
269
+ domains like angles.
270
+
271
+ The wrapping is computed as:
272
+ x' = xl + (x - xl) mod (xu - xl)
273
+
274
+ Example:
275
+ >>> # For angular variables [0, 2*pi]
276
+ >>> repair = WrapRepair()
277
+ >>> repaired = repair(angles, 0, 2*np.pi)
278
+
279
+ Note:
280
+ Gradient is discontinuous at boundaries but flows
281
+ through the modulo operation.
282
+ """
283
+
284
+ def _repair(
285
+ self,
286
+ x: Tensor,
287
+ xl: Tensor,
288
+ xu: Tensor,
289
+ ) -> Tensor:
290
+ span = xu - xl
291
+
292
+ # Handle zero span
293
+ span = torch.where(span > 0, span, torch.ones_like(span))
294
+
295
+ # Periodic wrapping
296
+ x_wrapped = xl + torch.remainder(x - xl, span)
297
+
298
+ return x_wrapped
299
+
300
+ def __repr__(self) -> str:
301
+ return "WrapRepair()"
302
+
303
+
304
+ # =============================================================================
305
+ # Random Repair
306
+ # =============================================================================
307
+
308
+ class RandomRepair(Repair):
309
+ """
310
+ Random repair (reset violating genes).
311
+
312
+ Genes that violate bounds are reset to random values within
313
+ the feasible region. This is more disruptive than other
314
+ methods but can help escape from boundary regions.
315
+
316
+ Example:
317
+ >>> repair = RandomRepair()
318
+ >>> repaired = repair(population, xl, xu)
319
+
320
+ Note:
321
+ Not differentiable through the random reset operation.
322
+ Gradient is zero for repaired genes.
323
+ """
324
+
325
+ def _repair(
326
+ self,
327
+ x: Tensor,
328
+ xl: Tensor,
329
+ xu: Tensor,
330
+ ) -> Tensor:
331
+ # Find violations
332
+ below = x < xl
333
+ above = x > xu
334
+ violates = below | above
335
+
336
+ # Generate random replacements
337
+ random_vals = xl + (xu - xl) * torch.rand_like(x)
338
+
339
+ # Replace only violating genes
340
+ x_repaired = torch.where(violates, random_vals, x)
341
+
342
+ return x_repaired
343
+
344
+ def __repr__(self) -> str:
345
+ return "RandomRepair()"
346
+
347
+
348
+ # =============================================================================
349
+ # No Repair (Identity)
350
+ # =============================================================================
351
+
352
+ class NoRepair(Repair):
353
+ """
354
+ No repair (identity operator).
355
+
356
+ Returns input unchanged. Useful as a placeholder when
357
+ repair should be disabled or handled elsewhere.
358
+
359
+ Example:
360
+ >>> repair = NoRepair()
361
+ >>> repaired = repair(population, xl, xu) # Returns unchanged
362
+ """
363
+
364
+ def _repair(
365
+ self,
366
+ x: Tensor,
367
+ xl: Tensor,
368
+ xu: Tensor,
369
+ ) -> Tensor:
370
+ return x
371
+
372
+ def __repr__(self) -> str:
373
+ return "NoRepair()"
374
+
375
+
376
+ # =============================================================================
377
+ # Configurable Bounds Repair
378
+ # =============================================================================
379
+
380
+ class BoundsRepair(Repair):
381
+ """
382
+ Configurable bounds repair with method selection.
383
+
384
+ Convenience class that allows selecting the repair method
385
+ via a string or enum parameter.
386
+
387
+ Args:
388
+ method: Repair method to use. Options:
389
+ - 'clip': Clamp to bounds (default)
390
+ - 'reflect': Bounce off boundaries
391
+ - 'wrap': Periodic wrapping
392
+ - 'random': Reset violating genes
393
+ - 'none': No repair
394
+
395
+ Example:
396
+ >>> repair = BoundsRepair(method='reflect')
397
+ >>> repaired = repair(population, xl, xu)
398
+ >>>
399
+ >>> # Or use enum
400
+ >>> from evograd.operators import RepairMethod
401
+ >>> repair = BoundsRepair(method=RepairMethod.WRAP)
402
+ """
403
+
404
+ _METHOD_MAP = {
405
+ 'clip': ClipRepair,
406
+ 'clamp': ClipRepair,
407
+ 'reflect': ReflectRepair,
408
+ 'bounce': ReflectRepair,
409
+ 'wrap': WrapRepair,
410
+ 'periodic': WrapRepair,
411
+ 'toroidal': WrapRepair,
412
+ 'random': RandomRepair,
413
+ 'none': NoRepair,
414
+ }
415
+
416
+ def __init__(
417
+ self,
418
+ method: Union[str, RepairMethod] = "clip",
419
+ ) -> None:
420
+ super().__init__()
421
+
422
+ # Convert enum to string
423
+ if isinstance(method, RepairMethod):
424
+ method = method.value
425
+
426
+ method = method.lower()
427
+
428
+ if method not in self._METHOD_MAP:
429
+ valid = list(self._METHOD_MAP.keys())
430
+ raise ValueError(
431
+ f"Unknown repair method '{method}'. Valid options: {valid}"
432
+ )
433
+
434
+ self.method = method
435
+ self._repair_impl = self._METHOD_MAP[method]()
436
+
437
+ def _repair(
438
+ self,
439
+ x: Tensor,
440
+ xl: Tensor,
441
+ xu: Tensor,
442
+ ) -> Tensor:
443
+ return self._repair_impl._repair(x, xl, xu)
444
+
445
+ def __repr__(self) -> str:
446
+ return f"BoundsRepair(method='{self.method}')"
447
+
448
+
449
+ # =============================================================================
450
+ # Soft Clip Repair (Differentiable-friendly)
451
+ # =============================================================================
452
+
453
+ class SoftClipRepair(Repair):
454
+ """
455
+ Soft clip repair using smooth approximation.
456
+
457
+ Uses a smooth approximation of the clip function that
458
+ provides non-zero gradients near boundaries. This is
459
+ useful when gradients are important but values should
460
+ still be approximately within bounds.
461
+
462
+ The soft clip is computed using:
463
+ softplus(x - xl) - softplus(x - xu) + xl
464
+
465
+ Args:
466
+ beta: Smoothness parameter (higher = sharper, closer to hard clip).
467
+ margin: How far outside bounds the soft clip extends.
468
+
469
+ Example:
470
+ >>> repair = SoftClipRepair(beta=10.0)
471
+ >>> repaired = repair(population, xl, xu)
472
+
473
+ Note:
474
+ Values may slightly exceed bounds. Use hard clip after
475
+ if strict feasibility is required.
476
+ """
477
+
478
+ def __init__(
479
+ self,
480
+ beta: float = 10.0,
481
+ margin: float = 0.1,
482
+ ) -> None:
483
+ super().__init__()
484
+ self.beta = beta
485
+ self.margin = margin
486
+
487
+ def _soft_clip(
488
+ self,
489
+ x: Tensor,
490
+ lower: Tensor,
491
+ upper: Tensor,
492
+ ) -> Tensor:
493
+ """Smooth clip using softplus."""
494
+ # Soft lower bound: max(x, lower) ≈ lower + softplus(x - lower)
495
+ x_lower = lower + torch.nn.functional.softplus(
496
+ (x - lower) * self.beta
497
+ ) / self.beta
498
+
499
+ # Soft upper bound: min(x, upper) ≈ upper - softplus(upper - x)
500
+ x_clipped = upper - torch.nn.functional.softplus(
501
+ (upper - x_lower) * self.beta
502
+ ) / self.beta
503
+
504
+ return x_clipped
505
+
506
+ def _repair(
507
+ self,
508
+ x: Tensor,
509
+ xl: Tensor,
510
+ xu: Tensor,
511
+ ) -> Tensor:
512
+ return self._soft_clip(x, xl, xu)
513
+
514
+ def __repr__(self) -> str:
515
+ return f"SoftClipRepair(beta={self.beta}, margin={self.margin})"
516
+
517
+
518
+ # =============================================================================
519
+ # Penalty-based Repair (returns penalty instead of repairing)
520
+ # =============================================================================
521
+
522
+ class PenaltyRepair(Repair):
523
+ """
524
+ Penalty-based "repair" that computes constraint violation.
525
+
526
+ Instead of modifying solutions, this computes a penalty
527
+ term that can be added to the fitness. Useful for
528
+ constrained optimisation with penalty methods.
529
+
530
+ The penalty is computed as:
531
+ penalty = sum(max(0, xl - x)^2 + max(0, x - xu)^2)
532
+
533
+ Args:
534
+ penalty_weight: Multiplier for the penalty term.
535
+ power: Exponent for violation (1=linear, 2=quadratic).
536
+
537
+ Example:
538
+ >>> repair = PenaltyRepair(penalty_weight=1000)
539
+ >>> penalty = repair.compute_penalty(population, xl, xu)
540
+ >>> fitness_penalised = fitness + penalty
541
+
542
+ Note:
543
+ forward() still returns the input unchanged. Use
544
+ compute_penalty() to get the penalty values.
545
+ """
546
+
547
+ def __init__(
548
+ self,
549
+ penalty_weight: float = 1.0,
550
+ power: float = 2.0,
551
+ ) -> None:
552
+ super().__init__()
553
+ self.penalty_weight = penalty_weight
554
+ self.power = power
555
+
556
+ def compute_penalty(
557
+ self,
558
+ x: Tensor,
559
+ xl: Tensor,
560
+ xu: Tensor,
561
+ ) -> Tensor:
562
+ """
563
+ Compute penalty for constraint violations.
564
+
565
+ Args:
566
+ x: Solutions [n_pop, n_var].
567
+ xl: Lower bounds.
568
+ xu: Upper bounds.
569
+
570
+ Returns:
571
+ Penalty values [n_pop].
572
+ """
573
+ # Lower bound violations
574
+ lower_violation = torch.clamp(xl - x, min=0.0)
575
+
576
+ # Upper bound violations
577
+ upper_violation = torch.clamp(x - xu, min=0.0)
578
+
579
+ # Total penalty per individual
580
+ penalty = (
581
+ lower_violation.pow(self.power).sum(dim=-1) +
582
+ upper_violation.pow(self.power).sum(dim=-1)
583
+ )
584
+
585
+ return self.penalty_weight * penalty
586
+
587
+ def _repair(
588
+ self,
589
+ x: Tensor,
590
+ xl: Tensor,
591
+ xu: Tensor,
592
+ ) -> Tensor:
593
+ # No modification - penalty is computed separately
594
+ return x
595
+
596
+ def __repr__(self) -> str:
597
+ return (
598
+ f"PenaltyRepair("
599
+ f"penalty_weight={self.penalty_weight}, "
600
+ f"power={self.power})"
601
+ )