View Javadoc
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 &lt; 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 &gt; minWeightValue)
144 	 * @param _connections list of network connections (will be copied and sorted by innovation number)
145 	 * @throws IllegalArgumentException if numInputs &lt;= 0, numOutputs &lt;= 0, minWeightValue &gt;= 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 }