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))));

	}
}