symbios_neat/lib.rs
1//! # Symbios NEAT
2//!
3//! A high-performance `NeuroEvolution` of Augmenting Topologies (NEAT) engine
4//! for morphogenetic engineering applications.
5//!
6//! ## Features
7//!
8//! - **Hash-Based Innovation**: Lock-free, deterministic parallel mutation using
9//! `Hash(input_node, output_node)` instead of global counters
10//! - **Arena-Graph Model**: Cache-friendly `SlotMap` storage for nodes and connections
11//! - **CPPN Support**: Periodic and radial activation functions (Sine, Cosine, Gaussian, Abs)
12//! for Compositional Pattern Producing Networks
13//! - **HyperNEAT Substrates**: Indirect encoding via [`substrate::substrate_to_network`],
14//! with `LayeredSubstrate`, `GridSubstrate2D`, and `GridSubstrate3D` provided out of the box
15//! - **Pattern / Voxel / Image Export**: `generate_pattern_2d`, `generate_voxel_grid`,
16//! and (with the `image` feature) `generate_image`
17//! - **Speciation**: [`species::NeatDistance`] adapter for `symbios_genetics::speciation`
18//! - **Genotype Trait**: Implements `symbios_genetics::Genotype` for use with evolutionary algorithms
19//!
20//! ## Quick Start
21//!
22//! ```rust
23//! use symbios_neat::{NeatGenome, NeatConfig, CppnEvaluator};
24//! use rand::SeedableRng;
25//! use rand_chacha::ChaCha8Rng;
26//!
27//! // Create a CPPN for 2D pattern generation
28//! let config = NeatConfig::cppn(2, 1);
29//! let mut rng = ChaCha8Rng::seed_from_u64(42);
30//! let genome = NeatGenome::fully_connected(config, &mut rng);
31//!
32//! // Compile and evaluate. `new` returns `Result<_, EvaluatorError>` —
33//! // it errors on cyclic genomes; call `genome.break_cycles()` first if needed.
34//! let evaluator = CppnEvaluator::new(&genome).expect("acyclic genome");
35//! let output = evaluator.query_2d(0.5, -0.5);
36//! println!("Output: {:?}", output);
37//! ```
38//!
39//! ## Using with Symbios Genetics
40//!
41//! ```rust,ignore
42//! use symbios_genetics::{Evaluator, Evolver, algorithms::simple::SimpleGA};
43//! use symbios_neat::{NeatGenome, NeatConfig, CppnEvaluator};
44//!
45//! // Define fitness function
46//! struct XorFitness;
47//! impl Evaluator<NeatGenome> for XorFitness {
48//! fn evaluate(&self, genome: &NeatGenome) -> (f32, Vec<f32>, Vec<f32>) {
49//! // CppnEvaluator::new returns Err on cyclic genomes; assign worst fitness if so.
50//! let Ok(eval) = CppnEvaluator::new(genome) else {
51//! return (0.0, vec![0.0], vec![]);
52//! };
53//! let mut error = 0.0;
54//!
55//! // XOR truth table
56//! for (inputs, expected) in &[
57//! ([0.0, 0.0], 0.0),
58//! ([0.0, 1.0], 1.0),
59//! ([1.0, 0.0], 1.0),
60//! ([1.0, 1.0], 0.0),
61//! ] {
62//! let output = eval.evaluate(inputs)[0];
63//! error += (output - expected).powi(2);
64//! }
65//!
66//! let fitness = 4.0 - error; // Max fitness = 4.0
67//! (fitness, vec![fitness], vec![])
68//! }
69//! }
70//!
71//! // Run evolution
72//! let config = NeatConfig::minimal(2, 1);
73//! let mut rng = rand::rng();
74//! let initial: Vec<NeatGenome> = (0..100)
75//! .map(|_| NeatGenome::fully_connected(config.clone(), &mut rng))
76//! .collect();
77//!
78//! let mut ga = SimpleGA::new(initial, 0.3, 5, 42);
79//! for _ in 0..100 {
80//! ga.step(&XorFitness);
81//! }
82//! ```
83//!
84//! ## Architecture
85//!
86//! ### Hash-Based Innovation (Sovereign Innovation)
87//!
88//! Traditional NEAT uses a global innovation counter requiring synchronization.
89//! This crate uses deterministic hashing:
90//!
91//! - **Connections**: `innovation = Hash(input_node_innovation, output_node_innovation)`
92//! - **Nodes (from split)**: `innovation = Hash(connection_innovation, SPLIT_MARKER)`
93//!
94//! This enables lock-free parallel mutation across threads without coordination.
95//!
96//! ### Arena-Graph Model
97//!
98//! Nodes and connections are stored in flat `SlotMap` buffers:
99//!
100//! - No reference counting overhead
101//! - Cache-friendly memory layout
102//! - Trivially serializable via Serde
103//! - Safe generational indices prevent use-after-free
104
105pub mod activation;
106pub mod evaluator;
107pub mod gene;
108pub mod genome;
109pub mod innovation;
110pub mod network;
111pub mod species;
112pub mod substrate;
113pub mod topology;
114
115// Re-exports for convenience
116pub use activation::Activation;
117pub use evaluator::{CppnEvaluator, EvalScratchpad, EvaluatorError, PatternError};
118pub use gene::{ConnectionGene, ConnectionId, NodeGene, NodeId, NodeType};
119pub use genome::{NeatConfig, NeatGenome};
120pub use innovation::{
121 connection_innovation, node_split_innovation, split_connection_a_innovation,
122 split_connection_b_innovation,
123};
124pub use network::{FeedforwardNetwork, Scratchpad};
125pub use species::NeatDistance;
126pub use substrate::{
127 GridSubstrate2D, GridSubstrate3D, LayeredSubstrate, Substrate, SubstrateNode,
128 substrate_to_network,
129};
130pub use topology::GraphTopology;
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use rand::SeedableRng;
136 use rand_chacha::ChaCha8Rng;
137 use symbios_genetics::Genotype;
138
139 #[test]
140 fn test_genotype_trait_implementation() {
141 let config = NeatConfig::minimal(2, 1);
142 let mut rng = ChaCha8Rng::seed_from_u64(42);
143
144 let mut genome = NeatGenome::fully_connected(config, &mut rng);
145
146 // Test mutation
147 genome.mutate(&mut rng, 1.0);
148
149 // Test crossover
150 let mut genome2 = genome.clone();
151 genome2.mutate(&mut rng, 1.0);
152
153 let child = genome.crossover(&genome2, &mut rng);
154 assert!(!child.input_ids.is_empty());
155 assert!(!child.output_ids.is_empty());
156 }
157
158 #[test]
159 fn test_serialization_roundtrip() {
160 let config = NeatConfig::cppn(3, 2);
161 let mut rng = ChaCha8Rng::seed_from_u64(123);
162 let mut genome = NeatGenome::fully_connected(config, &mut rng);
163
164 // Add some structure
165 let conn_id = genome.connections.iter().next().unwrap().0;
166 genome.add_node(conn_id, &mut rng);
167
168 // Serialize
169 let json = serde_json::to_string(&genome).expect("Serialization failed");
170
171 // Deserialize
172 let restored: NeatGenome = serde_json::from_str(&json).expect("Deserialization failed");
173
174 // Verify structure preserved
175 assert_eq!(genome.nodes.len(), restored.nodes.len());
176 assert_eq!(genome.connections.len(), restored.connections.len());
177 assert_eq!(genome.input_ids.len(), restored.input_ids.len());
178 assert_eq!(genome.output_ids.len(), restored.output_ids.len());
179 }
180
181 #[test]
182 fn test_cppn_pattern_generation() {
183 let config = NeatConfig::cppn(2, 1);
184 let mut rng = ChaCha8Rng::seed_from_u64(42);
185 let genome = NeatGenome::fully_connected(config, &mut rng);
186
187 let evaluator = CppnEvaluator::new(&genome).unwrap();
188 let pattern = evaluator.generate_pattern_2d(16, 16, 0).unwrap();
189
190 assert_eq!(pattern.len(), 256);
191
192 // Verify all values in valid range
193 for &val in &pattern {
194 assert!((0.0..=1.0).contains(&val), "Value out of range: {}", val);
195 }
196 }
197
198 #[test]
199 fn test_innovation_determinism() {
200 // Same structural mutation should produce same innovation
201 let inn1 = connection_innovation(1, 2);
202 let inn2 = connection_innovation(1, 2);
203 assert_eq!(inn1, inn2);
204
205 let node_inn1 = node_split_innovation(100);
206 let node_inn2 = node_split_innovation(100);
207 assert_eq!(node_inn1, node_inn2);
208 }
209}