1 package net.bmahe.genetics4j.neat.chromosomes; 2 3 import java.util.ArrayList; 4 import java.util.Collections; 5 import java.util.Comparator; 6 import java.util.List; 7 import java.util.Objects; 8 import java.util.Set; 9 import java.util.stream.Collectors; 10 import java.util.stream.IntStream; 11 12 import org.apache.commons.lang3.Validate; 13 14 import net.bmahe.genetics4j.core.chromosomes.Chromosome; 15 import net.bmahe.genetics4j.neat.Connection; 16 17 /** 18 * Represents a neural network chromosome in the NEAT (NeuroEvolution of Augmenting Topologies) algorithm. 19 * 20 * <p>NeatChromosome is the core genetic representation in NEAT, encoding a neural network as a collection 21 * of connections between nodes. Each chromosome defines a complete neural network topology with input nodes, 22 * output nodes, optional hidden nodes, and weighted connections. The chromosome maintains essential 23 * parameters for network construction and genetic operations. 24 * 25 * <p>Key characteristics: 26 * <ul> 27 * <li><strong>Network topology</strong>: Encoded as a list of connections with innovation numbers</li> 28 * <li><strong>Node organization</strong>: Fixed input/output nodes with dynamically added hidden nodes</li> 29 * <li><strong>Weight constraints</strong>: Configurable minimum and maximum weight bounds</li> 30 * <li><strong>Innovation tracking</strong>: Connections sorted by innovation number for genetic alignment</li> 31 * </ul> 32 * 33 * <p>NEAT algorithm integration: 34 * <ul> 35 * <li><strong>Structural mutations</strong>: Add/delete nodes and connections while preserving innovation tracking</li> 36 * <li><strong>Weight mutations</strong>: Modify connection weights within specified bounds</li> 37 * <li><strong>Genetic crossover</strong>: Innovation-number-based gene alignment for topology recombination</li> 38 * <li><strong>Compatibility distance</strong>: Genetic similarity measurement for speciation</li> 39 * </ul> 40 * 41 * <p>Network structure: 42 * <ul> 43 * <li><strong>Input nodes</strong>: Indices 0 to (numInputs - 1), receive external inputs</li> 44 * <li><strong>Output nodes</strong>: Indices numInputs to (numInputs + numOutputs - 1), produce network outputs</li> 45 * <li><strong>Hidden nodes</strong>: Indices starting from (numInputs + numOutputs), created by add-node mutations</li> 46 * <li><strong>Connections</strong>: Weighted links between nodes with enable/disable states and innovation numbers</li> 47 * </ul> 48 * 49 * <p>Common usage patterns: 50 * <pre>{@code 51 * // Create a basic NEAT chromosome 52 * List<Connection> connections = List.of( 53 * Connection.of(0, 2, 0.5f, true, 0), // input 0 -> output 0 54 * Connection.of(1, 3, -0.3f, true, 1) // input 1 -> output 1 55 * ); 56 * 57 * NeatChromosome chromosome = new NeatChromosome( 58 * 2, // number of inputs 59 * 2, // number of outputs 60 * -1.0f, // minimum weight 61 * 1.0f, // maximum weight 62 * connections 63 * ); 64 * 65 * // Access chromosome properties 66 * int numAlleles = chromosome.getNumAlleles(); 67 * Set<Integer> inputNodes = chromosome.getInputNodeIndices(); 68 * Set<Integer> outputNodes = chromosome.getOutputNodeIndices(); 69 * List<Connection> allConnections = chromosome.getConnections(); 70 * 71 * // Create feed-forward network for evaluation 72 * FeedForwardNetwork network = new FeedForwardNetwork( 73 * chromosome.getInputNodeIndices(), 74 * chromosome.getOutputNodeIndices(), 75 * chromosome.getConnections(), 76 * Activations::sigmoid 77 * ); 78 * }</pre> 79 * 80 * <p>Genetic operations compatibility: 81 * <ul> 82 * <li><strong>Mutation operations</strong>: Compatible with weight, add-node, add-connection, and state mutations</li> 83 * <li><strong>Crossover operations</strong>: Innovation numbers enable proper gene alignment between parents</li> 84 * <li><strong>Selection operations</strong>: Supports species-based selection through compatibility distance</li> 85 * <li><strong>Evaluation operations</strong>: Can be converted to executable neural networks</li> 86 * </ul> 87 * 88 * <p>Innovation number organization: 89 * <ul> 90 * <li><strong>Sorted connections</strong>: Connections automatically sorted by innovation number</li> 91 * <li><strong>Genetic alignment</strong>: Enables efficient crossover and compatibility calculations</li> 92 * <li><strong>Historical tracking</strong>: Maintains evolutionary history of structural changes</li> 93 * <li><strong>Population consistency</strong>: Same innovation numbers across population for same connection types</li> 94 * </ul> 95 * 96 * <p>Performance considerations: 97 * <ul> 98 * <li><strong>Immutable connections</strong>: Connection list is sorted once and made immutable</li> 99 * <li><strong>Efficient lookup</strong>: Node indices computed deterministically for fast access</li> 100 * <li><strong>Memory efficiency</strong>: Only stores necessary network topology information</li> 101 * <li><strong>Cache-friendly</strong>: Sorted connections improve cache locality for genetic operations</li> 102 * </ul> 103 * 104 * <p>Integration with NEAT ecosystem: 105 * <ul> 106 * <li><strong>Chromosome factories</strong>: Created by NeatConnectedChromosomeFactory and similar</li> 107 * <li><strong>Genetic operators</strong>: Processed by NEAT-specific mutation and crossover handlers</li> 108 * <li><strong>Network evaluation</strong>: Converted to FeedForwardNetwork for fitness computation</li> 109 * <li><strong>Speciation</strong>: Used in compatibility distance calculations for species formation</li> 110 * </ul> 111 * 112 * @see Connection 113 * @see FeedForwardNetwork 114 * @see InnovationManager 115 * @see net.bmahe.genetics4j.neat.spec.NeatChromosomeSpec 116 */ 117 public class NeatChromosome implements Chromosome { 118 119 private final int numInputs; 120 private final int numOutputs; 121 private final float minWeightValue; 122 private final float maxWeightValue; 123 private final List<Connection> connections; 124 125 /** 126 * Constructs a new NEAT chromosome with the specified network topology and parameters. 127 * 128 * <p>This constructor creates an immutable neural network chromosome by copying and sorting 129 * the provided connections by their innovation numbers. The sorting ensures efficient 130 * genetic operations and proper gene alignment during crossover operations. 131 * 132 * <p>Network structure validation: 133 * <ul> 134 * <li>Input and output counts must be positive</li> 135 * <li>Weight bounds must be properly ordered (min < max)</li> 136 * <li>Connections list must not be null (but can be empty)</li> 137 * <li>Connection endpoints should reference valid nodes (not enforced here)</li> 138 * </ul> 139 * 140 * @param _numInputs number of input nodes in the network (must be positive) 141 * @param _numOutputs number of output nodes in the network (must be positive) 142 * @param _minWeightValue minimum allowed connection weight value 143 * @param _maxWeightValue maximum allowed connection weight value (must be > minWeightValue) 144 * @param _connections list of network connections (will be copied and sorted by innovation number) 145 * @throws IllegalArgumentException if numInputs <= 0, numOutputs <= 0, minWeightValue >= maxWeightValue, or connections is null 146 */ 147 public NeatChromosome(final int _numInputs, final int _numOutputs, final float _minWeightValue, 148 final float _maxWeightValue, final List<Connection> _connections) { 149 Validate.isTrue(_numInputs > 0); 150 Validate.isTrue(_numOutputs > 0); 151 Validate.isTrue(_minWeightValue < _maxWeightValue); 152 Validate.notNull(_connections); 153 154 this.numInputs = _numInputs; 155 this.numOutputs = _numOutputs; 156 this.minWeightValue = _minWeightValue; 157 this.maxWeightValue = _maxWeightValue; 158 159 final List<Connection> copyOfConnections = new ArrayList<>(_connections); 160 Collections.sort(copyOfConnections, Comparator.comparing(Connection::innovation)); 161 this.connections = Collections.unmodifiableList(copyOfConnections); 162 } 163 164 /** 165 * Returns the total number of alleles (genetic components) in this chromosome. 166 * 167 * <p>For NEAT chromosomes, the allele count includes: 168 * <ul> 169 * <li>Input nodes: Each input node represents one allele</li> 170 * <li>Output nodes: Each output node represents one allele</li> 171 * <li>Connections: Each connection (with its weight and state) represents one allele</li> 172 * </ul> 173 * 174 * <p>Hidden nodes are not counted separately as they are implicit in the connection structure. 175 * This count is used by the genetic algorithm framework for population statistics and 176 * compatibility calculations. 177 * 178 * @return the total number of alleles in this chromosome 179 */ 180 @Override 181 public int getNumAlleles() { 182 return numInputs + numOutputs + connections.size(); 183 } 184 185 /** 186 * Returns the number of input nodes in this neural network. 187 * 188 * <p>Input nodes are the entry points for external data into the network. They are 189 * assigned node indices from 0 to (numInputs - 1) and do not apply activation functions 190 * to their input values. 191 * 192 * @return the number of input nodes (always positive) 193 */ 194 public int getNumInputs() { 195 return numInputs; 196 } 197 198 /** 199 * Returns the number of output nodes in this neural network. 200 * 201 * <p>Output nodes produce the final results of network computation. They are assigned 202 * node indices from numInputs to (numInputs + numOutputs - 1) and apply activation 203 * functions to their weighted input sums. 204 * 205 * @return the number of output nodes (always positive) 206 */ 207 public int getNumOutputs() { 208 return numOutputs; 209 } 210 211 /** 212 * Returns the minimum allowed connection weight value for this network. 213 * 214 * <p>This bound is used by mutation operators to constrain weight perturbations and 215 * ensure that connection weights remain within reasonable ranges. Weight mutations 216 * should respect this bound to maintain network stability. 217 * 218 * @return the minimum allowed connection weight 219 */ 220 public float getMinWeightValue() { 221 return minWeightValue; 222 } 223 224 /** 225 * Returns the maximum allowed connection weight value for this network. 226 * 227 * <p>This bound is used by mutation operators to constrain weight perturbations and 228 * ensure that connection weights remain within reasonable ranges. Weight mutations 229 * should respect this bound to maintain network stability. 230 * 231 * @return the maximum allowed connection weight 232 */ 233 public float getMaxWeightValue() { 234 return maxWeightValue; 235 } 236 237 /** 238 * Returns an immutable list of all connections in this neural network. 239 * 240 * <p>The connections are sorted by innovation number to ensure consistent ordering 241 * for genetic operations. Each connection defines a weighted link between two nodes 242 * and includes an enabled/disabled state for topology exploration. 243 * 244 * <p>Connection properties: 245 * <ul> 246 * <li><strong>Immutable ordering</strong>: Connections are sorted by innovation number</li> 247 * <li><strong>Complete topology</strong>: Includes both enabled and disabled connections</li> 248 * <li><strong>Genetic information</strong>: Each connection carries innovation tracking data</li> 249 * <li><strong>Network structure</strong>: Defines the complete computational graph</li> 250 * </ul> 251 * 252 * @return immutable list of network connections, sorted by innovation number 253 */ 254 public List<Connection> getConnections() { 255 return connections; 256 } 257 258 /** 259 * Returns the set of input node indices for this neural network. 260 * 261 * <p>Input node indices are deterministically assigned as consecutive integers starting 262 * from 0. Input nodes receive external data and do not apply activation functions to 263 * their values. These indices are used for network construction and evaluation. 264 * 265 * <p>Index assignment: 266 * <ul> 267 * <li><strong>Range</strong>: 0 to (numInputs - 1)</li> 268 * <li><strong>Fixed assignment</strong>: Input indices never change during evolution</li> 269 * <li><strong>External interface</strong>: These indices map to external input data</li> 270 * <li><strong>No activation</strong>: Input nodes pass through their values unchanged</li> 271 * </ul> 272 * 273 * @return set of input node indices (0 to numInputs-1) 274 */ 275 public Set<Integer> getInputNodeIndices() { 276 return IntStream.range(0, numInputs) 277 .boxed() 278 .collect(Collectors.toSet()); 279 } 280 281 /** 282 * Returns the set of output node indices for this neural network. 283 * 284 * <p>Output node indices are deterministically assigned as consecutive integers starting 285 * immediately after the input nodes. Output nodes apply activation functions to their 286 * weighted input sums and produce the final network results. 287 * 288 * <p>Index assignment: 289 * <ul> 290 * <li><strong>Range</strong>: numInputs to (numInputs + numOutputs - 1)</li> 291 * <li><strong>Fixed assignment</strong>: Output indices never change during evolution</li> 292 * <li><strong>Network results</strong>: These nodes produce the final computational output</li> 293 * <li><strong>Activation applied</strong>: Output nodes apply activation functions to their sums</li> 294 * </ul> 295 * 296 * @return set of output node indices (numInputs to numInputs+numOutputs-1) 297 */ 298 public Set<Integer> getOutputNodeIndices() { 299 return IntStream.range(numInputs, getNumInputs() + getNumOutputs()) 300 .boxed() 301 .collect(Collectors.toSet()); 302 } 303 304 @Override 305 public int hashCode() { 306 return Objects.hash(connections, maxWeightValue, minWeightValue, numInputs, numOutputs); 307 } 308 309 @Override 310 public boolean equals(Object obj) { 311 if (this == obj) 312 return true; 313 if (obj == null) 314 return false; 315 if (getClass() != obj.getClass()) 316 return false; 317 NeatChromosome other = (NeatChromosome) obj; 318 return Objects.equals(connections, other.connections) 319 && Float.floatToIntBits(maxWeightValue) == Float.floatToIntBits(other.maxWeightValue) 320 && Float.floatToIntBits(minWeightValue) == Float.floatToIntBits(other.minWeightValue) 321 && numInputs == other.numInputs && numOutputs == other.numOutputs; 322 } 323 324 @Override 325 public String toString() { 326 return "NeatChromosome [numInputs=" + numInputs + ", numOutputs=" + numOutputs + ", minWeightValue=" 327 + minWeightValue + ", maxWeightValue=" + maxWeightValue + ", connections=" + connections + "]"; 328 } 329 }