1 package net.bmahe.genetics4j.core.evolutionlisteners;
2
3 import java.time.LocalDateTime;
4 import java.util.Comparator;
5 import java.util.List;
6 import java.util.Objects;
7 import java.util.function.Function;
8 import java.util.stream.IntStream;
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 * A simple default evolution listener that outputs progress to System.out without requiring external logging
17 * dependencies.
18 *
19 * <p>This implementation provides basic evolution monitoring by displaying generation information, population size,
20 * timestamp, thread information, and the best performing individuals based on fitness comparison. It's designed as a
21 * zero-dependency alternative to logging-based listeners for users who want immediate feedback without setting up
22 * logging frameworks.
23 *
24 * <p>The output format includes:
25 * <ul>
26 * <li>Timestamp and thread name for tracking execution context</li>
27 * <li>Generation number and population size</li>
28 * <li>Completion status when evolution finishes</li>
29 * <li>Top N individuals with their fitness values and genotype representations</li>
30 * </ul>
31 *
32 * <p>Features:
33 * <ul>
34 * <li>Zero external dependencies - uses System.out for output</li>
35 * <li>Works with any Comparable fitness type</li>
36 * <li>Configurable number of top individuals to display</li>
37 * <li>Optional generation skipping for reduced output frequency</li>
38 * <li>Customizable genotype pretty-printing</li>
39 * <li>Thread-safe implementation</li>
40 * <li>Automatic fitness ordering (respects EA configuration's fitness comparator)</li>
41 * <li>Custom comparator support for specialized fitness ordering</li>
42 * </ul>
43 *
44 * <p>Example usage:
45 *
46 * <pre>{@code
47 * // Basic usage - shows best individual each generation
48 * EvolutionListener<Double> listener = new DefaultEvolutionListener<>();
49 *
50 * // Show top 3 individuals every 10 generations
51 * EvolutionListener<Double> listener = new DefaultEvolutionListener<>(3, 10);
52 *
53 * // With custom genotype formatter
54 * EvolutionListener<Double> listener = new DefaultEvolutionListener<>(1,
55 * 0,
56 * null,
57 * genotype -> "Custom: " + genotype.toString());
58 *
59 * // With custom fitness comparator (e.g., for minimization problems)
60 * EvolutionListener<Double> listener = new DefaultEvolutionListener<>(1, 0, Comparator.reverseOrder(), null);
61 * }</pre>
62 *
63 * <p><strong>Thread Safety:</strong> This class is thread-safe and can be used in multi-threaded evolutionary algorithm
64 * executions. The output includes thread names to help identify concurrent executions.
65 *
66 * <p><strong>Performance Considerations:</strong> When using generation skipping ({@code skipN > 0}), the listener
67 * performs minimal work for skipped generations, making it suitable for long-running evolutions where frequent output
68 * is not desired.
69 *
70 * @param <T> the type of fitness values, must be Comparable
71 * @see EvolutionListener
72 * @see net.bmahe.genetics4j.core.spec.AbstractEAConfiguration#fitnessComparator()
73 * @since 4.0.0
74 */
75 public class DefaultEvolutionListener<T extends Comparable<T>> implements EvolutionListener<T> {
76
77 private final int topN;
78 private final int skipN;
79 private final Comparator<T> userComparator;
80 private final Function<Genotype, String> prettyPrinter;
81
82 private AbstractEAConfiguration<T> eaConfiguration;
83 private Comparator<T> comparator;
84
85 /**
86 * Creates a default evolution listener that shows the best individual each generation.
87 */
88 public DefaultEvolutionListener() {
89 this(1, 0, null, null);
90 }
91
92 /**
93 * Creates a default evolution listener that shows the top N individuals each generation.
94 *
95 * @param topN the number of top individuals to display (must be positive)
96 */
97 public DefaultEvolutionListener(final int topN) {
98 this(topN, 0, null, null);
99 }
100
101 /**
102 * Creates a default evolution listener with generation skipping.
103 *
104 * @param topN the number of top individuals to display (must be positive)
105 * @param skipN the number of generations to skip between outputs (0 = no skipping)
106 */
107 public DefaultEvolutionListener(final int topN, final int skipN) {
108 this(topN, skipN, null, null);
109 }
110
111 /**
112 * Creates a default evolution listener with custom genotype formatting.
113 *
114 * @param topN the number of top individuals to display (must be positive)
115 * @param prettyPrinter function to format genotypes for display (null uses toString())
116 */
117 public DefaultEvolutionListener(final int topN, final Function<Genotype, String> prettyPrinter) {
118 this(topN, 0, null, prettyPrinter);
119 }
120
121 /**
122 * Creates a fully configurable default evolution listener.
123 *
124 * @param topN the number of top individuals to display (must be positive). If larger than population size,
125 * all individuals will be shown.
126 * @param skipN the number of generations to skip between outputs (0 = no skipping). For example, skipN=2
127 * means display every 3rd generation.
128 * @param userComparator custom comparator for fitness values (null uses EA configuration ordering). This allows for
129 * custom fitness ordering, such as minimization problems using
130 * {@code Comparator.reverseOrder()}.
131 * @param prettyPrinter function to format genotypes for display (null uses {@code Genotype::toString}). Useful for
132 * custom genotype representations.
133 * @throws IllegalArgumentException if topN is not positive or skipN is negative
134 */
135 public DefaultEvolutionListener(final int topN,
136 final int skipN,
137 final Comparator<T> userComparator,
138 final Function<Genotype, String> prettyPrinter) {
139 Validate.isTrue(topN > 0, "topN must be positive");
140 Validate.isTrue(skipN >= 0, "skipN must be non-negative");
141
142 this.topN = topN;
143 this.skipN = skipN;
144 this.userComparator = userComparator;
145 this.prettyPrinter = prettyPrinter != null ? prettyPrinter : Genotype::toString;
146 }
147
148 /**
149 * {@inheritDoc}
150 *
151 * <p>Initializes the listener with the EA configuration and sets up the fitness comparator for ordering individuals.
152 * If a custom comparator was provided during construction, it takes precedence over the EA configuration's
153 * comparator.
154 *
155 * @param eaConfiguration the evolutionary algorithm configuration
156 * @throws NullPointerException if eaConfiguration is null
157 */
158 @Override
159 public void preEvaluation(final AbstractEAConfiguration<T> eaConfiguration) {
160 Objects.requireNonNull(eaConfiguration);
161
162 this.eaConfiguration = eaConfiguration;
163 this.comparator = (userComparator != null ? userComparator : eaConfiguration.fitnessComparator()).reversed();
164 }
165
166 /**
167 * {@inheritDoc}
168 *
169 * <p>Displays evolution progress including timestamp, thread name, generation number, population size, and the top
170 * performing individuals. Output is skipped for certain generations if skipN > 0 was configured during construction.
171 *
172 * <p>The method sorts individuals by fitness using the configured comparator and displays up to topN individuals.
173 * When evolution is complete (isDone=true), additional completion messages are shown.
174 *
175 * @param generation the current generation number
176 * @param population the current population of genotypes
177 * @param fitness the fitness values corresponding to the population
178 * @param isDone true if this is the final generation
179 * @throws NullPointerException if population, fitness, or eaConfiguration is null
180 * @throws IllegalStateException if preEvaluation was not called before this method
181 */
182 @Override
183 public void onEvolution(final long generation, final List<Genotype> population, final List<T> fitness,
184 final boolean isDone) {
185 Objects.requireNonNull(population, "population cannot be null");
186 Objects.requireNonNull(fitness, "fitness cannot be null");
187 Objects.requireNonNull(
188 comparator,
189 "comaparator cannot be null and likely due to eaConfiguration was not passed on initialization");
190
191 if (skipN > 0 && generation % (skipN + 1) != 0) {
192 return;
193 }
194
195 final String status = isDone ? " (COMPLETED)" : "";
196 System.out.printf(
197 "%s - [%s] - Generation %d: Population size: %d%s%n",
198 LocalDateTime.now(),
199 Thread.currentThread().getName(),
200 generation,
201 population.size(),
202 status);
203
204 if (fitness.isEmpty()) {
205 IO.println(" No individuals to display");
206 return;
207 }
208
209 final int displayCount = Math.min(topN, fitness.size());
210
211 if (displayCount == 1) {
212 IO.println("\tBest individual:");
213 } else {
214 System.out.printf("\tTop %d individuals:%n", displayCount);
215 }
216
217 IntStream.range(0, fitness.size())
218 .boxed()
219 .sorted((a, b) -> comparator.compare(fitness.get(a), fitness.get(b)))
220 .limit(displayCount)
221 .forEach(index -> {
222 final T fitnessValue = fitness.get(index);
223 final String genotypeStr = prettyPrinter.apply(population.get(index));
224 System.out.printf("\t\tFitness: %s -> %s%n", fitnessValue, genotypeStr);
225 });
226
227 if (isDone) {
228 IO.println("Evolution completed successfully!");
229 }
230 }
231 }