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 network 30 * chromosomes, creating offspring that inherit network topology and connection weights from both parents while 31 * 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 * 61 * <pre>{@code 62 * // Create NEAT chromosome combinator 63 * RandomGenerator randomGen = RandomGenerator.getDefault(); 64 * NeatCombination policy = NeatCombination.builder() 65 * .inheritanceThresold(0.7) // 70% bias toward fitter parent 66 * .reenableGeneInheritanceThresold(0.25) // 25% gene re-enabling chance 67 * .parentComparisonPolicy(FitnessComparison.build()) 68 * .build(); 69 * 70 * ParentComparisonHandler comparisonHandler = new FitnessComparisonHandler(); 71 * NeatChromosomeCombinator<Double> combinator = new NeatChromosomeCombinator<>( 72 * randomGen, policy, comparisonHandler 73 * ); 74 * 75 * // Perform crossover 76 * NeatChromosome parent1 = // ... first parent 77 * NeatChromosome parent2 = // ... second parent 78 * Double fitness1 = 0.85; 79 * Double fitness2 = 0.72; 80 * 81 * List<Chromosome> offspring = combinator.combine( 82 * eaConfiguration, parent1, fitness1, parent2, fitness2 83 * ); 84 * NeatChromosome child = (NeatChromosome) offspring.get(0); 85 * }</pre> 86 * 87 * <p>Inheritance threshold effects: 88 * <ul> 89 * <li><strong>0.5</strong>: Unbiased inheritance, equal probability from both parents</li> 90 * <li><strong>> 0.5</strong>: Bias toward fitter parent, promotes convergence</li> 91 * <li><strong>< 0.5</strong>: Bias toward less fit parent, increases diversity</li> 92 * <li><strong>1.0</strong>: Always inherit from fitter parent (when fitness differs)</li> 93 * </ul> 94 * 95 * <p>Gene re-enabling mechanism: 96 * <ul> 97 * <li><strong>Preservation</strong>: Disabled genes maintain connection topology information</li> 98 * <li><strong>Re-activation</strong>: Chance to re-enable genes that are enabled in other parent</li> 99 * <li><strong>Exploration</strong>: Allows rediscovery of previously disabled connection patterns</li> 100 * <li><strong>Genetic diversity</strong>: Prevents permanent loss of structural information</li> 101 * </ul> 102 * 103 * <p>Duplicate connection prevention: 104 * <ul> 105 * <li><strong>Links cache</strong>: Tracks already included connections to prevent duplicates</li> 106 * <li><strong>Topology validation</strong>: Ensures each connection appears at most once</li> 107 * <li><strong>Cache efficiency</strong>: O(1) lookup for connection existence checking</li> 108 * <li><strong>Memory management</strong>: Cache cleared after each crossover operation</li> 109 * </ul> 110 * 111 * <p>Performance considerations: 112 * <ul> 113 * <li><strong>Linear time complexity</strong>: O(n + m) where n, m are parent connection counts</li> 114 * <li><strong>Innovation sorting</strong>: Leverages pre-sorted connection lists for efficiency</li> 115 * <li><strong>Memory efficiency</strong>: Minimal allocation during crossover</li> 116 * <li><strong>Cache optimization</strong>: Efficient duplicate detection and prevention</li> 117 * </ul> 118 * 119 * @param <T> the fitness value type (typically Double) 120 * @see NeatCombination 121 * @see ParentComparisonHandler 122 * @see NeatChromosome 123 * @see ChromosomeCombinator 124 */ 125 public class NeatChromosomeCombinator<T extends Comparable<T>> implements ChromosomeCombinator<T> { 126 public static final Logger logger = LogManager.getLogger(NeatChromosomeCombinator.class); 127 128 private final RandomGenerator randomGenerator; 129 private final NeatCombination neatCombination; 130 private final ParentComparisonHandler parentComparisonHandler; 131 132 /** 133 * Checks whether a connection already exists in the links cache. 134 * 135 * <p>The links cache prevents duplicate connections in the offspring by tracking all connections that have already 136 * been added. This ensures each connection 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 node has been added to the offspring, 159 * preventing the same connection from being added again during the crossover process. 160 * 161 * @param linksCache cache mapping from-node indices to sets of to-node indices 162 * @param connection connection to add to the cache 163 * @throws IllegalArgumentException if linksCache or connection is null 164 */ 165 private void insertInlinksCache(final Map<Integer, Set<Integer>> linksCache, final Connection connection) { 166 Validate.notNull(linksCache); 167 Validate.notNull(connection); 168 169 final int fromNodeIndex = connection.fromNodeIndex(); 170 final int toNodeIndex = connection.toNodeIndex(); 171 172 linksCache.computeIfAbsent(fromNodeIndex, k -> new HashSet<>()) 173 .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 Validate.notNull(chosenParent); 190 Validate.notNull(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, final NeatCombination _neatCombination, 214 final ParentComparisonHandler _parentComparisonHandler) { 215 Validate.notNull(_randomGenerator); 216 Validate.notNull(_neatCombination); 217 Validate.notNull(_parentComparisonHandler); 218 219 this.randomGenerator = _randomGenerator; 220 this.neatCombination = _neatCombination; 221 this.parentComparisonHandler = _parentComparisonHandler; 222 } 223 224 /** 225 * Performs genetic crossover between two NEAT chromosomes to produce offspring. 226 * 227 * <p>This method implements the NEAT crossover algorithm, aligning genes by innovation number and applying 228 * inheritance rules based on parent fitness and configuration parameters. The result is a single offspring 229 * chromosome that inherits network topology and connection weights from both parents. 230 * 231 * <p>Crossover process: 232 * <ol> 233 * <li>Compare parent fitness to determine inheritance bias</li> 234 * <li>Align genes by innovation number between parents</li> 235 * <li>Process matching genes with random inheritance (biased)</li> 236 * <li>Process disjoint genes based on fitness comparison</li> 237 * <li>Process excess genes from fitter parent</li> 238 * <li>Apply gene re-enabling rules for disabled connections</li> 239 * </ol> 240 * 241 * @param eaConfiguration evolutionary algorithm configuration containing fitness comparator 242 * @param firstChromosome first parent chromosome (must be NeatChromosome) 243 * @param firstParentFitness fitness value of first parent 244 * @param secondChromosome second parent chromosome (must be NeatChromosome) 245 * @param secondParentFitness fitness value of second parent 246 * @return list containing single offspring chromosome 247 * @throws IllegalArgumentException if chromosomes are not NeatChromosome instances or any parameter is null 248 */ 249 @Override 250 public List<Chromosome> combine(final AbstractEAConfiguration<T> eaConfiguration, final Chromosome firstChromosome, 251 final T firstParentFitness, final Chromosome secondChromosome, final T secondParentFitness) { 252 Validate.notNull(eaConfiguration); 253 Validate.notNull(firstChromosome); 254 Validate.notNull(firstParentFitness); 255 Validate.isInstanceOf(NeatChromosome.class, firstChromosome); 256 Validate.notNull(secondChromosome); 257 Validate.notNull(secondParentFitness); 258 Validate.isInstanceOf(NeatChromosome.class, secondChromosome); 259 260 final NeatChromosome firstNeatChromosome = (NeatChromosome) firstChromosome; 261 final NeatChromosome secondNeatChromosome = (NeatChromosome) secondChromosome; 262 final Comparator<T> fitnessComparator = eaConfiguration.fitnessComparator(); 263 final double inheritanceThresold = neatCombination.inheritanceThresold(); 264 final ParentComparisonPolicy parentComparisonPolicy = neatCombination.parentComparisonPolicy(); 265 266 final int fitnessComparison = fitnessComparator.compare(firstParentFitness, secondParentFitness); 267 final ChosenOtherChromosome comparedChromosomes = parentComparisonHandler 268 .compare(parentComparisonPolicy, firstNeatChromosome, secondNeatChromosome, fitnessComparison); 269 final NeatChromosome bestChromosome = comparedChromosomes.chosen(); 270 final NeatChromosome worstChromosome = comparedChromosomes.other(); 271 272 final List<Connection> combinedConnections = new ArrayList<>(); 273 final Map<Integer, Set<Integer>> linksCache = new HashMap<>(); 274 275 final var bestConnections = bestChromosome.getConnections(); 276 final var worstConnections = worstChromosome.getConnections(); 277 278 int indexBest = 0; 279 int indexWorst = 0; 280 281 while (indexBest < bestConnections.size() && indexWorst < worstConnections.size()) { 282 283 final var bestConnection = bestConnections.get(indexBest); 284 final var worstConnection = worstConnections.get(indexWorst); 285 286 if (bestConnection.innovation() == worstConnection.innovation()) { 287 /** 288 * If innovation is the same, we pick the connection randomly 289 */ 290 var original = bestConnection; 291 var other = worstConnection; 292 if (randomGenerator.nextDouble() < 1 - inheritanceThresold) { 293 original = worstConnection; 294 other = bestConnection; 295 } 296 if (linksCacheContainsConnection(linksCache, original) == false) { 297 298 /** 299 * If the chosen gene is disabled but the other one is enabled, then there is a chance we will re-enable 300 * it 301 */ 302 final boolean isEnabled = shouldReEnable(original, other) ? true : original.isEnabled(); 303 304 final var childConnection = Connection.builder() 305 .from(original) 306 .isEnabled(isEnabled) 307 .build(); 308 combinedConnections.add(childConnection); 309 insertInlinksCache(linksCache, original); 310 } 311 indexBest++; 312 indexWorst++; 313 } else if (bestConnection.innovation() > worstConnection.innovation()) { 314 315 /** 316 * If the fitnesses are equal, then we randomly inherit from the parent Otherwise, we do not inherit from 317 * the lesser gene 318 */ 319 if (fitnessComparison == 0 && randomGenerator.nextDouble() < 1.0 - inheritanceThresold) { 320 final var original = worstConnection; 321 if (linksCacheContainsConnection(linksCache, original) == false) { 322 combinedConnections.add(Connection.copyOf(original)); 323 insertInlinksCache(linksCache, original); 324 } 325 } 326 327 indexWorst++; 328 } else { 329 330 /** 331 * If the fitnesses are equal, then we randomly inherit from the parent Otherwise, we always inherit from 332 * the better gene 333 */ 334 335 if (fitnessComparison != 0 || randomGenerator.nextDouble() < inheritanceThresold) { 336 if (linksCacheContainsConnection(linksCache, bestConnection) == false) { 337 combinedConnections.add(Connection.copyOf(bestConnection)); 338 insertInlinksCache(linksCache, bestConnection); 339 } 340 } 341 indexBest++; 342 } 343 } 344 345 /* 346 * Case where the best connection has more genes. It's called excess genes 347 */ 348 while (indexBest < bestConnections.size()) { 349 /** 350 * If the fitnesses are equal, then we randomly inherit from the parent Otherwise, we always inherit from the 351 * better gene 352 */ 353 if (fitnessComparison != 0 || randomGenerator.nextDouble() < inheritanceThresold) { 354 final var bestConnection = bestConnections.get(indexBest); 355 if (linksCacheContainsConnection(linksCache, bestConnection) == false) { 356 combinedConnections.add(Connection.copyOf(bestConnection)); 357 insertInlinksCache(linksCache, bestConnection); 358 } 359 360 } 361 indexBest++; 362 } 363 364 /* 365 * Case where the worst connection has more genes. It's called excess genes. Since we don't inherit when their 366 * fitness aren't equal, it means we can skip the excess genes from the weaker connections. However we will 367 * randomly inherit if their fitnesses are equal 368 */ 369 while (fitnessComparison == 0 && indexWorst < worstConnections.size()) { 370 if (randomGenerator.nextDouble() < 1.0 - inheritanceThresold) { 371 final var worstConnection = worstConnections.get(indexWorst); 372 if (linksCacheContainsConnection(linksCache, worstConnection) == false) { 373 combinedConnections.add(Connection.copyOf(worstConnection)); 374 insertInlinksCache(linksCache, worstConnection); 375 } 376 377 } 378 indexWorst++; 379 } 380 381 return List.of(new NeatChromosome(bestChromosome.getNumInputs(), 382 bestChromosome.getNumOutputs(), 383 bestChromosome.getMinWeightValue(), 384 bestChromosome.getMaxWeightValue(), 385 combinedConnections)); 386 } 387 }