scirs2_spatial/neuromorphic/core/
neurons.rs

1//! Spiking Neuron Models
2//!
3//! This module implements various spiking neuron models used in neuromorphic
4//! computing, including the leaky integrate-and-fire model and more sophisticated
5//! neuron types with adaptive behaviors.
6
7/// Spiking neuron model using leaky integrate-and-fire dynamics
8///
9/// This neuron model integrates input currents over time and generates spikes
10/// when the membrane potential exceeds a threshold. After spiking, the neuron
11/// enters a refractory period during which it cannot spike again.
12///
13/// # Model Dynamics
14/// The membrane potential follows the equation:
15/// dV/dt = -leak_constant * V + I(t)
16///
17/// Where V is membrane potential and I(t) is input current.
18///
19/// # Example
20/// ```rust
21/// use scirs2_spatial::neuromorphic::core::SpikingNeuron;
22///
23/// let mut neuron = SpikingNeuron::new(vec![0.0, 0.0]);
24///
25/// // Simulate neuron for several time steps
26/// let dt = 0.1;
27/// let input_current = 1.5;
28///
29/// for _ in 0..20 {
30///     let spiked = neuron.update(dt, input_current);
31///     if spiked {
32///         println!("Neuron spiked!");
33///     }
34/// }
35/// ```
36#[derive(Debug, Clone)]
37pub struct SpikingNeuron {
38    /// Membrane potential
39    pub membrane_potential: f64,
40    /// Spike threshold
41    pub threshold: f64,
42    /// Refractory period
43    pub refractory_period: f64,
44    /// Time since last spike
45    pub time_since_spike: f64,
46    /// Leak constant
47    pub leak_constant: f64,
48    /// Input current
49    pub input_current: f64,
50    /// Neuron position in space
51    pub position: Vec<f64>,
52    /// Learning rate
53    pub learning_rate: f64,
54}
55
56impl SpikingNeuron {
57    /// Create new spiking neuron
58    ///
59    /// # Arguments
60    /// * `position` - Spatial position of the neuron in N-dimensional space
61    ///
62    /// # Returns
63    /// A new `SpikingNeuron` with default parameters
64    pub fn new(position: Vec<f64>) -> Self {
65        Self {
66            membrane_potential: 0.0,
67            threshold: 1.0,
68            refractory_period: 2.0,
69            time_since_spike: 2.1, // Start outside refractory period
70            leak_constant: 0.1,
71            input_current: 0.0,
72            position,
73            learning_rate: 0.01,
74        }
75    }
76
77    /// Create a new spiking neuron with custom parameters
78    ///
79    /// # Arguments
80    /// * `position` - Spatial position of the neuron
81    /// * `threshold` - Spike threshold
82    /// * `refractory_period` - Duration of refractory period
83    /// * `leak_constant` - Membrane leak constant
84    /// * `learning_rate` - Learning rate for adaptation
85    pub fn with_params(
86        position: Vec<f64>,
87        threshold: f64,
88        refractory_period: f64,
89        leak_constant: f64,
90        learning_rate: f64,
91    ) -> Self {
92        Self {
93            membrane_potential: 0.0,
94            threshold,
95            refractory_period,
96            time_since_spike: refractory_period + 0.1, // Start outside refractory period
97            leak_constant,
98            input_current: 0.0,
99            position,
100            learning_rate,
101        }
102    }
103
104    /// Update neuron state and check for spike
105    ///
106    /// Integrates the neuron dynamics for one time step and determines if
107    /// a spike should be generated.
108    ///
109    /// # Arguments
110    /// * `dt` - Time step size
111    /// * `input_current` - Input current for this time step
112    ///
113    /// # Returns
114    /// True if the neuron spiked, false otherwise
115    pub fn update(&mut self, dt: f64, input_current: f64) -> bool {
116        self.time_since_spike += dt;
117
118        // Check if in refractory period
119        if self.time_since_spike < self.refractory_period {
120            return false;
121        }
122
123        // Update membrane potential using leaky integrate-and-fire model
124        self.input_current = input_current;
125        let leak_term = -self.leak_constant * self.membrane_potential;
126        self.membrane_potential += dt * (leak_term + input_current);
127
128        // Check for spike
129        if self.membrane_potential >= self.threshold {
130            self.membrane_potential = 0.0; // Reset potential
131            self.time_since_spike = 0.0; // Reset spike timer
132            true
133        } else {
134            false
135        }
136    }
137
138    /// Calculate distance-based influence on another neuron
139    ///
140    /// Computes the spatial influence this neuron has on another neuron
141    /// based on their relative positions using a Gaussian function.
142    ///
143    /// # Arguments
144    /// * `other_position` - Position of the other neuron
145    ///
146    /// # Returns
147    /// Influence strength (0 to 1)
148    pub fn calculate_influence(&self, other_position: &[f64]) -> f64 {
149        if self.position.len() != other_position.len() {
150            return 0.0;
151        }
152
153        let distance: f64 = self
154            .position
155            .iter()
156            .zip(other_position.iter())
157            .map(|(&a, &b)| (a - b).powi(2))
158            .sum::<f64>()
159            .sqrt();
160
161        // Gaussian influence function
162        (-distance.powi(2) / 2.0).exp()
163    }
164
165    /// Get neuron position
166    pub fn position(&self) -> &[f64] {
167        &self.position
168    }
169
170    /// Set neuron position
171    pub fn set_position(&mut self, position: Vec<f64>) {
172        self.position = position;
173    }
174
175    /// Get membrane potential
176    pub fn membrane_potential(&self) -> f64 {
177        self.membrane_potential
178    }
179
180    /// Set membrane potential
181    pub fn set_membrane_potential(&mut self, potential: f64) {
182        self.membrane_potential = potential;
183    }
184
185    /// Get spike threshold
186    pub fn threshold(&self) -> f64 {
187        self.threshold
188    }
189
190    /// Set spike threshold
191    pub fn set_threshold(&mut self, threshold: f64) {
192        self.threshold = threshold;
193    }
194
195    /// Get refractory period
196    pub fn refractory_period(&self) -> f64 {
197        self.refractory_period
198    }
199
200    /// Set refractory period
201    pub fn set_refractory_period(&mut self, period: f64) {
202        self.refractory_period = period;
203    }
204
205    /// Get leak constant
206    pub fn leak_constant(&self) -> f64 {
207        self.leak_constant
208    }
209
210    /// Set leak constant
211    pub fn set_leak_constant(&mut self, leak: f64) {
212        self.leak_constant = leak;
213    }
214
215    /// Get learning rate
216    pub fn learning_rate(&self) -> f64 {
217        self.learning_rate
218    }
219
220    /// Set learning rate
221    pub fn set_learning_rate(&mut self, rate: f64) {
222        self.learning_rate = rate;
223    }
224
225    /// Check if neuron is in refractory period
226    pub fn is_refractory(&self) -> bool {
227        self.time_since_spike < self.refractory_period
228    }
229
230    /// Get time since last spike
231    pub fn time_since_spike(&self) -> f64 {
232        self.time_since_spike
233    }
234
235    /// Reset neuron to initial state
236    pub fn reset(&mut self) {
237        self.membrane_potential = 0.0;
238        self.time_since_spike = 0.0;
239        self.input_current = 0.0;
240    }
241
242    /// Inject current into the neuron
243    pub fn inject_current(&mut self, current: f64) {
244        self.input_current += current;
245    }
246
247    /// Calculate distance to another neuron
248    ///
249    /// # Arguments
250    /// * `other` - Reference to another neuron
251    ///
252    /// # Returns
253    /// Euclidean distance between neurons, or None if dimensions don't match
254    pub fn distance_to(&self, other: &SpikingNeuron) -> Option<f64> {
255        if self.position.len() != other.position.len() {
256            return None;
257        }
258
259        let distance = self
260            .position
261            .iter()
262            .zip(other.position.iter())
263            .map(|(&a, &b)| (a - b).powi(2))
264            .sum::<f64>()
265            .sqrt();
266
267        Some(distance)
268    }
269
270    /// Adapt threshold based on recent activity (homeostatic plasticity)
271    ///
272    /// # Arguments
273    /// * `target_rate` - Target firing rate
274    /// * `actual_rate` - Actual firing rate
275    /// * `adaptation_rate` - Rate of threshold adaptation
276    pub fn adapt_threshold(&mut self, target_rate: f64, actual_rate: f64, adaptation_rate: f64) {
277        let rate_error = actual_rate - target_rate;
278        self.threshold += adaptation_rate * rate_error;
279
280        // Keep threshold in reasonable bounds
281        self.threshold = self.threshold.clamp(0.1, 10.0);
282    }
283
284    /// Update learning rate based on recent performance
285    ///
286    /// # Arguments
287    /// * `performance_factor` - Factor indicating learning performance (0-1)
288    /// * `adaptation_rate` - Rate of learning rate adaptation
289    pub fn adapt_learning_rate(&mut self, performance_factor: f64, adaptation_rate: f64) {
290        // Increase learning rate if performance is poor, decrease if good
291        let adjustment = adaptation_rate * (1.0 - performance_factor);
292        self.learning_rate += adjustment;
293
294        // Keep learning rate in reasonable bounds
295        self.learning_rate = self.learning_rate.clamp(0.001, 1.0);
296    }
297}
298
299/// Adaptive spiking neuron with homeostatic mechanisms
300///
301/// This neuron extends the basic spiking neuron with adaptive threshold
302/// and learning rate mechanisms to maintain stable firing rates.
303#[derive(Debug, Clone)]
304pub struct AdaptiveSpikingNeuron {
305    /// Base spiking neuron
306    base_neuron: SpikingNeuron,
307    /// Target firing rate for homeostasis
308    target_firing_rate: f64,
309    /// Recent firing rate estimation
310    recent_firing_rate: f64,
311    /// Threshold adaptation rate
312    threshold_adaptation_rate: f64,
313    /// Spike count for rate estimation
314    spike_count: usize,
315    /// Time window for rate estimation
316    rate_estimation_window: f64,
317    /// Current time for rate estimation
318    current_time: f64,
319}
320
321impl AdaptiveSpikingNeuron {
322    /// Create new adaptive spiking neuron
323    ///
324    /// # Arguments
325    /// * `position` - Spatial position of the neuron
326    /// * `target_firing_rate` - Target firing rate for homeostasis
327    pub fn new(position: Vec<f64>, target_firing_rate: f64) -> Self {
328        Self {
329            base_neuron: SpikingNeuron::new(position),
330            target_firing_rate,
331            recent_firing_rate: 0.0,
332            threshold_adaptation_rate: 0.001,
333            spike_count: 0,
334            rate_estimation_window: 100.0,
335            current_time: 0.0,
336        }
337    }
338
339    /// Update neuron with homeostatic adaptation
340    ///
341    /// # Arguments
342    /// * `dt` - Time step size
343    /// * `input_current` - Input current for this time step
344    ///
345    /// # Returns
346    /// True if the neuron spiked, false otherwise
347    pub fn update(&mut self, dt: f64, input_current: f64) -> bool {
348        self.current_time += dt;
349
350        // Update base neuron
351        let spiked = self.base_neuron.update(dt, input_current);
352
353        if spiked {
354            self.spike_count += 1;
355        }
356
357        // Update firing rate estimate periodically
358        if self.current_time >= self.rate_estimation_window {
359            self.recent_firing_rate = self.spike_count as f64 / self.current_time;
360
361            // Apply homeostatic adaptation
362            self.base_neuron.adapt_threshold(
363                self.target_firing_rate,
364                self.recent_firing_rate,
365                self.threshold_adaptation_rate,
366            );
367
368            // Reset for next window
369            self.spike_count = 0;
370            self.current_time = 0.0;
371        }
372
373        spiked
374    }
375
376    /// Get reference to base neuron
377    pub fn base_neuron(&self) -> &SpikingNeuron {
378        &self.base_neuron
379    }
380
381    /// Get mutable reference to base neuron
382    pub fn base_neuron_mut(&mut self) -> &mut SpikingNeuron {
383        &mut self.base_neuron
384    }
385
386    /// Get target firing rate
387    pub fn target_firing_rate(&self) -> f64 {
388        self.target_firing_rate
389    }
390
391    /// Set target firing rate
392    pub fn set_target_firing_rate(&mut self, rate: f64) {
393        self.target_firing_rate = rate;
394    }
395
396    /// Get recent firing rate
397    pub fn recent_firing_rate(&self) -> f64 {
398        self.recent_firing_rate
399    }
400
401    /// Get current spike count
402    pub fn spike_count(&self) -> usize {
403        self.spike_count
404    }
405
406    /// Reset adaptation state
407    pub fn reset_adaptation(&mut self) {
408        self.spike_count = 0;
409        self.current_time = 0.0;
410        self.recent_firing_rate = 0.0;
411    }
412}
413
414#[cfg(test)]
415mod tests {
416    use super::*;
417
418    #[test]
419    fn test_spiking_neuron_creation() {
420        let neuron = SpikingNeuron::new(vec![0.0, 0.0]);
421        assert_eq!(neuron.position(), &[0.0, 0.0]);
422        assert_eq!(neuron.membrane_potential(), 0.0);
423        assert_eq!(neuron.threshold(), 1.0);
424        assert!(!neuron.is_refractory());
425    }
426
427    #[test]
428    fn test_neuron_spiking() {
429        let mut neuron = SpikingNeuron::new(vec![0.0, 0.0]);
430
431        // Test no spike with low input
432        let spiked = neuron.update(0.1, 0.1);
433        assert!(!spiked);
434        assert!(neuron.membrane_potential() > 0.0);
435
436        // Test spike with high input
437        let spiked = neuron.update(0.1, 10.0);
438        assert!(spiked);
439        assert_eq!(neuron.membrane_potential(), 0.0); // Should be reset after spike
440    }
441
442    #[test]
443    fn test_refractory_period() {
444        let mut neuron = SpikingNeuron::new(vec![0.0, 0.0]);
445
446        // Generate a spike
447        neuron.update(0.1, 10.0);
448        assert!(neuron.is_refractory());
449
450        // Try to spike again immediately - should fail
451        let spiked = neuron.update(0.1, 10.0);
452        assert!(!spiked);
453
454        // Wait for refractory period to pass
455        for _ in 0..25 {
456            // 2.5 time units at dt=0.1
457            neuron.update(0.1, 0.0);
458        }
459        assert!(!neuron.is_refractory());
460
461        // Now should be able to spike again
462        let spiked = neuron.update(0.1, 10.0);
463        assert!(spiked);
464    }
465
466    #[test]
467    fn test_neuron_influence() {
468        let neuron1 = SpikingNeuron::new(vec![0.0, 0.0]);
469        let neuron2 = SpikingNeuron::new(vec![1.0, 1.0]);
470
471        let influence = neuron1.calculate_influence(neuron2.position());
472        assert!(influence > 0.0 && influence <= 1.0);
473
474        // Self-influence should be 1.0
475        let self_influence = neuron1.calculate_influence(neuron1.position());
476        assert!((self_influence - 1.0).abs() < 1e-10);
477    }
478
479    #[test]
480    fn test_neuron_distance() {
481        let neuron1 = SpikingNeuron::new(vec![0.0, 0.0]);
482        let neuron2 = SpikingNeuron::new(vec![3.0, 4.0]);
483
484        let distance = neuron1.distance_to(&neuron2).unwrap();
485        assert!((distance - 5.0).abs() < 1e-10);
486
487        // Test mismatched dimensions
488        let neuron3 = SpikingNeuron::new(vec![1.0]);
489        assert!(neuron1.distance_to(&neuron3).is_none());
490    }
491
492    #[test]
493    #[ignore]
494    fn test_threshold_adaptation() {
495        let mut neuron = SpikingNeuron::new(vec![0.0, 0.0]);
496        let initial_threshold = neuron.threshold();
497
498        // Simulate high firing rate - threshold should increase
499        neuron.adapt_threshold(0.1, 0.5, 0.1); // target=0.1, actual=0.5
500        assert!(neuron.threshold() > initial_threshold);
501
502        // Simulate low firing rate - threshold should decrease
503        neuron.adapt_threshold(0.1, 0.05, 0.1); // target=0.1, actual=0.05
504        assert!(neuron.threshold() < initial_threshold);
505    }
506
507    #[test]
508    fn test_adaptive_neuron() {
509        let mut adaptive_neuron = AdaptiveSpikingNeuron::new(vec![0.0, 0.0], 0.1);
510
511        assert_eq!(adaptive_neuron.target_firing_rate(), 0.1);
512        assert_eq!(adaptive_neuron.spike_count(), 0);
513        assert_eq!(adaptive_neuron.recent_firing_rate(), 0.0);
514
515        // Simulate some activity
516        for _ in 0..50 {
517            adaptive_neuron.update(0.1, 2.0);
518        }
519
520        // Should have some spikes
521        assert!(adaptive_neuron.spike_count() > 0);
522    }
523
524    #[test]
525    fn test_neuron_reset() {
526        let mut neuron = SpikingNeuron::new(vec![0.0, 0.0]);
527
528        // Change neuron state
529        neuron.update(0.1, 5.0);
530        neuron.inject_current(1.0);
531
532        // Reset should restore initial state
533        neuron.reset();
534        assert_eq!(neuron.membrane_potential(), 0.0);
535        assert_eq!(neuron.time_since_spike(), 0.0);
536        assert_eq!(neuron.input_current, 0.0);
537    }
538
539    #[test]
540    fn test_parameter_setters() {
541        let mut neuron = SpikingNeuron::new(vec![0.0, 0.0]);
542
543        neuron.set_threshold(2.0);
544        assert_eq!(neuron.threshold(), 2.0);
545
546        neuron.set_refractory_period(5.0);
547        assert_eq!(neuron.refractory_period(), 5.0);
548
549        neuron.set_leak_constant(0.2);
550        assert_eq!(neuron.leak_constant(), 0.2);
551
552        neuron.set_learning_rate(0.05);
553        assert_eq!(neuron.learning_rate(), 0.05);
554
555        neuron.set_position(vec![1.0, 2.0, 3.0]);
556        assert_eq!(neuron.position(), &[1.0, 2.0, 3.0]);
557    }
558}