Skip to main content

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}