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>> 0.5</strong>: Bias toward fitter parent, promotes convergence</li> 90 * <li><strong>< 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 }