View Javadoc
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 }