ruvector_mincut/snn/
morphogenetic.rs

1//! # Layer 5: Morphogenetic Networks as Developmental SNN
2//!
3//! Implements Turing-pattern-like self-organizing growth for graph structures.
4//!
5//! ## Theory
6//!
7//! ```text
8//! Turing Pattern          SNN Equivalent         Graph Manifestation
9//! ──────────────          ──────────────         ────────────────────
10//! Activator               Excitatory neurons     Edge addition
11//! Inhibitor               Inhibitory neurons     Edge removal
12//! Diffusion               Spike propagation      Weight spreading
13//! Reaction                Local computation      Node-local mincut
14//! Pattern formation       Self-organization      Cluster emergence
15//! ```
16//!
17//! ## Growth Rules
18//!
19//! - High neural activation → new edges
20//! - Low activation → edge pruning
21//! - Maturity detected via mincut stability
22
23use super::{
24    neuron::{LIFNeuron, NeuronConfig, NeuronPopulation},
25    network::{SpikingNetwork, NetworkConfig, LayerConfig},
26    SimTime, Spike,
27};
28use crate::graph::{DynamicGraph, VertexId};
29use std::collections::HashMap;
30
31/// Configuration for morphogenetic development
32#[derive(Debug, Clone)]
33pub struct MorphConfig {
34    /// Grid size (neurons arranged spatially)
35    pub grid_size: usize,
36    /// Growth activation threshold
37    pub growth_threshold: f64,
38    /// Prune activation threshold
39    pub prune_threshold: f64,
40    /// Minimum edge weight to keep
41    pub prune_weight: f64,
42    /// Target connectivity for maturity
43    pub target_connectivity: f64,
44    /// Stability epsilon for maturity detection
45    pub stability_epsilon: f64,
46    /// Diffusion kernel sigma
47    pub diffusion_sigma: f64,
48    /// Maximum development steps
49    pub max_steps: usize,
50    /// Time step
51    pub dt: f64,
52}
53
54impl Default for MorphConfig {
55    fn default() -> Self {
56        Self {
57            grid_size: 10,
58            growth_threshold: 0.6,
59            prune_threshold: 0.2,
60            prune_weight: 0.1,
61            target_connectivity: 0.5,
62            stability_epsilon: 0.01,
63            diffusion_sigma: 2.0,
64            max_steps: 1000,
65            dt: 1.0,
66        }
67    }
68}
69
70/// Turing pattern type for different growth modes
71#[derive(Debug, Clone, Copy, PartialEq)]
72pub enum TuringPattern {
73    /// Spots pattern (local clusters)
74    Spots,
75    /// Stripes pattern (elongated structures)
76    Stripes,
77    /// Labyrinth pattern (complex connectivity)
78    Labyrinth,
79    /// Uniform (no pattern)
80    Uniform,
81}
82
83/// Growth rules derived from neural activity
84#[derive(Debug, Clone)]
85pub struct GrowthRules {
86    /// Threshold for creating new edges
87    pub growth_threshold: f64,
88    /// Threshold for pruning edges
89    pub prune_threshold: f64,
90    /// Minimum weight to survive pruning
91    pub prune_weight: f64,
92    /// Target connectivity (edge density)
93    pub target_connectivity: f64,
94    /// Pattern type to develop
95    pub pattern: TuringPattern,
96}
97
98impl Default for GrowthRules {
99    fn default() -> Self {
100        Self {
101            growth_threshold: 0.6,
102            prune_threshold: 0.2,
103            prune_weight: 0.1,
104            target_connectivity: 0.5,
105            pattern: TuringPattern::Spots,
106        }
107    }
108}
109
110/// A spatial position on the grid
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
112pub struct GridPosition {
113    /// X coordinate
114    pub x: usize,
115    /// Y coordinate
116    pub y: usize,
117}
118
119impl GridPosition {
120    /// Create a new grid position
121    pub fn new(x: usize, y: usize) -> Self {
122        Self { x, y }
123    }
124
125    /// Convert to linear index
126    pub fn to_index(&self, grid_size: usize) -> usize {
127        self.y * grid_size + self.x
128    }
129
130    /// Create from linear index
131    pub fn from_index(idx: usize, grid_size: usize) -> Self {
132        Self {
133            x: idx % grid_size,
134            y: idx / grid_size,
135        }
136    }
137
138    /// Euclidean distance to another position
139    pub fn distance(&self, other: &GridPosition) -> f64 {
140        let dx = self.x as f64 - other.x as f64;
141        let dy = self.y as f64 - other.y as f64;
142        (dx * dx + dy * dy).sqrt()
143    }
144}
145
146/// Diffusion kernel for spatial coupling
147#[derive(Debug, Clone)]
148pub struct DiffusionKernel {
149    /// Kernel sigma (spread)
150    pub sigma: f64,
151    /// Excitatory range
152    pub excite_range: f64,
153    /// Inhibitory range
154    pub inhibit_range: f64,
155}
156
157impl DiffusionKernel {
158    /// Create a new diffusion kernel
159    pub fn new(sigma: f64) -> Self {
160        Self {
161            sigma,
162            excite_range: sigma,
163            inhibit_range: sigma * 3.0,
164        }
165    }
166
167    /// Compute kernel weight between two positions
168    pub fn weight(&self, pos1: &GridPosition, pos2: &GridPosition) -> (f64, f64) {
169        let d = pos1.distance(pos2);
170
171        // Mexican hat profile: excitation close, inhibition far
172        let excite = (-d * d / (2.0 * self.excite_range * self.excite_range)).exp();
173        let inhibit = (-d * d / (2.0 * self.inhibit_range * self.inhibit_range)).exp() * 0.5;
174
175        (excite, inhibit)
176    }
177}
178
179/// A morphogenetic neuron pair (excitatory + inhibitory)
180#[derive(Debug, Clone)]
181pub struct MorphNeuronPair {
182    /// Excitatory neuron
183    pub excitatory: LIFNeuron,
184    /// Inhibitory neuron
185    pub inhibitory: LIFNeuron,
186    /// Spatial position
187    pub position: GridPosition,
188}
189
190impl MorphNeuronPair {
191    /// Create a new neuron pair at position
192    pub fn new(id: usize, position: GridPosition) -> Self {
193        let e_config = NeuronConfig {
194            tau_membrane: 15.0,
195            threshold: 0.8,
196            ..NeuronConfig::default()
197        };
198
199        let i_config = NeuronConfig {
200            tau_membrane: 25.0,
201            threshold: 0.6,
202            ..NeuronConfig::default()
203        };
204
205        Self {
206            excitatory: LIFNeuron::with_config(id * 2, e_config),
207            inhibitory: LIFNeuron::with_config(id * 2 + 1, i_config),
208            position,
209        }
210    }
211
212    /// Get net activation (excitation - inhibition)
213    pub fn net_activation(&self) -> f64 {
214        self.excitatory.membrane_potential() - self.inhibitory.membrane_potential()
215    }
216}
217
218/// Morphogenetic SNN with Turing-pattern dynamics
219pub struct MorphogeneticSNN {
220    /// Spatial grid of excitatory/inhibitory neuron pairs
221    neurons: Vec<MorphNeuronPair>,
222    /// Grid size
223    grid_size: usize,
224    /// Diffusion kernel
225    diffusion_kernel: DiffusionKernel,
226    /// Growing graph structure
227    graph: DynamicGraph,
228    /// Growth rules
229    growth_rules: GrowthRules,
230    /// Configuration
231    config: MorphConfig,
232    /// Current simulation time
233    time: SimTime,
234    /// Mincut history for maturity detection
235    mincut_history: Vec<f64>,
236    /// Is development complete
237    is_mature: bool,
238}
239
240/// Maximum grid size to prevent memory exhaustion (256x256 = 65536 neurons)
241const MAX_GRID_SIZE: usize = 256;
242
243impl MorphogeneticSNN {
244    /// Create a new morphogenetic SNN
245    ///
246    /// # Panics
247    /// Panics if grid_size exceeds MAX_GRID_SIZE (256) to prevent memory exhaustion.
248    pub fn new(config: MorphConfig) -> Self {
249        // Resource limit: prevent grid_size² memory explosion
250        let grid_size = config.grid_size.min(MAX_GRID_SIZE);
251        let n = grid_size * grid_size;
252
253        // Create neuron pairs on grid
254        let neurons: Vec<_> = (0..n)
255            .map(|i| {
256                let pos = GridPosition::from_index(i, grid_size);
257                MorphNeuronPair::new(i, pos)
258            })
259            .collect();
260
261        // Initialize graph with nodes
262        let graph = DynamicGraph::new();
263        for i in 0..n {
264            graph.add_vertex(i as u64);
265        }
266
267        // Add initial local connectivity (4-neighborhood)
268        for y in 0..grid_size {
269            for x in 0..grid_size {
270                let i = y * grid_size + x;
271
272                if x + 1 < grid_size {
273                    let j = y * grid_size + (x + 1);
274                    let _ = graph.insert_edge(i as u64, j as u64, 0.5);
275                }
276
277                if y + 1 < grid_size {
278                    let j = (y + 1) * grid_size + x;
279                    let _ = graph.insert_edge(i as u64, j as u64, 0.5);
280                }
281            }
282        }
283
284        let growth_rules = GrowthRules {
285            growth_threshold: config.growth_threshold,
286            prune_threshold: config.prune_threshold,
287            prune_weight: config.prune_weight,
288            target_connectivity: config.target_connectivity,
289            ..GrowthRules::default()
290        };
291
292        Self {
293            neurons,
294            grid_size,
295            diffusion_kernel: DiffusionKernel::new(config.diffusion_sigma),
296            graph,
297            growth_rules,
298            config,
299            time: 0.0,
300            mincut_history: Vec::new(),
301            is_mature: false,
302        }
303    }
304
305    /// Run one development step
306    pub fn develop_step(&mut self) {
307        let dt = self.config.dt;
308        self.time += dt;
309
310        let n = self.neurons.len();
311
312        // 1. Neural dynamics with spatial diffusion
313        let mut activities = vec![0.0; n];
314
315        for i in 0..n {
316            // Compute diffusion input
317            let pos_i = self.neurons[i].position;
318            let mut e_input = 0.0;
319            let mut i_input = 0.0;
320
321            for j in 0..n {
322                if i == j {
323                    continue;
324                }
325
326                let pos_j = &self.neurons[j].position;
327                let (e_weight, i_weight) = self.diffusion_kernel.weight(&pos_i, pos_j);
328
329                e_input += e_weight * self.neurons[j].excitatory.membrane_potential();
330                i_input += i_weight * self.neurons[j].inhibitory.membrane_potential();
331            }
332
333            // Update neurons
334            let e_current = e_input - self.neurons[i].inhibitory.membrane_potential();
335            let i_current = i_input + self.neurons[i].excitatory.membrane_potential();
336
337            self.neurons[i].excitatory.step(e_current, dt, self.time);
338            self.neurons[i].inhibitory.step(i_current, dt, self.time);
339
340            activities[i] = self.neurons[i].net_activation();
341        }
342
343        // 2. Growth rules: modify graph based on activation
344        self.apply_growth_rules(&activities);
345
346        // 3. Update mincut history for maturity detection
347        let mincut = self.estimate_mincut();
348        self.mincut_history.push(mincut);
349
350        if self.mincut_history.len() > 100 {
351            self.mincut_history.remove(0);
352        }
353
354        // 4. Check maturity
355        self.is_mature = self.check_maturity(mincut);
356    }
357
358    /// Apply growth rules based on neural activities
359    fn apply_growth_rules(&mut self, activities: &[f64]) {
360        let n = self.neurons.len();
361
362        // Collect growth/prune decisions
363        let mut to_add: Vec<(VertexId, VertexId)> = Vec::new();
364        let mut to_remove: Vec<(VertexId, VertexId)> = Vec::new();
365
366        for i in 0..n {
367            let pos_i = &self.neurons[i].position;
368
369            // High activation → sprout new connections
370            if activities[i] > self.growth_rules.growth_threshold {
371                // Find growth targets (nearby nodes without existing edges)
372                for j in 0..n {
373                    if i == j {
374                        continue;
375                    }
376
377                    let pos_j = &self.neurons[j].position;
378                    let dist = pos_i.distance(pos_j);
379
380                    // Only connect to nearby nodes
381                    if dist <= self.diffusion_kernel.excite_range * 2.0 {
382                        let u = i as u64;
383                        let v = j as u64;
384
385                        if !self.graph.has_edge(u, v) {
386                            to_add.push((u, v));
387                        }
388                    }
389                }
390            }
391
392            // Low activation → retract connections
393            if activities[i] < self.growth_rules.prune_threshold {
394                let u = i as u64;
395
396                for (v, _) in self.graph.neighbors(u) {
397                    if let Some(edge) = self.graph.get_edge(u, v) {
398                        if edge.weight < self.growth_rules.prune_weight {
399                            to_remove.push((u, v));
400                        }
401                    }
402                }
403            }
404        }
405
406        // Apply changes
407        for (u, v) in to_add {
408            if !self.graph.has_edge(u, v) {
409                let _ = self.graph.insert_edge(u, v, 0.5);
410            }
411        }
412
413        for (u, v) in to_remove {
414            let _ = self.graph.delete_edge(u, v);
415        }
416    }
417
418    /// Estimate mincut (simplified)
419    fn estimate_mincut(&self) -> f64 {
420        if self.graph.num_vertices() == 0 {
421            return 0.0;
422        }
423
424        // Approximate: minimum degree
425        self.graph.vertices()
426            .iter()
427            .map(|&v| self.graph.degree(v) as f64)
428            .fold(f64::INFINITY, f64::min)
429    }
430
431    /// Check if development is mature
432    fn check_maturity(&self, current_mincut: f64) -> bool {
433        // Mature when connectivity target reached AND mincut is stable
434        let connectivity = self.graph.num_edges() as f64 /
435            (self.graph.num_vertices() * (self.graph.num_vertices() - 1) / 2).max(1) as f64;
436
437        if connectivity < self.growth_rules.target_connectivity {
438            return false;
439        }
440
441        // Check mincut stability
442        if self.mincut_history.len() < 20 {
443            return false;
444        }
445
446        let recent: Vec<_> = self.mincut_history.iter().rev().take(20).cloned().collect();
447        let mean = recent.iter().sum::<f64>() / recent.len() as f64;
448        let variance = recent.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / recent.len() as f64;
449
450        variance.sqrt() < self.config.stability_epsilon
451    }
452
453    /// Run development until mature or max steps
454    pub fn develop(&mut self) -> usize {
455        let mut steps = 0;
456
457        while steps < self.config.max_steps && !self.is_mature {
458            self.develop_step();
459            steps += 1;
460        }
461
462        steps
463    }
464
465    /// Get developed graph
466    pub fn graph(&self) -> &DynamicGraph {
467        &self.graph
468    }
469
470    /// Get grid size
471    pub fn grid_size(&self) -> usize {
472        self.grid_size
473    }
474
475    /// Check if mature
476    pub fn is_mature(&self) -> bool {
477        self.is_mature
478    }
479
480    /// Get current activation pattern
481    pub fn activation_pattern(&self) -> Vec<f64> {
482        self.neurons.iter().map(|n| n.net_activation()).collect()
483    }
484
485    /// Detect pattern type from activation
486    pub fn detect_pattern(&self) -> TuringPattern {
487        let activations = self.activation_pattern();
488
489        // Compute spatial autocorrelation
490        let mean = activations.iter().sum::<f64>() / activations.len() as f64;
491
492        let mut local_corr = 0.0;
493        let mut global_corr = 0.0;
494        let mut count = 0;
495
496        for i in 0..self.neurons.len() {
497            for j in (i + 1)..self.neurons.len() {
498                let dist = self.neurons[i].position.distance(&self.neurons[j].position);
499                let prod = (activations[i] - mean) * (activations[j] - mean);
500
501                if dist <= 2.0 {
502                    local_corr += prod;
503                }
504                global_corr += prod / dist.max(0.1);
505                count += 1;
506            }
507        }
508
509        local_corr /= count.max(1) as f64;
510        global_corr /= count.max(1) as f64;
511
512        // Classify pattern
513        if local_corr > 0.3 && global_corr < 0.1 {
514            TuringPattern::Spots
515        } else if local_corr > 0.2 && global_corr > 0.2 {
516            TuringPattern::Stripes
517        } else if local_corr > 0.1 {
518            TuringPattern::Labyrinth
519        } else {
520            TuringPattern::Uniform
521        }
522    }
523
524    /// Reset development
525    pub fn reset(&mut self) {
526        for pair in &mut self.neurons {
527            pair.excitatory.reset();
528            pair.inhibitory.reset();
529        }
530
531        self.graph.clear();
532        for i in 0..self.neurons.len() {
533            self.graph.add_vertex(i as u64);
534        }
535
536        // Re-add initial connectivity
537        for y in 0..self.grid_size {
538            for x in 0..self.grid_size {
539                let i = y * self.grid_size + x;
540
541                if x + 1 < self.grid_size {
542                    let j = y * self.grid_size + (x + 1);
543                    let _ = self.graph.insert_edge(i as u64, j as u64, 0.5);
544                }
545
546                if y + 1 < self.grid_size {
547                    let j = (y + 1) * self.grid_size + x;
548                    let _ = self.graph.insert_edge(i as u64, j as u64, 0.5);
549                }
550            }
551        }
552
553        self.time = 0.0;
554        self.mincut_history.clear();
555        self.is_mature = false;
556    }
557}
558
559#[cfg(test)]
560mod tests {
561    use super::*;
562
563    #[test]
564    fn test_grid_position() {
565        let pos1 = GridPosition::new(0, 0);
566        let pos2 = GridPosition::new(3, 4);
567
568        assert_eq!(pos1.distance(&pos2), 5.0);
569        assert_eq!(pos1.to_index(10), 0);
570        assert_eq!(pos2.to_index(10), 43);
571    }
572
573    #[test]
574    fn test_diffusion_kernel() {
575        let kernel = DiffusionKernel::new(2.0);
576
577        let pos1 = GridPosition::new(0, 0);
578        let pos2 = GridPosition::new(1, 0);
579        let pos3 = GridPosition::new(5, 0);
580
581        let (e1, i1) = kernel.weight(&pos1, &pos2);
582        let (e2, i2) = kernel.weight(&pos1, &pos3);
583
584        // Closer should have higher excitation
585        assert!(e1 > e2);
586    }
587
588    #[test]
589    fn test_morphogenetic_snn_creation() {
590        let mut config = MorphConfig::default();
591        config.grid_size = 5;
592
593        let snn = MorphogeneticSNN::new(config);
594
595        assert_eq!(snn.neurons.len(), 25);
596        assert!(!snn.is_mature());
597    }
598
599    #[test]
600    fn test_morphogenetic_development() {
601        let mut config = MorphConfig::default();
602        config.grid_size = 5;
603        config.max_steps = 100;
604
605        let mut snn = MorphogeneticSNN::new(config);
606
607        let steps = snn.develop();
608        assert!(steps <= 100);
609
610        // Graph should have evolved
611        assert!(snn.graph().num_edges() > 0);
612    }
613
614    #[test]
615    fn test_pattern_detection() {
616        let mut config = MorphConfig::default();
617        config.grid_size = 5;
618
619        let snn = MorphogeneticSNN::new(config);
620        let pattern = snn.detect_pattern();
621
622        // Initial state should be some pattern
623        assert!(pattern == TuringPattern::Uniform ||
624                pattern == TuringPattern::Spots ||
625                pattern == TuringPattern::Stripes ||
626                pattern == TuringPattern::Labyrinth);
627    }
628}