View Javadoc
1   package net.bmahe.genetics4j.core.evolutionlisteners;
2   
3   import java.util.Comparator;
4   import java.util.List;
5   import java.util.Objects;
6   import java.util.function.Function;
7   import java.util.stream.IntStream;
8   
9   import org.apache.commons.lang3.Validate;
10  import org.apache.logging.log4j.Logger;
11  
12  import net.bmahe.genetics4j.core.Genotype;
13  import net.bmahe.genetics4j.core.spec.AbstractEAConfiguration;
14  
15  /**
16   * Evolution listener that logs the top N individuals from each generation.
17   *
18   * <p>This listener provides detailed logging of the best-performing individuals during evolution, allowing for
19   * real-time monitoring of algorithm progress and population quality. It supports configurable logging frequency, custom
20   * fitness comparators, and pretty-printing of genotypes.
21   *
22   * <p>Key features:
23   * <ul>
24   * <li><strong>Top-N logging</strong>: Configurable number of best individuals to log per generation</li>
25   * <li><strong>Skip functionality</strong>: Optional generation skipping to reduce log volume</li>
26   * <li><strong>Custom comparators</strong>: Override default fitness comparison logic</li>
27   * <li><strong>Pretty printing</strong>: Customizable genotype display formatting</li>
28   * <li><strong>Automatic sorting</strong>: Uses EA configuration's fitness comparator when none provided</li>
29   * </ul>
30   *
31   * <p>The listener automatically adapts to the fitness comparison strategy defined in the EA configuration during the
32   * {@code preEvaluation} phase. If a custom comparator is provided during construction, it will be used instead of the
33   * configuration's comparator.
34   *
35   * <p>Usage examples:
36   *
37   * <pre>{@code
38   * // Basic usage - log top 5 individuals every generation
39   * Logger logger = LogManager.getLogger();
40   * EvolutionListener<Double> topLogger = new EvolutionListenerLogTopN<>(logger, 5, 0);
41   *
42   * // Log top 3 individuals every 10 generations with custom formatting
43   * EvolutionListener<Double> periodicLogger = new EvolutionListenerLogTopN<>(logger,
44   * 		3,
45   * 		10,
46   * 		null,
47   * 		genotype -> "Custom: " + genotype.toString());
48   *
49   * // Use custom fitness comparator for minimization problems
50   * Comparator<Double> minimizer = Double::compare; // Natural order for minimization
51   * EvolutionListener<Double> minLogger = new EvolutionListenerLogTopN<>(logger, 5, 0, minimizer, null);
52   * }</pre>
53   *
54   * <p>Thread safety: This listener is thread-safe and can be used in parallel evolution contexts. The logger instance
55   * should be thread-safe (Log4j loggers are thread-safe by default).
56   *
57   * @param <T> the type of fitness values, must be Comparable
58   * @see EvolutionListener
59   * @see DefaultEvolutionListener
60   * @see SimpleEvolutionListener
61   */
62  public class EvolutionListenerLogTopN<T extends Comparable<T>> implements EvolutionListener<T> {
63  
64  	private final Logger logger;
65  	private final int topN;
66  	private final int skipN;
67  	private final Comparator<T> userComparator;
68  
69  	private Function<Genotype, String> prettyPrinter;
70  	private AbstractEAConfiguration<T> eaConfiguration;
71  	private Comparator<T> comparator;
72  
73  	/**
74  	 * Constructs a new EvolutionListenerLogTopN with full configuration options.
75  	 *
76  	 * @param _logger         the Log4j logger instance to use for output
77  	 * @param _topN           the number of top individuals to log each generation (must be > 0)
78  	 * @param _skipN          the number of generations to skip between logging (0 = log every generation, must be >= 0)
79  	 * @param _userComparator custom fitness comparator for sorting individuals, or null to use EA configuration's
80  	 *                        comparator
81  	 * @param _prettyPrinter  function to format genotype display, or null to use default toString()
82  	 * @throws NullPointerException     if _logger is null
83  	 * @throws IllegalArgumentException if _topN <= 0 or _skipN < 0
84  	 */
85  	public EvolutionListenerLogTopN(final Logger _logger,
86  			final int _topN,
87  			final int _skipN,
88  			final Comparator<T> _userComparator,
89  			final Function<Genotype, String> _prettyPrinter) {
90  		Objects.requireNonNull(_logger);
91  		Validate.isTrue(_topN > 0);
92  		Validate.isTrue(_skipN >= 0);
93  
94  		this.logger = _logger;
95  		this.topN = _topN;
96  		this.skipN = _skipN;
97  		this.userComparator = _userComparator;
98  		this.prettyPrinter = _prettyPrinter != null ? _prettyPrinter : Genotype::toString;
99  	}
100 
101 	/**
102 	 * Constructs a new EvolutionListenerLogTopN with default formatting and comparison.
103 	 *
104 	 * <p>This convenience constructor uses the EA configuration's fitness comparator and default toString() formatting
105 	 * for genotypes.
106 	 *
107 	 * @param _logger the Log4j logger instance to use for output
108 	 * @param _topN   the number of top individuals to log each generation (must be > 0)
109 	 * @param _skipN  the number of generations to skip between logging (0 = log every generation, must be >= 0)
110 	 * @throws NullPointerException     if _logger is null
111 	 * @throws IllegalArgumentException if _topN <= 0 or _skipN < 0
112 	 */
113 	public EvolutionListenerLogTopN(final Logger _logger, final int _topN, final int _skipN) {
114 		this(_logger, _topN, _skipN, null, null);
115 	}
116 
117 	/**
118 	 * Initializes the listener with EA configuration and sets up the fitness comparator.
119 	 *
120 	 * <p>This method is called before evolution begins to provide access to the EA configuration. The listener uses this
121 	 * information to determine the appropriate fitness comparison strategy. If a custom comparator was provided during
122 	 * construction, it takes precedence over the configuration's comparator.
123 	 *
124 	 * <p>The resulting comparator is reversed to ensure that the "best" individuals (highest fitness according to the
125 	 * comparator) are sorted first in the top-N listing.
126 	 *
127 	 * @param eaConfiguration the EA configuration containing fitness comparison logic
128 	 * @throws NullPointerException if eaConfiguration is null
129 	 */
130 	@Override
131 	public void preEvaluation(final AbstractEAConfiguration<T> eaConfiguration) {
132 		Objects.requireNonNull(eaConfiguration);
133 
134 		this.eaConfiguration = eaConfiguration;
135 		this.comparator = (userComparator != null ? userComparator : eaConfiguration.fitnessComparator()).reversed();
136 
137 	}
138 
139 	/**
140 	 * Logs the top N individuals for the current generation.
141 	 *
142 	 * <p>This method is called after each generation to log the best-performing individuals. The behavior is controlled
143 	 * by the skipN parameter - if skipN > 0, logging only occurs when the generation number is divisible by skipN.
144 	 *
145 	 * <p>The method sorts all individuals by fitness using the configured comparator and logs the top N results with
146 	 * their fitness values and formatted genotype representations.
147 	 *
148 	 * <p>Output format:
149 	 * 
150 	 * <pre>
151 	 * Top {N} individuals at generation {generation}
152 	 *   Fitness: {fitness_value} -> {formatted_genotype}
153 	 *   Fitness: {fitness_value} -> {formatted_genotype}
154 	 *   ...
155 	 * </pre>
156 	 *
157 	 * @param generation the current generation number (0-based)
158 	 * @param population the list of genotypes in the current generation
159 	 * @param fitness    the list of fitness values corresponding to each genotype
160 	 * @param isDone     whether the evolution has completed (not used by this implementation)
161 	 * @throws IllegalStateException if preEvaluation has not been called
162 	 */
163 	@Override
164 	public void onEvolution(final long generation, final List<Genotype> population, final List<T> fitness,
165 			final boolean isDone) {
166 		Objects.requireNonNull(comparator);
167 
168 		if (skipN > 0 && generation % skipN != 0) {
169 			return;
170 		}
171 
172 		logger.info("Top {} individuals at generation {}", topN, generation);
173 		IntStream.range(0, fitness.size())
174 				.boxed()
175 				.sorted((a, b) -> comparator.compare(fitness.get(a), fitness.get(b)))
176 				.limit(topN)
177 				.forEach(
178 						index -> logger.info(
179 								"  Fitness: {} -> {}",
180 								fitness.get(index),
181 								this.prettyPrinter.apply(population.get(index))));
182 
183 	}
184 }