1 package net.bmahe.genetics4j.neat.combination;
2
3 import java.util.ArrayList;
4 import java.util.Comparator;
5 import java.util.HashMap;
6 import java.util.HashSet;
7 import java.util.List;
8 import java.util.Map;
9 import java.util.Objects;
10 import java.util.Set;
11 import java.util.random.RandomGenerator;
12
13 import org.apache.commons.lang3.Validate;
14 import org.apache.logging.log4j.LogManager;
15 import org.apache.logging.log4j.Logger;
16
17 import net.bmahe.genetics4j.core.chromosomes.Chromosome;
18 import net.bmahe.genetics4j.core.combination.ChromosomeCombinator;
19 import net.bmahe.genetics4j.core.spec.AbstractEAConfiguration;
20 import net.bmahe.genetics4j.neat.Connection;
21 import net.bmahe.genetics4j.neat.chromosomes.NeatChromosome;
22 import net.bmahe.genetics4j.neat.combination.parentcompare.ChosenOtherChromosome;
23 import net.bmahe.genetics4j.neat.combination.parentcompare.ParentComparisonHandler;
24 import net.bmahe.genetics4j.neat.spec.combination.NeatCombination;
25 import net.bmahe.genetics4j.neat.spec.combination.parentcompare.ParentComparisonPolicy;
26
27 /**
28 * Implements genetic crossover for NEAT (NeuroEvolution of Augmenting Topologies) neural network chromosomes.
29 *
30 * <p>NeatChromosomeCombinator performs innovation-number-based genetic recombination between two neural network
31 * chromosomes, creating offspring that inherit network topology and connection weights from both parents while
32 * preserving the historical tracking essential to the NEAT algorithm.
33 *
34 * <p>NEAT crossover algorithm:
35 * <ol>
36 * <li><strong>Parent comparison</strong>: Determine which parent is "fitter" using comparison policy</li>
37 * <li><strong>Gene alignment</strong>: Match connections by innovation number between parents</li>
38 * <li><strong>Matching genes</strong>: Randomly inherit from either parent (biased by inheritance threshold)</li>
39 * <li><strong>Disjoint genes</strong>: Inherit from fitter parent when innovation ranges overlap</li>
40 * <li><strong>Excess genes</strong>: Inherit from fitter parent beyond other parent's range</li>
41 * <li><strong>Gene re-enabling</strong>: Potentially re-enable disabled genes based on threshold</li>
42 * </ol>
43 *
44 * <p>Key genetic operations:
45 * <ul>
46 * <li><strong>Innovation alignment</strong>: Uses innovation numbers to match corresponding genes</li>
47 * <li><strong>Fitness-biased inheritance</strong>: Favors genes from fitter parent based on inheritance threshold</li>
48 * <li><strong>Gene state management</strong>: Handles enabled/disabled connection states during crossover</li>
49 * <li><strong>Topology preservation</strong>: Ensures offspring have valid network topology</li>
50 * </ul>
51 *
52 * <p>Gene classification:
53 * <ul>
54 * <li><strong>Matching genes</strong>: Same innovation number in both parents, inherit randomly</li>
55 * <li><strong>Disjoint genes</strong>: Innovation number exists in one parent within other's range</li>
56 * <li><strong>Excess genes</strong>: Innovation number beyond other parent's highest innovation</li>
57 * <li><strong>Disabled genes</strong>: May be re-enabled if other parent has enabled version</li>
58 * </ul>
59 *
60 * <p>Common usage patterns:
61 *
62 * <pre>{@code
63 * // Create NEAT chromosome combinator
64 * RandomGenerator randomGen = RandomGenerator.getDefault();
65 * NeatCombination policy = NeatCombination.builder()
66 * .inheritanceThresold(0.7) // 70% bias toward fitter parent
67 * .reenableGeneInheritanceThresold(0.25) // 25% gene re-enabling chance
68 * .parentComparisonPolicy(FitnessComparison.build())
69 * .build();
70 *
71 * ParentComparisonHandler comparisonHandler = new FitnessComparisonHandler();
72 * NeatChromosomeCombinator<Double> combinator = new NeatChromosomeCombinator<>(
73 * randomGen, policy, comparisonHandler
74 * );
75 *
76 * // Perform crossover
77 * NeatChromosome parent1 = // ... first parent
78 * NeatChromosome parent2 = // ... second parent
79 * Double fitness1 = 0.85;
80 * Double fitness2 = 0.72;
81 *
82 * List<Chromosome> offspring = combinator.combine(
83 * eaConfiguration, parent1, fitness1, parent2, fitness2
84 * );
85 * NeatChromosome child = (NeatChromosome) offspring.get(0);
86 * }</pre>
87 *
88 * <p>Inheritance threshold effects:
89 * <ul>
90 * <li><strong>0.5</strong>: Unbiased inheritance, equal probability from both parents</li>
91 * <li><strong>> 0.5</strong>: Bias toward fitter parent, promotes convergence</li>
92 * <li><strong>< 0.5</strong>: Bias toward less fit parent, increases diversity</li>
93 * <li><strong>1.0</strong>: Always inherit from fitter parent (when fitness differs)</li>
94 * </ul>
95 *
96 * <p>Gene re-enabling mechanism:
97 * <ul>
98 * <li><strong>Preservation</strong>: Disabled genes maintain connection topology information</li>
99 * <li><strong>Re-activation</strong>: Chance to re-enable genes that are enabled in other parent</li>
100 * <li><strong>Exploration</strong>: Allows rediscovery of previously disabled connection patterns</li>
101 * <li><strong>Genetic diversity</strong>: Prevents permanent loss of structural information</li>
102 * </ul>
103 *
104 * <p>Duplicate connection prevention:
105 * <ul>
106 * <li><strong>Links cache</strong>: Tracks already included connections to prevent duplicates</li>
107 * <li><strong>Topology validation</strong>: Ensures each connection appears at most once</li>
108 * <li><strong>Cache efficiency</strong>: O(1) lookup for connection existence checking</li>
109 * <li><strong>Memory management</strong>: Cache cleared after each crossover operation</li>
110 * </ul>
111 *
112 * <p>Performance considerations:
113 * <ul>
114 * <li><strong>Linear time complexity</strong>: O(n + m) where n, m are parent connection counts</li>
115 * <li><strong>Innovation sorting</strong>: Leverages pre-sorted connection lists for efficiency</li>
116 * <li><strong>Memory efficiency</strong>: Minimal allocation during crossover</li>
117 * <li><strong>Cache optimization</strong>: Efficient duplicate detection and prevention</li>
118 * </ul>
119 *
120 * @param <T> the fitness value type (typically Double)
121 * @see NeatCombination
122 * @see ParentComparisonHandler
123 * @see NeatChromosome
124 * @see ChromosomeCombinator
125 */
126 public class NeatChromosomeCombinator<T extends Comparable<T>> implements ChromosomeCombinator<T> {
127 public static final Logger logger = LogManager.getLogger(NeatChromosomeCombinator.class);
128
129 private final RandomGenerator randomGenerator;
130 private final NeatCombination neatCombination;
131 private final ParentComparisonHandler parentComparisonHandler;
132
133 /**
134 * Checks whether a connection already exists in the links cache.
135 *
136 * <p>The links cache prevents duplicate connections in the offspring by tracking all connections that have already
137 * been added. This ensures each connection appears at most once in the resulting chromosome.
138 *
139 * @param linksCache cache mapping from-node indices to sets of to-node indices
140 * @param connection connection to check for existence
141 * @return true if connection already exists in cache, false otherwise
142 * @throws IllegalArgumentException if linksCache or connection is null
143 */
144 private boolean linksCacheContainsConnection(final Map<Integer, Set<Integer>> linksCache,
145 final Connection connection) {
146 Objects.requireNonNull(linksCache);
147 Objects.requireNonNull(connection);
148
149 final int fromNodeIndex = connection.fromNodeIndex();
150 final int toNodeIndex = connection.toNodeIndex();
151
152 return linksCache.containsKey(fromNodeIndex) == true
153 && linksCache.get(fromNodeIndex).contains(toNodeIndex) == true;
154 }
155
156 /**
157 * Adds a connection to the links cache to prevent future duplicates.
158 *
159 * <p>This method records that a connection from the specified source to target node has been added to the offspring,
160 * preventing the same connection from being added again during the crossover process.
161 *
162 * @param linksCache cache mapping from-node indices to sets of to-node indices
163 * @param connection connection to add to the cache
164 * @throws IllegalArgumentException if linksCache or connection is null
165 */
166 private void insertInlinksCache(final Map<Integer, Set<Integer>> linksCache, final Connection connection) {
167 Objects.requireNonNull(linksCache);
168 Objects.requireNonNull(connection);
169
170 final int fromNodeIndex = connection.fromNodeIndex();
171 final int toNodeIndex = connection.toNodeIndex();
172
173 linksCache.computeIfAbsent(fromNodeIndex, k -> new HashSet<>()).add(toNodeIndex);
174 }
175
176 /**
177 * Determines whether a disabled gene should be re-enabled during crossover.
178 *
179 * <p>If the chosen parent has a disabled connection but the other parent has the same connection enabled, there is a
180 * configurable chance to re-enable the connection in the offspring. This mechanism prevents permanent loss of
181 * potentially useful connections.
182 *
183 * @param chosenParent the connection selected for inheritance
184 * @param otherParent the corresponding connection from the other parent
185 * @return true if the disabled connection should be re-enabled, false otherwise
186 * @throws IllegalArgumentException if either connection is null
187 */
188 protected boolean shouldReEnable(final Connection chosenParent, final Connection otherParent) {
189 Objects.requireNonNull(chosenParent);
190 Objects.requireNonNull(otherParent);
191
192 boolean shouldReEnable = false;
193 if (chosenParent.isEnabled() == false && otherParent.isEnabled() == true) {
194 if (randomGenerator.nextDouble() < neatCombination.reenableGeneInheritanceThresold()) {
195 shouldReEnable = true;
196 }
197 }
198
199 return shouldReEnable;
200 }
201
202 /**
203 * Constructs a new NEAT chromosome combinator with the specified components.
204 *
205 * <p>The combinator uses the random generator for stochastic decisions during crossover, the combination policy for
206 * inheritance parameters, and the comparison handler for determining parent fitness relationships.
207 *
208 * @param _randomGenerator random number generator for stochastic crossover decisions
209 * @param _neatCombination crossover policy defining inheritance parameters
210 * @param _parentComparisonHandler handler for comparing parent fitness and determining inheritance bias
211 * @throws IllegalArgumentException if any parameter is null
212 */
213 public NeatChromosomeCombinator(final RandomGenerator _randomGenerator,
214 final NeatCombination _neatCombination,
215 final ParentComparisonHandler _parentComparisonHandler) {
216 Objects.requireNonNull(_randomGenerator);
217 Objects.requireNonNull(_neatCombination);
218 Objects.requireNonNull(_parentComparisonHandler);
219
220 this.randomGenerator = _randomGenerator;
221 this.neatCombination = _neatCombination;
222 this.parentComparisonHandler = _parentComparisonHandler;
223 }
224
225 /**
226 * Performs genetic crossover between two NEAT chromosomes to produce offspring.
227 *
228 * <p>This method implements the NEAT crossover algorithm, aligning genes by innovation number and applying
229 * inheritance rules based on parent fitness and configuration parameters. The result is a single offspring
230 * chromosome that inherits network topology and connection weights from both parents.
231 *
232 * <p>Crossover process:
233 * <ol>
234 * <li>Compare parent fitness to determine inheritance bias</li>
235 * <li>Align genes by innovation number between parents</li>
236 * <li>Process matching genes with random inheritance (biased)</li>
237 * <li>Process disjoint genes based on fitness comparison</li>
238 * <li>Process excess genes from fitter parent</li>
239 * <li>Apply gene re-enabling rules for disabled connections</li>
240 * </ol>
241 *
242 * @param eaConfiguration evolutionary algorithm configuration containing fitness comparator
243 * @param firstChromosome first parent chromosome (must be NeatChromosome)
244 * @param firstParentFitness fitness value of first parent
245 * @param secondChromosome second parent chromosome (must be NeatChromosome)
246 * @param secondParentFitness fitness value of second parent
247 * @return list containing single offspring chromosome
248 * @throws IllegalArgumentException if chromosomes are not NeatChromosome instances or any parameter is null
249 */
250 @Override
251 public List<Chromosome> combine(final AbstractEAConfiguration<T> eaConfiguration, final Chromosome firstChromosome,
252 final T firstParentFitness, final Chromosome secondChromosome, final T secondParentFitness) {
253 Objects.requireNonNull(eaConfiguration);
254 Objects.requireNonNull(firstChromosome);
255 Objects.requireNonNull(firstParentFitness);
256 Validate.isInstanceOf(NeatChromosome.class, firstChromosome);
257 Objects.requireNonNull(secondChromosome);
258 Objects.requireNonNull(secondParentFitness);
259 Validate.isInstanceOf(NeatChromosome.class, secondChromosome);
260
261 final NeatChromosome firstNeatChromosome = (NeatChromosome) firstChromosome;
262 final NeatChromosome secondNeatChromosome = (NeatChromosome) secondChromosome;
263 Validate.isTrue(
264 firstNeatChromosome.getNodeLayout().isCompatibleWith(secondNeatChromosome.getNodeLayout()),
265 "Cannot combine NEAT chromosomes with incompatible node layouts");
266 final Comparator<T> fitnessComparator = eaConfiguration.fitnessComparator();
267 final double inheritanceThresold = neatCombination.inheritanceThresold();
268 final ParentComparisonPolicy parentComparisonPolicy = neatCombination.parentComparisonPolicy();
269
270 final int fitnessComparison = fitnessComparator.compare(firstParentFitness, secondParentFitness);
271 final ChosenOtherChromosome comparedChromosomes = parentComparisonHandler
272 .compare(parentComparisonPolicy, firstNeatChromosome, secondNeatChromosome, fitnessComparison);
273 final NeatChromosome bestChromosome = comparedChromosomes.chosen();
274 final NeatChromosome worstChromosome = comparedChromosomes.other();
275
276 final List<Connection> combinedConnections = new ArrayList<>();
277 final Map<Integer, Set<Integer>> linksCache = new HashMap<>();
278
279 final var bestConnections = bestChromosome.getConnections();
280 final var worstConnections = worstChromosome.getConnections();
281
282 int indexBest = 0;
283 int indexWorst = 0;
284
285 while (indexBest < bestConnections.size() && indexWorst < worstConnections.size()) {
286
287 final var bestConnection = bestConnections.get(indexBest);
288 final var worstConnection = worstConnections.get(indexWorst);
289
290 if (bestConnection.innovation() == worstConnection.innovation()) {
291 /**
292 * If innovation is the same, we pick the connection randomly
293 */
294 var original = bestConnection;
295 var other = worstConnection;
296 if (randomGenerator.nextDouble() < 1 - inheritanceThresold) {
297 original = worstConnection;
298 other = bestConnection;
299 }
300 if (linksCacheContainsConnection(linksCache, original) == false) {
301
302 /**
303 * If the chosen gene is disabled but the other one is enabled, then there is a chance we will re-enable
304 * it
305 */
306 final boolean isEnabled = shouldReEnable(original, other) ? true : original.isEnabled();
307
308 final var childConnection = Connection.builder().from(original).isEnabled(isEnabled).build();
309 combinedConnections.add(childConnection);
310 insertInlinksCache(linksCache, original);
311 }
312 indexBest++;
313 indexWorst++;
314 } else if (bestConnection.innovation() > worstConnection.innovation()) {
315
316 /**
317 * If the fitnesses are equal, then we randomly inherit from the parent Otherwise, we do not inherit from
318 * the lesser gene
319 */
320 if (fitnessComparison == 0 && randomGenerator.nextDouble() < 1.0 - inheritanceThresold) {
321 final var original = worstConnection;
322 if (linksCacheContainsConnection(linksCache, original) == false) {
323 combinedConnections.add(Connection.copyOf(original));
324 insertInlinksCache(linksCache, original);
325 }
326 }
327
328 indexWorst++;
329 } else {
330
331 /**
332 * If the fitnesses are equal, then we randomly inherit from the parent Otherwise, we always inherit from
333 * the better gene
334 */
335
336 if (fitnessComparison != 0 || randomGenerator.nextDouble() < inheritanceThresold) {
337 if (linksCacheContainsConnection(linksCache, bestConnection) == false) {
338 combinedConnections.add(Connection.copyOf(bestConnection));
339 insertInlinksCache(linksCache, bestConnection);
340 }
341 }
342 indexBest++;
343 }
344 }
345
346 /*
347 * Case where the best connection has more genes. It's called excess genes
348 */
349 while (indexBest < bestConnections.size()) {
350 /**
351 * If the fitnesses are equal, then we randomly inherit from the parent Otherwise, we always inherit from the
352 * better gene
353 */
354 if (fitnessComparison != 0 || randomGenerator.nextDouble() < inheritanceThresold) {
355 final var bestConnection = bestConnections.get(indexBest);
356 if (linksCacheContainsConnection(linksCache, bestConnection) == false) {
357 combinedConnections.add(Connection.copyOf(bestConnection));
358 insertInlinksCache(linksCache, bestConnection);
359 }
360
361 }
362 indexBest++;
363 }
364
365 /*
366 * Case where the worst connection has more genes. It's called excess genes. Since we don't inherit when their
367 * fitness aren't equal, it means we can skip the excess genes from the weaker connections. However we will
368 * randomly inherit if their fitnesses are equal
369 */
370 while (fitnessComparison == 0 && indexWorst < worstConnections.size()) {
371 if (randomGenerator.nextDouble() < 1.0 - inheritanceThresold) {
372 final var worstConnection = worstConnections.get(indexWorst);
373 if (linksCacheContainsConnection(linksCache, worstConnection) == false) {
374 combinedConnections.add(Connection.copyOf(worstConnection));
375 insertInlinksCache(linksCache, worstConnection);
376 }
377
378 }
379 indexWorst++;
380 }
381
382 return List.of(
383 new NeatChromosome(bestChromosome.getNodeLayout(),
384 bestChromosome.getMinWeightValue(),
385 bestChromosome.getMaxWeightValue(),
386 combinedConnections));
387 }
388 }