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