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