1 package net.bmahe.genetics4j.core.termination;
2
3 import java.time.Duration;
4 import java.util.Arrays;
5 import java.util.Comparator;
6 import java.util.List;
7 import java.util.Objects;
8 import java.util.Optional;
9
10 import org.apache.commons.lang3.Validate;
11
12 import net.bmahe.genetics4j.core.Genotype;
13 import net.bmahe.genetics4j.core.spec.AbstractEAConfiguration;
14
15 /**
16 * Utility class providing factory methods for creating common termination conditions in evolutionary algorithms.
17 *
18 * <p>Terminations provides a comprehensive set of pre-built termination criteria that can be used individually or
19 * combined to create complex stopping conditions for evolutionary algorithms. Each method returns a {@link Termination}
20 * instance that encapsulates the specific logic for determining when evolution should stop.
21 *
22 * <p>Available termination criteria include:
23 * <ul>
24 * <li><strong>Generation-based</strong>: Stop after a maximum number of generations</li>
25 * <li><strong>Time-based</strong>: Stop after a specified duration has elapsed</li>
26 * <li><strong>Fitness-based</strong>: Stop when fitness reaches certain thresholds</li>
27 * <li><strong>Convergence-based</strong>: Stop when fitness stops improving for a period</li>
28 * <li><strong>Logical combinations</strong>: Combine multiple criteria with AND/OR logic</li>
29 * </ul>
30 *
31 * <p>Termination criteria can be combined to create sophisticated stopping conditions:
32 *
33 * <pre>{@code
34 * // Stop after 100 generations OR when fitness reaches 0.95 OR after 5 minutes
35 * Termination<Double> complexTermination = Terminations.or(
36 * Terminations.ofMaxGeneration(100),
37 * Terminations.ofFitnessAtLeast(0.95),
38 * Terminations.ofMaxTime(Duration.ofMinutes(5)));
39 *
40 * // Stop only when BOTH conditions are met: good fitness AND stable evolution
41 * Termination<Double> conservativeTermination = Terminations
42 * .and(Terminations.ofFitnessAtLeast(0.9), Terminations.ofStableFitness(20));
43 *
44 * // Simple generation limit for quick experiments
45 * Termination<Double> simpleTermination = Terminations.ofMaxGeneration(50);
46 * }</pre>
47 *
48 * <p>Common usage patterns:
49 * <ul>
50 * <li><strong>Development and testing</strong>: Use generation limits for quick experimentation</li>
51 * <li><strong>Production systems</strong>: Combine time limits with fitness criteria for reliability</li>
52 * <li><strong>Research applications</strong>: Use convergence detection to study algorithm behavior</li>
53 * <li><strong>Resource-constrained environments</strong>: Use time-based limits for predictable execution</li>
54 * </ul>
55 *
56 * <p>Design considerations:
57 * <ul>
58 * <li><strong>Performance</strong>: Termination checks are called frequently; implementations are optimized</li>
59 * <li><strong>Thread safety</strong>: Some termination criteria maintain internal state safely</li>
60 * <li><strong>Flexibility</strong>: All criteria can be combined using logical operators</li>
61 * <li><strong>Reliability</strong>: Include fallback termination criteria to prevent infinite loops</li>
62 * </ul>
63 *
64 * @see Termination
65 * @see net.bmahe.genetics4j.core.EASystem
66 * @see net.bmahe.genetics4j.core.spec.EAExecutionContext
67 */
68 public class Terminations {
69
70 /**
71 * Creates a termination condition that stops evolution after a specified number of generations.
72 *
73 * <p>This is the most common termination criterion, providing a simple upper bound on the number of evolutionary
74 * cycles. The algorithm will terminate when the generation counter reaches or exceeds the specified maximum.
75 *
76 * @param <T> the type of fitness values in the evolutionary algorithm
77 * @param maxGeneration the maximum number of generations to run (must be positive)
78 * @return a termination condition that stops after the specified number of generations
79 * @throws IllegalArgumentException if maxGeneration is not positive
80 */
81 public static <T extends Comparable<T>> Termination<T> ofMaxGeneration(final long maxGeneration) {
82 Validate.isTrue(maxGeneration > 0);
83
84 return new Termination<>() {
85
86 @Override
87 public boolean isDone(final AbstractEAConfiguration<T> eaConfiguration, final long generation,
88 final List<Genotype> population, final List<T> fitness) {
89 Validate.isTrue(generation >= 0);
90
91 return generation >= maxGeneration;
92 }
93 };
94 }
95
96 /**
97 * Creates a termination condition that stops evolution after a specified time duration.
98 *
99 * <p>This time-based termination is useful for ensuring predictable execution times, especially in production
100 * environments or when computational resources are limited. The timer starts on the first evaluation and stops when
101 * the elapsed time exceeds the specified duration.
102 *
103 * @param <T> the type of fitness values in the evolutionary algorithm
104 * @param duration the maximum time to run the algorithm
105 * @return a termination condition that stops after the specified duration
106 * @throws IllegalArgumentException if duration is null
107 */
108 public static <T extends Comparable<T>> Termination<T> ofMaxTime(final Duration duration) {
109 Objects.requireNonNull(duration);
110
111 return new Termination<>() {
112
113 private final long durationNanos = duration.toNanos();
114 private Long startTime;
115
116 @Override
117 public boolean isDone(final AbstractEAConfiguration<T> eaConfiguration, final long generation,
118 final List<Genotype> population, final List<T> fitness) {
119 Validate.isTrue(generation >= 0);
120
121 final long nowNanos = System.nanoTime();
122
123 if (startTime == null) {
124 startTime = nowNanos;
125 }
126
127 return (nowNanos - startTime) >= durationNanos;
128 }
129 };
130 }
131
132 /**
133 * Creates a termination condition that requires ALL specified conditions to be met.
134 *
135 * <p>This logical AND operation creates a conservative termination strategy where evolution continues until every
136 * provided termination criterion is satisfied. Useful for ensuring multiple quality conditions are met before
137 * stopping.
138 *
139 * @param <T> the type of fitness values in the evolutionary algorithm
140 * @param terminations the termination conditions that must all be satisfied
141 * @return a termination condition that stops only when all conditions are met
142 * @throws IllegalArgumentException if terminations is null or empty
143 */
144 @SafeVarargs
145 public static <T extends Comparable<T>> Termination<T> and(final Termination<T>... terminations) {
146 Objects.requireNonNull(terminations);
147 Validate.isTrue(terminations.length > 0);
148
149 return new Termination<>() {
150
151 @Override
152 public boolean isDone(final AbstractEAConfiguration<T> eaConfiguration, final long generation,
153 final List<Genotype> population, final List<T> fitness) {
154 return Arrays.stream(terminations)
155 .allMatch(termination -> termination.isDone(eaConfiguration, generation, population, fitness));
156 }
157
158 };
159 }
160
161 /**
162 * Creates a termination condition that stops when ANY of the specified conditions is met.
163 *
164 * <p>This logical OR operation creates a flexible termination strategy where evolution stops as soon as any one of
165 * the provided criteria is satisfied. Commonly used to provide multiple stopping conditions like time limits,
166 * generation limits, or fitness thresholds.
167 *
168 * @param <T> the type of fitness values in the evolutionary algorithm
169 * @param terminations the termination conditions, any of which can trigger stopping
170 * @return a termination condition that stops when any condition is met
171 * @throws IllegalArgumentException if terminations is null or empty
172 */
173 @SafeVarargs
174 public static <T extends Comparable<T>> Termination<T> or(final Termination<T>... terminations) {
175 Objects.requireNonNull(terminations);
176 Validate.isTrue(terminations.length > 0);
177
178 return new Termination<>() {
179
180 @Override
181 public boolean isDone(final AbstractEAConfiguration<T> eaConfiguration, final long generation,
182 final List<Genotype> population, final List<T> fitness) {
183 return Arrays.stream(terminations)
184 .anyMatch(termination -> termination.isDone(eaConfiguration, generation, population, fitness));
185 }
186
187 };
188 }
189
190 /**
191 * Creates a termination condition that stops when any individual reaches a minimum fitness threshold.
192 *
193 * <p>This fitness-based termination is useful for maximization problems where you want to stop as soon as a solution
194 * of acceptable quality is found. The condition is satisfied when any individual in the population has a fitness
195 * value greater than or equal to the threshold.
196 *
197 * @param <T> the type of fitness values in the evolutionary algorithm
198 * @param threshold the minimum fitness value required to trigger termination
199 * @return a termination condition that stops when fitness reaches the threshold
200 * @throws IllegalArgumentException if threshold is null
201 */
202 public static <T extends Comparable<T>> Termination<T> ofFitnessAtLeast(final T threshold) {
203 Objects.requireNonNull(threshold);
204 return new Termination<>() {
205
206 @Override
207 public boolean isDone(final AbstractEAConfiguration<T> eaConfiguration, final long generation,
208 final List<Genotype> population, final List<T> fitness) {
209 Validate.isTrue(generation >= 0);
210
211 return fitness.stream().anyMatch(fitnessValue -> threshold.compareTo(fitnessValue) <= 0);
212 }
213 };
214 }
215
216 /**
217 * Creates a termination condition that stops when any individual reaches a maximum fitness threshold.
218 *
219 * <p>This fitness-based termination is useful for minimization problems where you want to stop as soon as a solution
220 * of acceptable quality is found. The condition is satisfied when any individual in the population has a fitness
221 * value less than or equal to the threshold.
222 *
223 * @param <T> the type of fitness values in the evolutionary algorithm
224 * @param threshold the maximum fitness value required to trigger termination
225 * @return a termination condition that stops when fitness reaches the threshold
226 * @throws IllegalArgumentException if threshold is null
227 */
228 public static <T extends Comparable<T>> Termination<T> ofFitnessAtMost(final T threshold) {
229 Objects.requireNonNull(threshold);
230 return new Termination<>() {
231
232 @Override
233 public boolean isDone(final AbstractEAConfiguration<T> eaConfiguration, final long generation,
234 final List<Genotype> population, final List<T> fitness) {
235 Validate.isTrue(generation >= 0);
236
237 return fitness.stream().anyMatch(fitnessValue -> threshold.compareTo(fitnessValue) >= 0);
238 }
239 };
240 }
241
242 /**
243 * Creates a termination condition that stops when fitness stops improving for a specified number of generations.
244 *
245 * <p>This convergence-based termination detects when the evolutionary algorithm has reached a stable state where
246 * further evolution is unlikely to yield significant improvements. It tracks the best fitness value and stops
247 * evolution if no improvement is observed for the specified number of consecutive generations.
248 *
249 * <p>This termination criterion is particularly useful for:
250 * <ul>
251 * <li>Preventing unnecessary computation when the algorithm has converged</li>
252 * <li>Automatically adapting to problem difficulty</li>
253 * <li>Research applications studying convergence behavior</li>
254 * </ul>
255 *
256 * @param <T> the type of fitness values in the evolutionary algorithm
257 * @param stableGenerationsCount the number of generations without improvement required to trigger termination
258 * @return a termination condition that stops when fitness plateaus
259 * @throws IllegalArgumentException if stableGenerationsCount is not positive
260 */
261 public static <T extends Comparable<T>> Termination<T> ofStableFitness(final int stableGenerationsCount) {
262 Validate.isTrue(stableGenerationsCount > 0);
263
264 return new Termination<>() {
265
266 private long lastImprovedGeneration = -1;
267 private T lastBestFitness = null;
268
269 @Override
270 public boolean isDone(final AbstractEAConfiguration<T> eaConfiguration, final long generation,
271 final List<Genotype> population, final List<T> fitness) {
272 Validate.isTrue(generation >= 0);
273
274 final Comparator<T> fitnessComparator = eaConfiguration.fitnessComparator();
275
276 final Optional<T> bestFitnessOpt = fitness.stream().max(fitnessComparator);
277
278 if (lastImprovedGeneration < 0
279 || bestFitnessOpt.map(bestFitness -> fitnessComparator.compare(bestFitness, lastBestFitness) > 0)
280 .orElse(false)) {
281 lastImprovedGeneration = generation;
282 lastBestFitness = bestFitnessOpt.get();
283 }
284 return generation - lastImprovedGeneration > stableGenerationsCount;
285 }
286 };
287 }
288 }