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