Skip to main content

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        // Depression (LTD): post fired before pre, pre fires now
129        // dt = current_time - last_post_spike > 0 means post fired before current pre spike
130        if pre_spiked && self.last_post_spike > current_time - 50.0 {
131            let dt = current_time - self.last_post_spike;
132            if dt > 0.0 {
133                let delta_w = -self.stdp_rate * (-dt / self.stdp_tau).exp();
134                self.weight += delta_w;
135            }
136        }
137
138        // Potentiation (LTP): pre fired before post, post fires now
139        // dt = current_time - last_pre_spike > 0 means pre fired before current post spike
140        if post_spiked && self.last_pre_spike > current_time - 50.0 {
141            let dt = current_time - self.last_pre_spike;
142            if dt > 0.0 {
143                let delta_w = self.stdp_rate * (-dt / self.stdp_tau).exp();
144                self.weight += delta_w;
145            }
146        }
147
148        // Keep weights in reasonable bounds
149        self.weight = self.weight.clamp(self.min_weight, self.max_weight);
150    }
151
152    /// Calculate synaptic current
153    ///
154    /// Computes the current transmitted through this synapse given the
155    /// pre-synaptic spike strength.
156    ///
157    /// # Arguments
158    /// * `pre_spike_strength` - Strength of the pre-synaptic spike
159    ///
160    /// # Returns
161    /// Synaptic current (weight * spike strength)
162    pub fn synaptic_current(&self, pre_spike_strength: f64) -> f64 {
163        self.weight * pre_spike_strength
164    }
165
166    /// Get synaptic weight
167    pub fn weight(&self) -> f64 {
168        self.weight
169    }
170
171    /// Set synaptic weight
172    pub fn set_weight(&mut self, weight: f64) {
173        self.weight = weight.clamp(self.min_weight, self.max_weight);
174    }
175
176    /// Get pre-synaptic neuron ID
177    pub fn pre_neuron(&self) -> usize {
178        self.pre_neuron
179    }
180
181    /// Get post-synaptic neuron ID
182    pub fn post_neuron(&self) -> usize {
183        self.post_neuron
184    }
185
186    /// Get STDP learning rate
187    pub fn stdp_rate(&self) -> f64 {
188        self.stdp_rate
189    }
190
191    /// Set STDP learning rate
192    pub fn set_stdp_rate(&mut self, rate: f64) {
193        self.stdp_rate = rate;
194    }
195
196    /// Get STDP time constant
197    pub fn stdp_tau(&self) -> f64 {
198        self.stdp_tau
199    }
200
201    /// Set STDP time constant
202    pub fn set_stdp_tau(&mut self, tau: f64) {
203        self.stdp_tau = tau;
204    }
205
206    /// Get last pre-synaptic spike time
207    pub fn last_pre_spike(&self) -> f64 {
208        self.last_pre_spike
209    }
210
211    /// Get last post-synaptic spike time
212    pub fn last_post_spike(&self) -> f64 {
213        self.last_post_spike
214    }
215
216    /// Get weight bounds
217    pub fn weight_bounds(&self) -> (f64, f64) {
218        (self.min_weight, self.max_weight)
219    }
220
221    /// Set weight bounds
222    pub fn set_weight_bounds(&mut self, min_weight: f64, max_weight: f64) {
223        self.min_weight = min_weight;
224        self.max_weight = max_weight;
225        // Re-clamp current weight to new bounds
226        self.weight = self.weight.clamp(min_weight, max_weight);
227    }
228
229    /// Check if synapse is excitatory (positive weight)
230    pub fn is_excitatory(&self) -> bool {
231        self.weight > 0.0
232    }
233
234    /// Check if synapse is inhibitory (negative weight)
235    pub fn is_inhibitory(&self) -> bool {
236        self.weight < 0.0
237    }
238
239    /// Reset spike timing history
240    pub fn reset_spike_history(&mut self) {
241        self.last_pre_spike = -1000.0;
242        self.last_post_spike = -1000.0;
243    }
244
245    /// Calculate time since last pre-synaptic spike
246    pub fn time_since_pre_spike(&self, current_time: f64) -> f64 {
247        current_time - self.last_pre_spike
248    }
249
250    /// Calculate time since last post-synaptic spike
251    pub fn time_since_post_spike(&self, current_time: f64) -> f64 {
252        current_time - self.last_post_spike
253    }
254}
255
256/// Metaplastic synapse with history-dependent learning
257///
258/// This synapse extends basic STDP with metaplasticity - the learning rate
259/// itself adapts based on the history of synaptic activity. This provides
260/// more sophisticated learning dynamics.
261#[derive(Debug, Clone)]
262pub struct MetaplasticSynapse {
263    /// Base synapse
264    base_synapse: Synapse,
265    /// Learning rate history
266    learning_history: VecDeque<f64>,
267    /// Maximum history length
268    max_history_length: usize,
269    /// Metaplasticity time constant
270    metaplasticity_tau: f64,
271    /// Base learning rate
272    base_learning_rate: f64,
273}
274
275impl MetaplasticSynapse {
276    /// Create new metaplastic synapse
277    ///
278    /// # Arguments
279    /// * `pre_neuron` - ID of pre-synaptic neuron
280    /// * `post_neuron` - ID of post-synaptic neuron
281    /// * `initial_weight` - Initial synaptic weight
282    /// * `base_learning_rate` - Base STDP learning rate
283    pub fn new(
284        pre_neuron: usize,
285        post_neuron: usize,
286        initial_weight: f64,
287        base_learning_rate: f64,
288    ) -> Self {
289        let mut base_synapse = Synapse::new(pre_neuron, post_neuron, initial_weight);
290        base_synapse.set_stdp_rate(base_learning_rate);
291
292        Self {
293            base_synapse,
294            learning_history: VecDeque::new(),
295            max_history_length: 100,
296            metaplasticity_tau: 100.0,
297            base_learning_rate,
298        }
299    }
300
301    /// Update synapse with metaplasticity
302    ///
303    /// # Arguments
304    /// * `current_time` - Current simulation time
305    /// * `pre_spiked` - Whether pre-synaptic neuron spiked
306    /// * `post_spiked` - Whether post-synaptic neuron spiked
307    pub fn update(&mut self, current_time: f64, pre_spiked: bool, post_spiked: bool) {
308        let old_weight = self.base_synapse.weight();
309
310        // Update base synapse
311        self.base_synapse
312            .update_stdp(current_time, pre_spiked, post_spiked);
313
314        // Calculate weight change
315        let weight_change = (self.base_synapse.weight() - old_weight).abs();
316
317        // Add to learning history
318        self.learning_history.push_back(weight_change);
319        if self.learning_history.len() > self.max_history_length {
320            self.learning_history.pop_front();
321        }
322
323        // Update learning rate based on history
324        self.update_learning_rate();
325    }
326
327    /// Update learning rate based on recent activity
328    fn update_learning_rate(&mut self) {
329        if self.learning_history.is_empty() {
330            return;
331        }
332
333        // Calculate average recent activity
334        let avg_activity: f64 =
335            self.learning_history.iter().sum::<f64>() / self.learning_history.len() as f64;
336
337        // Adapt learning rate: decrease if high activity, increase if low activity
338        let adaptation_factor = (-avg_activity / self.metaplasticity_tau).exp();
339        let new_learning_rate = self.base_learning_rate * adaptation_factor;
340
341        self.base_synapse.set_stdp_rate(new_learning_rate);
342    }
343
344    /// Get reference to base synapse
345    pub fn base_synapse(&self) -> &Synapse {
346        &self.base_synapse
347    }
348
349    /// Get mutable reference to base synapse
350    pub fn base_synapse_mut(&mut self) -> &mut Synapse {
351        &mut self.base_synapse
352    }
353
354    /// Get current learning rate
355    pub fn current_learning_rate(&self) -> f64 {
356        self.base_synapse.stdp_rate()
357    }
358
359    /// Get average recent activity
360    pub fn average_recent_activity(&self) -> f64 {
361        if self.learning_history.is_empty() {
362            0.0
363        } else {
364            self.learning_history.iter().sum::<f64>() / self.learning_history.len() as f64
365        }
366    }
367
368    /// Reset metaplasticity history
369    pub fn reset_history(&mut self) {
370        self.learning_history.clear();
371        self.base_synapse.set_stdp_rate(self.base_learning_rate);
372    }
373}
374
375/// Homeostatic synapse with activity-dependent scaling
376///
377/// This synapse implements homeostatic scaling to maintain stable activity levels
378/// by globally scaling synaptic weights based on the target activity level.
379#[derive(Debug, Clone)]
380pub struct HomeostaticSynapse {
381    /// Base synapse
382    base_synapse: Synapse,
383    /// Target activity level
384    target_activity: f64,
385    /// Current activity estimate
386    current_activity: f64,
387    /// Homeostatic scaling rate
388    scaling_rate: f64,
389    /// Activity estimation time constant
390    activity_tau: f64,
391}
392
393impl HomeostaticSynapse {
394    /// Create new homeostatic synapse
395    ///
396    /// # Arguments
397    /// * `pre_neuron` - ID of pre-synaptic neuron
398    /// * `post_neuron` - ID of post-synaptic neuron
399    /// * `initial_weight` - Initial synaptic weight
400    /// * `target_activity` - Target activity level for homeostasis
401    pub fn new(
402        pre_neuron: usize,
403        post_neuron: usize,
404        initial_weight: f64,
405        target_activity: f64,
406    ) -> Self {
407        Self {
408            base_synapse: Synapse::new(pre_neuron, post_neuron, initial_weight),
409            target_activity,
410            current_activity: 0.0,
411            scaling_rate: 0.001,
412            activity_tau: 1000.0,
413        }
414    }
415
416    /// Update synapse with homeostatic scaling
417    ///
418    /// # Arguments
419    /// * `current_time` - Current simulation time
420    /// * `pre_spiked` - Whether pre-synaptic neuron spiked
421    /// * `post_spiked` - Whether post-synaptic neuron spiked
422    /// * `dt` - Time step size
423    pub fn update(&mut self, current_time: f64, pre_spiked: bool, post_spiked: bool, dt: f64) {
424        // Update base STDP
425        self.base_synapse
426            .update_stdp(current_time, pre_spiked, post_spiked);
427
428        // Update activity estimate
429        let activity_input = if post_spiked { 1.0 } else { 0.0 };
430        let decay = (-dt / self.activity_tau).exp();
431        self.current_activity = decay * self.current_activity + (1.0 - decay) * activity_input;
432
433        // Apply homeostatic scaling
434        let activity_error = self.current_activity - self.target_activity;
435        let scaling_factor = 1.0 - self.scaling_rate * activity_error;
436
437        let current_weight = self.base_synapse.weight();
438        self.base_synapse
439            .set_weight(current_weight * scaling_factor);
440    }
441
442    /// Get reference to base synapse
443    pub fn base_synapse(&self) -> &Synapse {
444        &self.base_synapse
445    }
446
447    /// Get current activity estimate
448    pub fn current_activity(&self) -> f64 {
449        self.current_activity
450    }
451
452    /// Get target activity
453    pub fn target_activity(&self) -> f64 {
454        self.target_activity
455    }
456
457    /// Set target activity
458    pub fn set_target_activity(&mut self, target: f64) {
459        self.target_activity = target;
460    }
461
462    /// Get activity error (current - target)
463    pub fn activity_error(&self) -> f64 {
464        self.current_activity - self.target_activity
465    }
466}
467
468#[cfg(test)]
469mod tests {
470    use super::*;
471
472    #[test]
473    fn test_synapse_creation() {
474        let synapse = Synapse::new(0, 1, 0.5);
475        assert_eq!(synapse.pre_neuron(), 0);
476        assert_eq!(synapse.post_neuron(), 1);
477        assert_eq!(synapse.weight(), 0.5);
478        assert!(synapse.is_excitatory());
479        assert!(!synapse.is_inhibitory());
480    }
481
482    #[test]
483    fn test_stdp_potentiation() {
484        let mut synapse = Synapse::new(0, 1, 0.5);
485        let initial_weight = synapse.weight();
486
487        // Pre-synaptic spike before post-synaptic spike (potentiation)
488        synapse.update_stdp(10.0, true, false); // Pre spike at t=10
489        synapse.update_stdp(15.0, false, true); // Post spike at t=15
490
491        // Weight should increase
492        assert!(synapse.weight() > initial_weight);
493    }
494
495    #[test]
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    fn test_metaplastic_synapse() {
552        let mut meta_synapse = MetaplasticSynapse::new(0, 1, 0.5, 0.01);
553
554        assert_eq!(meta_synapse.current_learning_rate(), 0.01);
555        assert_eq!(meta_synapse.average_recent_activity(), 0.0);
556
557        // Simulate some activity
558        for i in 0..10 {
559            meta_synapse.update(i as f64, true, false);
560            meta_synapse.update(i as f64 + 0.5, false, true);
561        }
562
563        // Learning rate should adapt based on activity
564        assert!(meta_synapse.average_recent_activity() > 0.0);
565    }
566
567    #[test]
568    fn test_homeostatic_synapse() {
569        let mut homeostatic = HomeostaticSynapse::new(0, 1, 0.5, 0.1);
570
571        assert_eq!(homeostatic.target_activity(), 0.1);
572        assert_eq!(homeostatic.current_activity(), 0.0);
573
574        // Simulate high activity
575        for _ in 0..50 {
576            homeostatic.update(1.0, true, true, 0.1);
577        }
578
579        // Activity should increase
580        assert!(homeostatic.current_activity() > 0.0);
581        assert!(homeostatic.activity_error() != 0.0);
582    }
583
584    #[test]
585    fn test_synapse_reset() {
586        let mut synapse = Synapse::new(0, 1, 0.5);
587
588        // Record some spike activity
589        synapse.update_stdp(10.0, true, false);
590        synapse.update_stdp(15.0, false, true);
591
592        // Reset should clear spike history
593        synapse.reset_spike_history();
594        assert_eq!(synapse.last_pre_spike(), -1000.0);
595        assert_eq!(synapse.last_post_spike(), -1000.0);
596    }
597
598    #[test]
599    fn test_custom_stdp_parameters() {
600        let synapse = Synapse::with_stdp_params(0, 1, 0.5, 0.05, 10.0, -1.0, 1.0);
601
602        assert_eq!(synapse.stdp_rate(), 0.05);
603        assert_eq!(synapse.stdp_tau(), 10.0);
604        assert_eq!(synapse.weight_bounds(), (-1.0, 1.0));
605    }
606}