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.
- evograd/__init__.py +67 -0
- evograd/algorithms/__init__.py +138 -0
- evograd/algorithms/cmaes.py +1365 -0
- evograd/algorithms/de.py +895 -0
- evograd/algorithms/ga.py +532 -0
- evograd/algorithms/pso.py +648 -0
- evograd/algorithms/shade.py +1165 -0
- evograd/benchmarks/functions/__init__.py +229 -0
- evograd/benchmarks/functions/base.py +217 -0
- evograd/benchmarks/functions/cec2017/__init__.py +250 -0
- evograd/benchmarks/functions/cec2017/basic.py +413 -0
- evograd/benchmarks/functions/cec2017/composition.py +580 -0
- evograd/benchmarks/functions/cec2017/data.pkl +0 -0
- evograd/benchmarks/functions/cec2017/data.py +350 -0
- evograd/benchmarks/functions/cec2017/hybrid.py +406 -0
- evograd/benchmarks/functions/cec2017/simple.py +326 -0
- evograd/benchmarks/functions/classical.py +649 -0
- evograd/benchmarks/functions/smoothed_funnel.py +476 -0
- evograd/benchmarks/functions/transforms.py +463 -0
- evograd/benchmarks/run_benchmark_functions.py +1208 -0
- evograd/core/__init__.py +73 -0
- evograd/core/algorithm.py +778 -0
- evograd/core/maximize.py +269 -0
- evograd/core/minimize.py +740 -0
- evograd/core/problem.py +444 -0
- evograd/core/result.py +571 -0
- evograd/core/termination.py +602 -0
- evograd/operators/__init__.py +178 -0
- evograd/operators/crossover.py +1117 -0
- evograd/operators/mutation.py +1098 -0
- evograd/operators/relaxations.py +175 -0
- evograd/operators/repair.py +601 -0
- evograd/operators/sampling.py +577 -0
- evograd/operators/selection.py +981 -0
- evograd/operators/survival.py +1000 -0
- evograd/tests/__init__.py +11 -0
- evograd/tests/run_all.py +78 -0
- evograd/tests/test_core.py +528 -0
- evograd/tests/test_ga.py +572 -0
- evograd/tests/test_operators.py +662 -0
- evograd/tests/test_per_individual.py +326 -0
- evograd/tests/test_utils.py +328 -0
- evograd/utils/__init__.py +97 -0
- evograd/utils/callbacks.py +926 -0
- evograd/utils/device.py +502 -0
- evograd/utils/duplicates.py +421 -0
- evograd_diff-0.1.0.dist-info/METADATA +439 -0
- evograd_diff-0.1.0.dist-info/RECORD +50 -0
- evograd_diff-0.1.0.dist-info/WHEEL +4 -0
- 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")
|