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 < 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 > minWeightValue)
137 * @param _connections list of network connections (will be copied and sorted by innovation number)
138 * @throws IllegalArgumentException if minWeightValue >= 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 }