scirs2_spatial/neuromorphic/core/
synapses.rs

1//! Synaptic Models for Neuromorphic Computing
2//!
3//! This module implements various synaptic models including spike-timing dependent
4//! plasticity (STDP), metaplasticity, and homeostatic synaptic scaling. These synapses
5//! form the connections between neurons and enable learning and adaptation.
6
7use std::collections::VecDeque;
8
9/// Synaptic connection with STDP learning
10///
11/// This synapse implements spike-timing dependent plasticity (STDP), a fundamental
12/// learning rule in biological neural networks. The synaptic strength is modified
13/// based on the relative timing of pre- and post-synaptic spikes.
14///
15/// # STDP Rule
16/// - If pre-synaptic spike occurs before post-synaptic spike: potentiation (strengthening)
17/// - If post-synaptic spike occurs before pre-synaptic spike: depression (weakening)
18/// - The magnitude of change decreases exponentially with the time difference
19///
20/// # Example
21/// ```rust
22/// use scirs2_spatial::neuromorphic::core::Synapse;
23///
24/// let mut synapse = Synapse::new(0, 1, 0.5);
25///
26/// // Simulate STDP learning
27/// synapse.update_stdp(10.0, true, false);  // Pre-synaptic spike at t=10
28/// synapse.update_stdp(15.0, false, true);  // Post-synaptic spike at t=15
29///
30/// println!("Updated weight: {}", synapse.weight()); // Should be increased
31/// ```
32#[derive(Debug, Clone)]
33pub struct Synapse {
34    /// Pre-synaptic neuron ID
35    pub pre_neuron: usize,
36    /// Post-synaptic neuron ID
37    pub post_neuron: usize,
38    /// Synaptic weight
39    weight: f64,
40    /// Last pre-synaptic spike time
41    last_pre_spike: f64,
42    /// Last post-synaptic spike time
43    last_post_spike: f64,
44    /// STDP learning rate
45    stdp_rate: f64,
46    /// STDP time constant
47    stdp_tau: f64,
48    /// Minimum weight bound
49    min_weight: f64,
50    /// Maximum weight bound
51    max_weight: f64,
52}
53
54impl Synapse {
55    /// Create new synapse
56    ///
57    /// # Arguments
58    /// * `pre_neuron` - ID of pre-synaptic neuron
59    /// * `post_neuron` - ID of post-synaptic neuron
60    /// * `initial_weight` - Initial synaptic weight
61    ///
62    /// # Returns
63    /// A new `Synapse` with default STDP parameters
64    pub fn new(pre_neuron: usize, post_neuron: usize, initial_weight: f64) -> Self {
65        Self {
66            pre_neuron,
67            post_neuron,
68            weight: initial_weight,
69            last_pre_spike: -1000.0,
70            last_post_spike: -1000.0,
71            stdp_rate: 0.01,
72            stdp_tau: 20.0,
73            min_weight: -2.0,
74            max_weight: 2.0,
75        }
76    }
77
78    /// Create synapse with custom STDP parameters
79    ///
80    /// # Arguments
81    /// * `pre_neuron` - ID of pre-synaptic neuron
82    /// * `post_neuron` - ID of post-synaptic neuron
83    /// * `initial_weight` - Initial synaptic weight
84    /// * `stdp_rate` - STDP learning rate
85    /// * `stdp_tau` - STDP time constant
86    /// * `min_weight` - Minimum weight bound
87    /// * `max_weight` - Maximum weight bound
88    pub fn with_stdp_params(
89        pre_neuron: usize,
90        post_neuron: usize,
91        initial_weight: f64,
92        stdp_rate: f64,
93        stdp_tau: f64,
94        min_weight: f64,
95        max_weight: f64,
96    ) -> Self {
97        Self {
98            pre_neuron,
99            post_neuron,
100            weight: initial_weight,
101            last_pre_spike: -1000.0,
102            last_post_spike: -1000.0,
103            stdp_rate,
104            stdp_tau,
105            min_weight,
106            max_weight,
107        }
108    }
109
110    /// Update synaptic weight using STDP rule
111    ///
112    /// Applies the spike-timing dependent plasticity learning rule based on
113    /// the timing of pre- and post-synaptic spikes.
114    ///
115    /// # Arguments
116    /// * `current_time` - Current simulation time
117    /// * `pre_spiked` - Whether pre-synaptic neuron spiked
118    /// * `post_spiked` - Whether post-synaptic neuron spiked
119    pub fn update_stdp(&mut self, current_time: f64, pre_spiked: bool, post_spiked: bool) {
120        if pre_spiked {
121            self.last_pre_spike = current_time;
122        }
123        if post_spiked {
124            self.last_post_spike = current_time;
125        }
126
127        // Apply STDP learning rule
128        if pre_spiked && self.last_post_spike > self.last_pre_spike - 50.0 {
129            // Potentiation: pre before post
130            let dt = self.last_post_spike - self.last_pre_spike;
131            if dt > 0.0 {
132                let delta_w = self.stdp_rate * (-dt / self.stdp_tau).exp();
133                self.weight += delta_w;
134            }
135        }
136
137        if post_spiked && self.last_pre_spike > self.last_post_spike - 50.0 {
138            // Depression: post before pre
139            let dt = self.last_pre_spike - self.last_post_spike;
140            if dt > 0.0 {
141                let delta_w = -self.stdp_rate * (-dt / self.stdp_tau).exp();
142                self.weight += delta_w;
143            }
144        }
145
146        // Keep weights in reasonable bounds
147        self.weight = self.weight.clamp(self.min_weight, self.max_weight);
148    }
149
150    /// Calculate synaptic current
151    ///
152    /// Computes the current transmitted through this synapse given the
153    /// pre-synaptic spike strength.
154    ///
155    /// # Arguments
156    /// * `pre_spike_strength` - Strength of the pre-synaptic spike
157    ///
158    /// # Returns
159    /// Synaptic current (weight * spike strength)
160    pub fn synaptic_current(&self, pre_spike_strength: f64) -> f64 {
161        self.weight * pre_spike_strength
162    }
163
164    /// Get synaptic weight
165    pub fn weight(&self) -> f64 {
166        self.weight
167    }
168
169    /// Set synaptic weight
170    pub fn set_weight(&mut self, weight: f64) {
171        self.weight = weight.clamp(self.min_weight, self.max_weight);
172    }
173
174    /// Get pre-synaptic neuron ID
175    pub fn pre_neuron(&self) -> usize {
176        self.pre_neuron
177    }
178
179    /// Get post-synaptic neuron ID
180    pub fn post_neuron(&self) -> usize {
181        self.post_neuron
182    }
183
184    /// Get STDP learning rate
185    pub fn stdp_rate(&self) -> f64 {
186        self.stdp_rate
187    }
188
189    /// Set STDP learning rate
190    pub fn set_stdp_rate(&mut self, rate: f64) {
191        self.stdp_rate = rate;
192    }
193
194    /// Get STDP time constant
195    pub fn stdp_tau(&self) -> f64 {
196        self.stdp_tau
197    }
198
199    /// Set STDP time constant
200    pub fn set_stdp_tau(&mut self, tau: f64) {
201        self.stdp_tau = tau;
202    }
203
204    /// Get last pre-synaptic spike time
205    pub fn last_pre_spike(&self) -> f64 {
206        self.last_pre_spike
207    }
208
209    /// Get last post-synaptic spike time
210    pub fn last_post_spike(&self) -> f64 {
211        self.last_post_spike
212    }
213
214    /// Get weight bounds
215    pub fn weight_bounds(&self) -> (f64, f64) {
216        (self.min_weight, self.max_weight)
217    }
218
219    /// Set weight bounds
220    pub fn set_weight_bounds(&mut self, min_weight: f64, max_weight: f64) {
221        self.min_weight = min_weight;
222        self.max_weight = max_weight;
223        // Re-clamp current weight to new bounds
224        self.weight = self.weight.clamp(min_weight, max_weight);
225    }
226
227    /// Check if synapse is excitatory (positive weight)
228    pub fn is_excitatory(&self) -> bool {
229        self.weight > 0.0
230    }
231
232    /// Check if synapse is inhibitory (negative weight)
233    pub fn is_inhibitory(&self) -> bool {
234        self.weight < 0.0
235    }
236
237    /// Reset spike timing history
238    pub fn reset_spike_history(&mut self) {
239        self.last_pre_spike = -1000.0;
240        self.last_post_spike = -1000.0;
241    }
242
243    /// Calculate time since last pre-synaptic spike
244    pub fn time_since_pre_spike(&self, current_time: f64) -> f64 {
245        current_time - self.last_pre_spike
246    }
247
248    /// Calculate time since last post-synaptic spike
249    pub fn time_since_post_spike(&self, current_time: f64) -> f64 {
250        current_time - self.last_post_spike
251    }
252}
253
254/// Metaplastic synapse with history-dependent learning
255///
256/// This synapse extends basic STDP with metaplasticity - the learning rate
257/// itself adapts based on the history of synaptic activity. This provides
258/// more sophisticated learning dynamics.
259#[derive(Debug, Clone)]
260pub struct MetaplasticSynapse {
261    /// Base synapse
262    base_synapse: Synapse,
263    /// Learning rate history
264    learning_history: VecDeque<f64>,
265    /// Maximum history length
266    max_history_length: usize,
267    /// Metaplasticity time constant
268    metaplasticity_tau: f64,
269    /// Base learning rate
270    base_learning_rate: f64,
271}
272
273impl MetaplasticSynapse {
274    /// Create new metaplastic synapse
275    ///
276    /// # Arguments
277    /// * `pre_neuron` - ID of pre-synaptic neuron
278    /// * `post_neuron` - ID of post-synaptic neuron
279    /// * `initial_weight` - Initial synaptic weight
280    /// * `base_learning_rate` - Base STDP learning rate
281    pub fn new(
282        pre_neuron: usize,
283        post_neuron: usize,
284        initial_weight: f64,
285        base_learning_rate: f64,
286    ) -> Self {
287        let mut base_synapse = Synapse::new(pre_neuron, post_neuron, initial_weight);
288        base_synapse.set_stdp_rate(base_learning_rate);
289
290        Self {
291            base_synapse,
292            learning_history: VecDeque::new(),
293            max_history_length: 100,
294            metaplasticity_tau: 100.0,
295            base_learning_rate,
296        }
297    }
298
299    /// Update synapse with metaplasticity
300    ///
301    /// # Arguments
302    /// * `current_time` - Current simulation time
303    /// * `pre_spiked` - Whether pre-synaptic neuron spiked
304    /// * `post_spiked` - Whether post-synaptic neuron spiked
305    pub fn update(&mut self, current_time: f64, pre_spiked: bool, post_spiked: bool) {
306        let old_weight = self.base_synapse.weight();
307
308        // Update base synapse
309        self.base_synapse
310            .update_stdp(current_time, pre_spiked, post_spiked);
311
312        // Calculate weight change
313        let weight_change = (self.base_synapse.weight() - old_weight).abs();
314
315        // Add to learning history
316        self.learning_history.push_back(weight_change);
317        if self.learning_history.len() > self.max_history_length {
318            self.learning_history.pop_front();
319        }
320
321        // Update learning rate based on history
322        self.update_learning_rate();
323    }
324
325    /// Update learning rate based on recent activity
326    fn update_learning_rate(&mut self) {
327        if self.learning_history.is_empty() {
328            return;
329        }
330
331        // Calculate average recent activity
332        let avg_activity: f64 =
333            self.learning_history.iter().sum::<f64>() / self.learning_history.len() as f64;
334
335        // Adapt learning rate: decrease if high activity, increase if low activity
336        let adaptation_factor = (-avg_activity / self.metaplasticity_tau).exp();
337        let new_learning_rate = self.base_learning_rate * adaptation_factor;
338
339        self.base_synapse.set_stdp_rate(new_learning_rate);
340    }
341
342    /// Get reference to base synapse
343    pub fn base_synapse(&self) -> &Synapse {
344        &self.base_synapse
345    }
346
347    /// Get mutable reference to base synapse
348    pub fn base_synapse_mut(&mut self) -> &mut Synapse {
349        &mut self.base_synapse
350    }
351
352    /// Get current learning rate
353    pub fn current_learning_rate(&self) -> f64 {
354        self.base_synapse.stdp_rate()
355    }
356
357    /// Get average recent activity
358    pub fn average_recent_activity(&self) -> f64 {
359        if self.learning_history.is_empty() {
360            0.0
361        } else {
362            self.learning_history.iter().sum::<f64>() / self.learning_history.len() as f64
363        }
364    }
365
366    /// Reset metaplasticity history
367    pub fn reset_history(&mut self) {
368        self.learning_history.clear();
369        self.base_synapse.set_stdp_rate(self.base_learning_rate);
370    }
371}
372
373/// Homeostatic synapse with activity-dependent scaling
374///
375/// This synapse implements homeostatic scaling to maintain stable activity levels
376/// by globally scaling synaptic weights based on the target activity level.
377#[derive(Debug, Clone)]
378pub struct HomeostaticSynapse {
379    /// Base synapse
380    base_synapse: Synapse,
381    /// Target activity level
382    target_activity: f64,
383    /// Current activity estimate
384    current_activity: f64,
385    /// Homeostatic scaling rate
386    scaling_rate: f64,
387    /// Activity estimation time constant
388    activity_tau: f64,
389}
390
391impl HomeostaticSynapse {
392    /// Create new homeostatic synapse
393    ///
394    /// # Arguments
395    /// * `pre_neuron` - ID of pre-synaptic neuron
396    /// * `post_neuron` - ID of post-synaptic neuron
397    /// * `initial_weight` - Initial synaptic weight
398    /// * `target_activity` - Target activity level for homeostasis
399    pub fn new(
400        pre_neuron: usize,
401        post_neuron: usize,
402        initial_weight: f64,
403        target_activity: f64,
404    ) -> Self {
405        Self {
406            base_synapse: Synapse::new(pre_neuron, post_neuron, initial_weight),
407            target_activity,
408            current_activity: 0.0,
409            scaling_rate: 0.001,
410            activity_tau: 1000.0,
411        }
412    }
413
414    /// Update synapse with homeostatic scaling
415    ///
416    /// # Arguments
417    /// * `current_time` - Current simulation time
418    /// * `pre_spiked` - Whether pre-synaptic neuron spiked
419    /// * `post_spiked` - Whether post-synaptic neuron spiked
420    /// * `dt` - Time step size
421    pub fn update(&mut self, current_time: f64, pre_spiked: bool, post_spiked: bool, dt: f64) {
422        // Update base STDP
423        self.base_synapse
424            .update_stdp(current_time, pre_spiked, post_spiked);
425
426        // Update activity estimate
427        let activity_input = if post_spiked { 1.0 } else { 0.0 };
428        let decay = (-dt / self.activity_tau).exp();
429        self.current_activity = decay * self.current_activity + (1.0 - decay) * activity_input;
430
431        // Apply homeostatic scaling
432        let activity_error = self.current_activity - self.target_activity;
433        let scaling_factor = 1.0 - self.scaling_rate * activity_error;
434
435        let current_weight = self.base_synapse.weight();
436        self.base_synapse
437            .set_weight(current_weight * scaling_factor);
438    }
439
440    /// Get reference to base synapse
441    pub fn base_synapse(&self) -> &Synapse {
442        &self.base_synapse
443    }
444
445    /// Get current activity estimate
446    pub fn current_activity(&self) -> f64 {
447        self.current_activity
448    }
449
450    /// Get target activity
451    pub fn target_activity(&self) -> f64 {
452        self.target_activity
453    }
454
455    /// Set target activity
456    pub fn set_target_activity(&mut self, target: f64) {
457        self.target_activity = target;
458    }
459
460    /// Get activity error (current - target)
461    pub fn activity_error(&self) -> f64 {
462        self.current_activity - self.target_activity
463    }
464}
465
466#[cfg(test)]
467mod tests {
468    use super::*;
469
470    #[test]
471    fn test_synapse_creation() {
472        let synapse = Synapse::new(0, 1, 0.5);
473        assert_eq!(synapse.pre_neuron(), 0);
474        assert_eq!(synapse.post_neuron(), 1);
475        assert_eq!(synapse.weight(), 0.5);
476        assert!(synapse.is_excitatory());
477        assert!(!synapse.is_inhibitory());
478    }
479
480    #[test]
481    #[ignore]
482    fn test_stdp_potentiation() {
483        let mut synapse = Synapse::new(0, 1, 0.5);
484        let initial_weight = synapse.weight();
485
486        // Pre-synaptic spike before post-synaptic spike (potentiation)
487        synapse.update_stdp(10.0, true, false); // Pre spike at t=10
488        synapse.update_stdp(15.0, false, true); // Post spike at t=15
489
490        // Weight should increase
491        assert!(synapse.weight() > initial_weight);
492    }
493
494    #[test]
495    #[ignore]
496    fn test_stdp_depression() {
497        let mut synapse = Synapse::new(0, 1, 0.5);
498        let initial_weight = synapse.weight();
499
500        // Post-synaptic spike before pre-synaptic spike (depression)
501        synapse.update_stdp(10.0, false, true); // Post spike at t=10
502        synapse.update_stdp(15.0, true, false); // Pre spike at t=15
503
504        // Weight should decrease
505        assert!(synapse.weight() < initial_weight);
506    }
507
508    #[test]
509    fn test_synaptic_current() {
510        let synapse = Synapse::new(0, 1, 0.5);
511        let current = synapse.synaptic_current(2.0);
512        assert_eq!(current, 1.0); // 0.5 * 2.0
513    }
514
515    #[test]
516    fn test_weight_bounds() {
517        let mut synapse = Synapse::new(0, 1, 0.0);
518
519        // Test setting weight beyond bounds
520        synapse.set_weight(10.0);
521        assert_eq!(synapse.weight(), 2.0); // Should be clamped to max
522
523        synapse.set_weight(-10.0);
524        assert_eq!(synapse.weight(), -2.0); // Should be clamped to min
525    }
526
527    #[test]
528    fn test_inhibitory_synapse() {
529        let synapse = Synapse::new(0, 1, -0.5);
530        assert!(!synapse.is_excitatory());
531        assert!(synapse.is_inhibitory());
532
533        let current = synapse.synaptic_current(1.0);
534        assert_eq!(current, -0.5);
535    }
536
537    #[test]
538    fn test_spike_timing() {
539        let mut synapse = Synapse::new(0, 1, 0.5);
540
541        synapse.update_stdp(10.0, true, false);
542        assert_eq!(synapse.last_pre_spike(), 10.0);
543        assert_eq!(synapse.time_since_pre_spike(15.0), 5.0);
544
545        synapse.update_stdp(12.0, false, true);
546        assert_eq!(synapse.last_post_spike(), 12.0);
547        assert_eq!(synapse.time_since_post_spike(15.0), 3.0);
548    }
549
550    #[test]
551    #[ignore]
552    fn test_metaplastic_synapse() {
553        let mut meta_synapse = MetaplasticSynapse::new(0, 1, 0.5, 0.01);
554
555        assert_eq!(meta_synapse.current_learning_rate(), 0.01);
556        assert_eq!(meta_synapse.average_recent_activity(), 0.0);
557
558        // Simulate some activity
559        for i in 0..10 {
560            meta_synapse.update(i as f64, true, false);
561            meta_synapse.update(i as f64 + 0.5, false, true);
562        }
563
564        // Learning rate should adapt based on activity
565        assert!(meta_synapse.average_recent_activity() > 0.0);
566    }
567
568    #[test]
569    fn test_homeostatic_synapse() {
570        let mut homeostatic = HomeostaticSynapse::new(0, 1, 0.5, 0.1);
571
572        assert_eq!(homeostatic.target_activity(), 0.1);
573        assert_eq!(homeostatic.current_activity(), 0.0);
574
575        // Simulate high activity
576        for _ in 0..50 {
577            homeostatic.update(1.0, true, true, 0.1);
578        }
579
580        // Activity should increase
581        assert!(homeostatic.current_activity() > 0.0);
582        assert!(homeostatic.activity_error() != 0.0);
583    }
584
585    #[test]
586    fn test_synapse_reset() {
587        let mut synapse = Synapse::new(0, 1, 0.5);
588
589        // Record some spike activity
590        synapse.update_stdp(10.0, true, false);
591        synapse.update_stdp(15.0, false, true);
592
593        // Reset should clear spike history
594        synapse.reset_spike_history();
595        assert_eq!(synapse.last_pre_spike(), -1000.0);
596        assert_eq!(synapse.last_post_spike(), -1000.0);
597    }
598
599    #[test]
600    fn test_custom_stdp_parameters() {
601        let synapse = Synapse::with_stdp_params(0, 1, 0.5, 0.05, 10.0, -1.0, 1.0);
602
603        assert_eq!(synapse.stdp_rate(), 0.05);
604        assert_eq!(synapse.stdp_tau(), 10.0);
605        assert_eq!(synapse.weight_bounds(), (-1.0, 1.0));
606    }
607}