DefaultEvolutionListener.java
package net.bmahe.genetics4j.core.evolutionlisteners;
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.IntStream;
import org.apache.commons.lang3.Validate;
import net.bmahe.genetics4j.core.Genotype;
import net.bmahe.genetics4j.core.spec.AbstractEAConfiguration;
/**
* A simple default evolution listener that outputs progress to System.out without requiring external logging
* dependencies.
*
* <p>This implementation provides basic evolution monitoring by displaying generation information, population size,
* timestamp, thread information, and the best performing individuals based on fitness comparison. It's designed as a
* zero-dependency alternative to logging-based listeners for users who want immediate feedback without setting up
* logging frameworks.
*
* <p>The output format includes:
* <ul>
* <li>Timestamp and thread name for tracking execution context</li>
* <li>Generation number and population size</li>
* <li>Completion status when evolution finishes</li>
* <li>Top N individuals with their fitness values and genotype representations</li>
* </ul>
*
* <p>Features:
* <ul>
* <li>Zero external dependencies - uses System.out for output</li>
* <li>Works with any Comparable fitness type</li>
* <li>Configurable number of top individuals to display</li>
* <li>Optional generation skipping for reduced output frequency</li>
* <li>Customizable genotype pretty-printing</li>
* <li>Thread-safe implementation</li>
* <li>Automatic fitness ordering (respects EA configuration's fitness comparator)</li>
* <li>Custom comparator support for specialized fitness ordering</li>
* </ul>
*
* <p>Example usage:
*
* <pre>{@code
* // Basic usage - shows best individual each generation
* EvolutionListener<Double> listener = new DefaultEvolutionListener<>();
*
* // Show top 3 individuals every 10 generations
* EvolutionListener<Double> listener = new DefaultEvolutionListener<>(3, 10);
*
* // With custom genotype formatter
* EvolutionListener<Double> listener = new DefaultEvolutionListener<>(1,
* 0,
* null,
* genotype -> "Custom: " + genotype.toString());
*
* // With custom fitness comparator (e.g., for minimization problems)
* EvolutionListener<Double> listener = new DefaultEvolutionListener<>(1, 0, Comparator.reverseOrder(), null);
* }</pre>
*
* <p><strong>Thread Safety:</strong> This class is thread-safe and can be used in multi-threaded evolutionary algorithm
* executions. The output includes thread names to help identify concurrent executions.
*
* <p><strong>Performance Considerations:</strong> When using generation skipping ({@code skipN > 0}), the listener
* performs minimal work for skipped generations, making it suitable for long-running evolutions where frequent output
* is not desired.
*
* @param <T> the type of fitness values, must be Comparable
* @see EvolutionListener
* @see net.bmahe.genetics4j.core.spec.AbstractEAConfiguration#fitnessComparator()
* @since 4.0.0
*/
public class DefaultEvolutionListener<T extends Comparable<T>> implements EvolutionListener<T> {
private final int topN;
private final int skipN;
private final Comparator<T> userComparator;
private final Function<Genotype, String> prettyPrinter;
private AbstractEAConfiguration<T> eaConfiguration;
private Comparator<T> comparator;
/**
* Creates a default evolution listener that shows the best individual each generation.
*/
public DefaultEvolutionListener() {
this(1, 0, null, null);
}
/**
* Creates a default evolution listener that shows the top N individuals each generation.
*
* @param topN the number of top individuals to display (must be positive)
*/
public DefaultEvolutionListener(final int topN) {
this(topN, 0, null, null);
}
/**
* Creates a default evolution listener with generation skipping.
*
* @param topN the number of top individuals to display (must be positive)
* @param skipN the number of generations to skip between outputs (0 = no skipping)
*/
public DefaultEvolutionListener(final int topN, final int skipN) {
this(topN, skipN, null, null);
}
/**
* Creates a default evolution listener with custom genotype formatting.
*
* @param topN the number of top individuals to display (must be positive)
* @param prettyPrinter function to format genotypes for display (null uses toString())
*/
public DefaultEvolutionListener(final int topN, final Function<Genotype, String> prettyPrinter) {
this(topN, 0, null, prettyPrinter);
}
/**
* Creates a fully configurable default evolution listener.
*
* @param topN the number of top individuals to display (must be positive). If larger than population size,
* all individuals will be shown.
* @param skipN the number of generations to skip between outputs (0 = no skipping). For example, skipN=2
* means display every 3rd generation.
* @param userComparator custom comparator for fitness values (null uses EA configuration ordering). This allows for
* custom fitness ordering, such as minimization problems using
* {@code Comparator.reverseOrder()}.
* @param prettyPrinter function to format genotypes for display (null uses {@code Genotype::toString}). Useful for
* custom genotype representations.
* @throws IllegalArgumentException if topN is not positive or skipN is negative
*/
public DefaultEvolutionListener(final int topN, final int skipN, final Comparator<T> userComparator,
final Function<Genotype, String> prettyPrinter) {
Validate.isTrue(topN > 0, "topN must be positive");
Validate.isTrue(skipN >= 0, "skipN must be non-negative");
this.topN = topN;
this.skipN = skipN;
this.userComparator = userComparator;
this.prettyPrinter = prettyPrinter != null ? prettyPrinter : Genotype::toString;
}
/**
* {@inheritDoc}
*
* <p>Initializes the listener with the EA configuration and sets up the fitness comparator for ordering individuals.
* If a custom comparator was provided during construction, it takes precedence over the EA configuration's
* comparator.
*
* @param eaConfiguration the evolutionary algorithm configuration
* @throws NullPointerException if eaConfiguration is null
*/
@Override
public void preEvaluation(final AbstractEAConfiguration<T> eaConfiguration) {
Objects.requireNonNull(eaConfiguration);
this.eaConfiguration = eaConfiguration;
this.comparator = (userComparator != null ? userComparator : eaConfiguration.fitnessComparator()).reversed();
}
/**
* {@inheritDoc}
*
* <p>Displays evolution progress including timestamp, thread name, generation number, population size, and the top
* performing individuals. Output is skipped for certain generations if skipN > 0 was configured during construction.
*
* <p>The method sorts individuals by fitness using the configured comparator and displays up to topN individuals.
* When evolution is complete (isDone=true), additional completion messages are shown.
*
* @param generation the current generation number
* @param population the current population of genotypes
* @param fitness the fitness values corresponding to the population
* @param isDone true if this is the final generation
* @throws NullPointerException if population, fitness, or eaConfiguration is null
* @throws IllegalStateException if preEvaluation was not called before this method
*/
@Override
public void onEvolution(final long generation, final List<Genotype> population, final List<T> fitness,
final boolean isDone) {
Objects.requireNonNull(population, "population cannot be null");
Objects.requireNonNull(fitness, "fitness cannot be null");
Objects.requireNonNull(comparator,
"comaparator cannot be null and likely due to eaConfiguration was not passed on initialization");
if (skipN > 0 && generation % (skipN + 1) != 0) {
return;
}
final String status = isDone ? " (COMPLETED)" : "";
System.out.printf("%s - [%s] - Generation %d: Population size: %d%s%n",
LocalDateTime.now(),
Thread.currentThread()
.getName(),
generation,
population.size(),
status);
if (fitness.isEmpty()) {
System.out.println(" No individuals to display");
return;
}
final int displayCount = Math.min(topN, fitness.size());
if (displayCount == 1) {
System.out.println("\tBest individual:");
} else {
System.out.printf("\tTop %d individuals:%n", displayCount);
}
IntStream.range(0, fitness.size())
.boxed()
.sorted((a, b) -> comparator.compare(fitness.get(a), fitness.get(b)))
.limit(displayCount)
.forEach(index -> {
final T fitnessValue = fitness.get(index);
final String genotypeStr = prettyPrinter.apply(population.get(index));
System.out.printf("\t\tFitness: %s -> %s%n", fitnessValue, genotypeStr);
});
if (isDone) {
System.out.println("Evolution completed successfully!");
}
}
}