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!");
		}
	}
}