EvolutionListenerLogTopN.java
package net.bmahe.genetics4j.core.evolutionlisteners;
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 org.apache.logging.log4j.Logger;
import net.bmahe.genetics4j.core.Genotype;
import net.bmahe.genetics4j.core.spec.AbstractEAConfiguration;
/**
* Evolution listener that logs the top N individuals from each generation.
*
* <p>This listener provides detailed logging of the best-performing individuals during evolution, allowing for
* real-time monitoring of algorithm progress and population quality. It supports configurable logging frequency, custom
* fitness comparators, and pretty-printing of genotypes.
*
* <p>Key features:
* <ul>
* <li><strong>Top-N logging</strong>: Configurable number of best individuals to log per generation</li>
* <li><strong>Skip functionality</strong>: Optional generation skipping to reduce log volume</li>
* <li><strong>Custom comparators</strong>: Override default fitness comparison logic</li>
* <li><strong>Pretty printing</strong>: Customizable genotype display formatting</li>
* <li><strong>Automatic sorting</strong>: Uses EA configuration's fitness comparator when none provided</li>
* </ul>
*
* <p>The listener automatically adapts to the fitness comparison strategy defined in the EA configuration during the
* {@code preEvaluation} phase. If a custom comparator is provided during construction, it will be used instead of the
* configuration's comparator.
*
* <p>Usage examples:
*
* <pre>{@code
* // Basic usage - log top 5 individuals every generation
* Logger logger = LogManager.getLogger();
* EvolutionListener<Double> topLogger = new EvolutionListenerLogTopN<>(logger, 5, 0);
*
* // Log top 3 individuals every 10 generations with custom formatting
* EvolutionListener<Double> periodicLogger = new EvolutionListenerLogTopN<>(logger,
* 3,
* 10,
* null,
* genotype -> "Custom: " + genotype.toString());
*
* // Use custom fitness comparator for minimization problems
* Comparator<Double> minimizer = Double::compare; // Natural order for minimization
* EvolutionListener<Double> minLogger = new EvolutionListenerLogTopN<>(logger, 5, 0, minimizer, null);
* }</pre>
*
* <p>Thread safety: This listener is thread-safe and can be used in parallel evolution contexts. The logger instance
* should be thread-safe (Log4j loggers are thread-safe by default).
*
* @param <T> the type of fitness values, must be Comparable
* @see EvolutionListener
* @see DefaultEvolutionListener
* @see SimpleEvolutionListener
*/
public class EvolutionListenerLogTopN<T extends Comparable<T>> implements EvolutionListener<T> {
private final Logger logger;
private final int topN;
private final int skipN;
private final Comparator<T> userComparator;
private Function<Genotype, String> prettyPrinter;
private AbstractEAConfiguration<T> eaConfiguration;
private Comparator<T> comparator;
/**
* Constructs a new EvolutionListenerLogTopN with full configuration options.
*
* @param _logger the Log4j logger instance to use for output
* @param _topN the number of top individuals to log each generation (must be > 0)
* @param _skipN the number of generations to skip between logging (0 = log every generation, must be >= 0)
* @param _userComparator custom fitness comparator for sorting individuals, or null to use EA configuration's
* comparator
* @param _prettyPrinter function to format genotype display, or null to use default toString()
* @throws NullPointerException if _logger is null
* @throws IllegalArgumentException if _topN <= 0 or _skipN < 0
*/
public EvolutionListenerLogTopN(final Logger _logger, final int _topN, final int _skipN,
final Comparator<T> _userComparator, final Function<Genotype, String> _prettyPrinter) {
Objects.requireNonNull(_logger);
Validate.isTrue(_topN > 0);
Validate.isTrue(_skipN >= 0);
this.logger = _logger;
this.topN = _topN;
this.skipN = _skipN;
this.userComparator = _userComparator;
this.prettyPrinter = _prettyPrinter != null ? _prettyPrinter : t -> t.toString();
}
/**
* Constructs a new EvolutionListenerLogTopN with default formatting and comparison.
*
* <p>This convenience constructor uses the EA configuration's fitness comparator and default toString() formatting
* for genotypes.
*
* @param _logger the Log4j logger instance to use for output
* @param _topN the number of top individuals to log each generation (must be > 0)
* @param _skipN the number of generations to skip between logging (0 = log every generation, must be >= 0)
* @throws NullPointerException if _logger is null
* @throws IllegalArgumentException if _topN <= 0 or _skipN < 0
*/
public EvolutionListenerLogTopN(final Logger _logger, final int _topN, final int _skipN) {
this(_logger, _topN, _skipN, null, null);
}
/**
* Initializes the listener with EA configuration and sets up the fitness comparator.
*
* <p>This method is called before evolution begins to provide access to the EA configuration. The listener uses this
* information to determine the appropriate fitness comparison strategy. If a custom comparator was provided during
* construction, it takes precedence over the configuration's comparator.
*
* <p>The resulting comparator is reversed to ensure that the "best" individuals (highest fitness according to the
* comparator) are sorted first in the top-N listing.
*
* @param eaConfiguration the EA configuration containing fitness comparison logic
* @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();
}
/**
* Logs the top N individuals for the current generation.
*
* <p>This method is called after each generation to log the best-performing individuals. The behavior is controlled
* by the skipN parameter - if skipN > 0, logging only occurs when the generation number is divisible by skipN.
*
* <p>The method sorts all individuals by fitness using the configured comparator and logs the top N results with
* their fitness values and formatted genotype representations.
*
* <p>Output format:
*
* <pre>
* Top {N} individuals at generation {generation}
* Fitness: {fitness_value} -> {formatted_genotype}
* Fitness: {fitness_value} -> {formatted_genotype}
* ...
* </pre>
*
* @param generation the current generation number (0-based)
* @param population the list of genotypes in the current generation
* @param fitness the list of fitness values corresponding to each genotype
* @param isDone whether the evolution has completed (not used by this implementation)
* @throws IllegalStateException if preEvaluation has not been called
*/
@Override
public void onEvolution(final long generation, final List<Genotype> population, final List<T> fitness,
final boolean isDone) {
Objects.requireNonNull(comparator);
if (skipN > 0 && generation % skipN != 0) {
return;
}
logger.info("Top {} individuals at generation {}", topN, generation);
IntStream.range(0, fitness.size())
.boxed()
.sorted((a, b) -> comparator.compare(fitness.get(a), fitness.get(b)))
.limit(topN)
.forEach((index) -> logger
.info(" Fitness: {} -> {}", fitness.get(index), this.prettyPrinter.apply(population.get(index))));
}
}