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 }