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,602 @@
1
+ """
2
+ Termination criteria for EvoGrad optimisation.
3
+
4
+ This module provides termination conditions that determine when
5
+ the optimisation loop should stop. Criteria can be combined
6
+ using logical operators (AND, OR).
7
+
8
+ Available termination criteria:
9
+ - MaxEvaluations: Stop after N fitness evaluations
10
+ - MaxGenerations: Stop after N generations
11
+ - TargetReached: Stop when fitness reaches target value
12
+ - ToleranceReached: Stop when improvement falls below threshold
13
+ - TimeLimit: Stop after N seconds
14
+ - NoTermination: Never terminates (use with caution)
15
+
16
+ Termination criteria are passed to minimize/maximize functions,
17
+ not to the algorithm itself.
18
+
19
+ Example:
20
+ >>> from evograd.core import MaxEvaluations, TargetReached
21
+ >>>
22
+ >>> # Single criterion
23
+ >>> termination = MaxEvaluations(10000)
24
+ >>>
25
+ >>> # Combined criteria (stop when ANY is met)
26
+ >>> termination = MaxEvaluations(10000) | TargetReached(1e-6)
27
+ >>>
28
+ >>> # Combined criteria (stop when ALL are met)
29
+ >>> termination = MaxGenerations(100) & ToleranceReached(1e-8)
30
+ >>>
31
+ >>> # Use in minimize
32
+ >>> result = minimize(algorithm, problem, termination=termination)
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import time
38
+ from abc import ABC, abstractmethod
39
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
40
+
41
+ if TYPE_CHECKING:
42
+ from evograd.core.algorithm import Algorithm
43
+
44
+ __all__ = [
45
+ "Termination",
46
+ "MaxEvaluations",
47
+ "MaxGenerations",
48
+ "TargetReached",
49
+ "ToleranceReached",
50
+ "TimeLimit",
51
+ "NoTermination",
52
+ "TerminationCollection",
53
+ "default_termination",
54
+ ]
55
+
56
+
57
+ # =============================================================================
58
+ # Base Termination Class
59
+ # =============================================================================
60
+
61
+ class Termination(ABC):
62
+ """
63
+ Abstract base class for termination criteria.
64
+
65
+ Subclasses must implement:
66
+ - _should_terminate(): Check if criterion is met
67
+
68
+ Optionally override:
69
+ - _update(): Update internal state each generation
70
+ - _reset(): Reset state for new optimisation run
71
+
72
+ Termination criteria can be combined using | (OR) and & (AND):
73
+ - criterion1 | criterion2: Stop when either is met
74
+ - criterion1 & criterion2: Stop when both are met
75
+ """
76
+
77
+ def __init__(self) -> None:
78
+ self._is_terminated = False
79
+ self._termination_reason: Optional[str] = None
80
+
81
+ @abstractmethod
82
+ def _should_terminate(self, algorithm: Algorithm) -> bool:
83
+ """
84
+ Check if termination criterion is met.
85
+
86
+ Args:
87
+ algorithm: The algorithm instance to check.
88
+
89
+ Returns:
90
+ True if should terminate, False otherwise.
91
+ """
92
+ pass
93
+
94
+ def _update(self, algorithm: Algorithm) -> None:
95
+ """
96
+ Update internal state. Called each generation.
97
+
98
+ Override in subclasses that need to track history.
99
+
100
+ Args:
101
+ algorithm: The algorithm instance.
102
+ """
103
+ pass
104
+
105
+ def _reset(self) -> None:
106
+ """
107
+ Reset internal state for new optimisation run.
108
+
109
+ Override in subclasses with internal state.
110
+ """
111
+ self._is_terminated = False
112
+ self._termination_reason = None
113
+
114
+ def should_terminate(self, algorithm: Algorithm) -> bool:
115
+ """
116
+ Check termination and update internal state.
117
+
118
+ Args:
119
+ algorithm: The algorithm instance to check.
120
+
121
+ Returns:
122
+ True if should terminate, False otherwise.
123
+ """
124
+ self._update(algorithm)
125
+
126
+ if self._should_terminate(algorithm):
127
+ self._is_terminated = True
128
+ return True
129
+
130
+ return False
131
+
132
+ def reset(self) -> "Termination":
133
+ """
134
+ Reset for new optimisation run.
135
+
136
+ Returns:
137
+ Self for method chaining.
138
+ """
139
+ self._reset()
140
+ return self
141
+
142
+ @property
143
+ def is_terminated(self) -> bool:
144
+ """Whether termination has been triggered."""
145
+ return self._is_terminated
146
+
147
+ @property
148
+ def reason(self) -> Optional[str]:
149
+ """Reason for termination, if terminated."""
150
+ return self._termination_reason
151
+
152
+ def __or__(self, other: "Termination") -> "TerminationCollection":
153
+ """Combine with OR: stop when either criterion is met."""
154
+ return TerminationCollection([self, other], mode="or")
155
+
156
+ def __and__(self, other: "Termination") -> "TerminationCollection":
157
+ """Combine with AND: stop when both criteria are met."""
158
+ return TerminationCollection([self, other], mode="and")
159
+
160
+ def __repr__(self) -> str:
161
+ return f"{self.__class__.__name__}()"
162
+
163
+
164
+ # =============================================================================
165
+ # Max Evaluations
166
+ # =============================================================================
167
+
168
+ class MaxEvaluations(Termination):
169
+ """
170
+ Terminate after maximum number of fitness evaluations.
171
+
172
+ Args:
173
+ max_evals: Maximum number of fitness evaluations.
174
+
175
+ Example:
176
+ >>> termination = MaxEvaluations(10000)
177
+ """
178
+
179
+ def __init__(self, max_evals: int) -> None:
180
+ super().__init__()
181
+
182
+ if max_evals < 1:
183
+ raise ValueError(f"n_evals must be >= 1, got {max_evals}")
184
+
185
+ self.max_evals = max_evals
186
+
187
+ def _should_terminate(self, algorithm: Algorithm) -> bool:
188
+ if algorithm.n_evals >= self.max_evals:
189
+ self._termination_reason = (
190
+ f"Maximum evaluations reached: {algorithm.n_evals} >= {self.max_evals}"
191
+ )
192
+ return True
193
+ return False
194
+
195
+ def progress(self, algorithm: Algorithm) -> float:
196
+ """Return progress as fraction [0, 1]."""
197
+ return min(1.0, algorithm.n_evals / self.max_evals)
198
+
199
+ def __repr__(self) -> str:
200
+ return f"MaxEvaluations({self.max_evals})"
201
+
202
+
203
+ # =============================================================================
204
+ # Max Generations
205
+ # =============================================================================
206
+
207
+ class MaxGenerations(Termination):
208
+ """
209
+ Terminate after maximum number of generations.
210
+
211
+ Args:
212
+ max_gens: Maximum number of generations.
213
+
214
+ Example:
215
+ >>> termination = MaxGenerations(500)
216
+ """
217
+
218
+ def __init__(self, max_gens: int) -> None:
219
+ super().__init__()
220
+
221
+ if max_gens < 1:
222
+ raise ValueError(f"n_gen must be >= 1, got {max_gens}")
223
+
224
+ self.max_gens = max_gens
225
+
226
+ def _should_terminate(self, algorithm: Algorithm) -> bool:
227
+ if algorithm.generation >= self.max_gens:
228
+ self._termination_reason = (
229
+ f"Maximum generations reached: {algorithm.generation} >= {self.max_gens}"
230
+ )
231
+ return True
232
+ return False
233
+
234
+ def progress(self, algorithm: Algorithm) -> float:
235
+ """Return progress as fraction [0, 1]."""
236
+ return min(1.0, algorithm.generation / self.max_gens)
237
+
238
+ def __repr__(self) -> str:
239
+ return f"MaxGenerations({self.max_gens})"
240
+
241
+
242
+ # =============================================================================
243
+ # Target Reached
244
+ # =============================================================================
245
+
246
+ class TargetReached(Termination):
247
+ """
248
+ Terminate when fitness reaches target value.
249
+
250
+ For minimisation: stop when best_fitness <= target
251
+ For maximisation: stop when best_fitness >= target
252
+
253
+ Args:
254
+ target_fitness: Target fitness value.
255
+ minimize: If True, stop when fitness <= target.
256
+ If False, stop when fitness >= target.
257
+
258
+ Example:
259
+ >>> # For minimisation (default)
260
+ >>> termination = TargetReached(1e-6)
261
+ >>>
262
+ >>> # For maximisation
263
+ >>> termination = TargetReached(0.99, minimize=False)
264
+ """
265
+
266
+ def __init__(self, target_fitness: float, minimize: bool = True) -> None:
267
+ super().__init__()
268
+ self.target_fitness = target_fitness
269
+ self.minimize = minimize
270
+
271
+ def _should_terminate(self, algorithm: Algorithm) -> bool:
272
+ best = algorithm.best_fitness
273
+
274
+ if self.minimize:
275
+ reached = best <= self.target_fitness
276
+ else:
277
+ reached = best >= self.target_fitness
278
+
279
+ if reached:
280
+ direction = "<=" if self.minimize else ">="
281
+ self._termination_reason = (
282
+ f"Target reached: {best:.6g} {direction} {self.target_fitness:.6g}"
283
+ )
284
+ return True
285
+
286
+ return False
287
+
288
+ def __repr__(self) -> str:
289
+ mode = "min" if self.minimize else "max"
290
+ return f"TargetReached({self.target_fitness}, {mode})"
291
+
292
+
293
+ # =============================================================================
294
+ # Tolerance Reached (Convergence)
295
+ # =============================================================================
296
+
297
+ class ToleranceReached(Termination):
298
+ """
299
+ Terminate when fitness improvement falls below tolerance.
300
+
301
+ Monitors the change in best fitness over a window of generations.
302
+ Stops when the relative or absolute change is below the threshold.
303
+
304
+ Args:
305
+ tol: Tolerance threshold for fitness change.
306
+ n_last: Number of generations to consider for change calculation.
307
+ mode: 'absolute' or 'relative' change measurement.
308
+
309
+ Example:
310
+ >>> # Stop when absolute change < 1e-8 over last 20 generations
311
+ >>> termination = ToleranceReached(tol=1e-8, n_last=20)
312
+ >>>
313
+ >>> # Stop when relative change < 0.1% over last 50 generations
314
+ >>> termination = ToleranceReached(tol=0.001, n_last=50, mode='relative')
315
+ """
316
+
317
+ def __init__(
318
+ self,
319
+ tol: float = 1e-6,
320
+ n_last: int = 20,
321
+ mode: str = "absolute",
322
+ ) -> None:
323
+ super().__init__()
324
+
325
+ if tol <= 0:
326
+ raise ValueError(f"tol must be > 0, got {tol}")
327
+ if n_last < 2:
328
+ raise ValueError(f"n_last must be >= 2, got {n_last}")
329
+ if mode not in ("absolute", "relative"):
330
+ raise ValueError(f"mode must be 'absolute' or 'relative', got '{mode}'")
331
+
332
+ self.tol = tol
333
+ self.n_last = n_last
334
+ self.mode = mode
335
+
336
+ self._history: List[float] = []
337
+
338
+ def _reset(self) -> None:
339
+ super()._reset()
340
+ self._history.clear()
341
+
342
+ def _update(self, algorithm: Algorithm) -> None:
343
+ self._history.append(algorithm.best_fitness)
344
+
345
+ # Keep only n_last entries
346
+ if len(self._history) > self.n_last:
347
+ self._history.pop(0)
348
+
349
+ def _should_terminate(self, algorithm: Algorithm) -> bool:
350
+ # Need enough history
351
+ if len(self._history) < self.n_last:
352
+ return False
353
+
354
+ old_val = self._history[0]
355
+ new_val = self._history[-1]
356
+
357
+ if self.mode == "absolute":
358
+ change = abs(new_val - old_val)
359
+ else: # relative
360
+ if abs(old_val) < 1e-10:
361
+ change = abs(new_val - old_val)
362
+ else:
363
+ change = abs((new_val - old_val) / old_val)
364
+
365
+ if change < self.tol:
366
+ self._termination_reason = (
367
+ f"Tolerance reached: {self.mode} change {change:.2e} < {self.tol:.2e} "
368
+ f"over {self.n_last} generations"
369
+ )
370
+ return True
371
+
372
+ return False
373
+
374
+ def __repr__(self) -> str:
375
+ return f"ToleranceReached(tol={self.tol}, n_last={self.n_last}, mode='{self.mode}')"
376
+
377
+
378
+ # =============================================================================
379
+ # Time Limit
380
+ # =============================================================================
381
+
382
+ class TimeLimit(Termination):
383
+ """
384
+ Terminate after time limit is reached.
385
+
386
+ Args:
387
+ max_seconds: Maximum time in seconds.
388
+
389
+ Example:
390
+ >>> # Stop after 60 seconds
391
+ >>> termination = TimeLimit(60)
392
+ >>>
393
+ >>> # Stop after 5 minutes
394
+ >>> termination = TimeLimit(5 * 60)
395
+ """
396
+
397
+ def __init__(self, max_seconds: float) -> None:
398
+ super().__init__()
399
+
400
+ if max_seconds <= 0:
401
+ raise ValueError(f"seconds must be > 0, got {max_seconds}")
402
+
403
+ self.max_seconds = max_seconds
404
+ self._start_time: Optional[float] = None
405
+
406
+ def _reset(self) -> None:
407
+ super()._reset()
408
+ self._start_time = None
409
+
410
+ def _update(self, algorithm: Algorithm) -> None:
411
+ if self._start_time is None:
412
+ self._start_time = time.perf_counter()
413
+
414
+ def _should_terminate(self, algorithm: Algorithm) -> bool:
415
+ if self._start_time is None:
416
+ return False
417
+
418
+ elapsed = time.perf_counter() - self._start_time
419
+
420
+ if elapsed >= self.max_seconds:
421
+ self._termination_reason = (
422
+ f"Time limit reached: {elapsed:.1f}s >= {self.max_seconds:.1f}s"
423
+ )
424
+ return True
425
+
426
+ return False
427
+
428
+ @property
429
+ def elapsed(self) -> float:
430
+ """Elapsed time in seconds."""
431
+ if self._start_time is None:
432
+ return 0.0
433
+ return time.perf_counter() - self._start_time
434
+
435
+ def progress(self, algorithm: Algorithm) -> float:
436
+ """Return progress as fraction [0, 1]."""
437
+ return min(1.0, self.elapsed / self.max_seconds)
438
+
439
+ def __repr__(self) -> str:
440
+ return f"TimeLimit({self.max_seconds}s)"
441
+
442
+
443
+ # =============================================================================
444
+ # No Termination
445
+ # =============================================================================
446
+
447
+ class NoTermination(Termination):
448
+ """
449
+ Never terminates. Use with caution!
450
+
451
+ Useful when termination is handled externally or for testing.
452
+
453
+ Example:
454
+ >>> termination = NoTermination()
455
+ """
456
+
457
+ def _should_terminate(self, algorithm: Algorithm) -> bool:
458
+ return False
459
+
460
+ def __repr__(self) -> str:
461
+ return "NoTermination()"
462
+
463
+
464
+ # =============================================================================
465
+ # Termination Collection (Combined Criteria)
466
+ # =============================================================================
467
+
468
+ class TerminationCollection(Termination):
469
+ """
470
+ Combine multiple termination criteria.
471
+
472
+ Args:
473
+ criteria: List of termination criteria.
474
+ mode: 'or' (stop when any is met) or 'and' (stop when all are met).
475
+
476
+ Example:
477
+ >>> # Stop when either max evals OR target reached
478
+ >>> combined = TerminationCollection(
479
+ ... [MaxEvaluations(10000), TargetReached(1e-6)],
480
+ ... mode='or'
481
+ ... )
482
+ >>>
483
+ >>> # Or use operators
484
+ >>> combined = MaxEvaluations(10000) | TargetReached(1e-6)
485
+ """
486
+
487
+ def __init__(
488
+ self,
489
+ criteria: List[Termination],
490
+ mode: str = "or",
491
+ ) -> None:
492
+ super().__init__()
493
+
494
+ if mode not in ("or", "and"):
495
+ raise ValueError(f"mode must be 'or' or 'and', got '{mode}'")
496
+
497
+ self.criteria = list(criteria)
498
+ self.mode = mode
499
+
500
+ def _reset(self) -> None:
501
+ super()._reset()
502
+ for criterion in self.criteria:
503
+ criterion.reset()
504
+
505
+ def _update(self, algorithm: Algorithm) -> None:
506
+ for criterion in self.criteria:
507
+ criterion._update(algorithm)
508
+
509
+ def _should_terminate(self, algorithm: Algorithm) -> bool:
510
+ results = [c._should_terminate(algorithm) for c in self.criteria]
511
+
512
+ if self.mode == "or":
513
+ # Terminate if ANY criterion is met
514
+ if any(results):
515
+ # Find which criterion triggered
516
+ reasons = [
517
+ c.reason for c, r in zip(self.criteria, results)
518
+ if r and c.reason
519
+ ]
520
+ self._termination_reason = " OR ".join(reasons)
521
+ return True
522
+ return False
523
+ else:
524
+ # Terminate only if ALL criteria are met
525
+ if all(results):
526
+ reasons = [c.reason for c in self.criteria if c.reason]
527
+ self._termination_reason = " AND ".join(reasons)
528
+ return True
529
+ return False
530
+
531
+ def __or__(self, other: Termination) -> "TerminationCollection":
532
+ """Add criterion with OR logic."""
533
+ if self.mode == "or":
534
+ return TerminationCollection(self.criteria + [other], mode="or")
535
+ return TerminationCollection([self, other], mode="or")
536
+
537
+ def __and__(self, other: Termination) -> "TerminationCollection":
538
+ """Add criterion with AND logic."""
539
+ if self.mode == "and":
540
+ return TerminationCollection(self.criteria + [other], mode="and")
541
+ return TerminationCollection([self, other], mode="and")
542
+
543
+ def __repr__(self) -> str:
544
+ op = " | " if self.mode == "or" else " & "
545
+ criteria_str = op.join(repr(c) for c in self.criteria)
546
+ return f"({criteria_str})"
547
+
548
+
549
+ # =============================================================================
550
+ # Convenience Functions
551
+ # =============================================================================
552
+
553
+ def default_termination(
554
+ max_evals: Optional[int] = None,
555
+ max_gen: Optional[int] = None,
556
+ target: Optional[float] = None,
557
+ tol: Optional[float] = None,
558
+ time_limit: Optional[float] = None,
559
+ ) -> Termination:
560
+ """
561
+ Create a termination criterion from common parameters.
562
+
563
+ Multiple criteria are combined with OR (stop when any is met).
564
+
565
+ Args:
566
+ max_evals: Maximum fitness evaluations.
567
+ max_gen: Maximum generations.
568
+ target: Target fitness value (for minimisation).
569
+ tol: Convergence tolerance.
570
+ time_limit: Time limit in seconds.
571
+
572
+ Returns:
573
+ Termination criterion (single or combined).
574
+
575
+ Example:
576
+ >>> # Stop at 10000 evals or when target reached
577
+ >>> termination = default_termination(max_evals=10000, target=1e-6)
578
+ """
579
+ criteria = []
580
+
581
+ if max_evals is not None:
582
+ criteria.append(MaxEvaluations(max_evals))
583
+
584
+ if max_gen is not None:
585
+ criteria.append(MaxGenerations(max_gen))
586
+
587
+ if target is not None:
588
+ criteria.append(TargetReached(target))
589
+
590
+ if tol is not None:
591
+ criteria.append(ToleranceReached(tol))
592
+
593
+ if time_limit is not None:
594
+ criteria.append(TimeLimit(time_limit))
595
+
596
+ if not criteria:
597
+ return MaxEvaluations(10000)
598
+
599
+ if len(criteria) == 1:
600
+ return criteria[0]
601
+
602
+ return TerminationCollection(criteria, mode="or")