synapse_models/
synapse.rs

1//! Complete synapse model integrating all components.
2//!
3//! This module provides a comprehensive synapse model that combines:
4//! - Presynaptic mechanisms (vesicle release)
5//! - Neurotransmitter dynamics
6//! - Postsynaptic receptors
7//! - Short-term plasticity
8//! - Long-term plasticity
9//! - Calcium dynamics
10
11use crate::calcium::CalciumDynamics;
12use crate::error::{Result, SynapseError};
13use crate::neurotransmitter::Neurotransmitter;
14use crate::plasticity::STDP;
15use crate::receptor::{AMPAReceptor, GABAAReceptor, NMDAReceptor, ReceptorDynamics};
16use crate::vesicle::VesiclePool;
17
18/// Type of synapse.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum SynapseType {
21    /// Excitatory synapse (e.g., glutamatergic).
22    Excitatory,
23    /// Inhibitory synapse (e.g., GABAergic).
24    Inhibitory,
25    /// Modulatory synapse (e.g., dopaminergic).
26    Modulatory,
27}
28
29/// Complete synapse model.
30///
31/// Integrates all components of synaptic transmission:
32/// - Presynaptic vesicle release
33/// - Neurotransmitter diffusion and clearance
34/// - Postsynaptic receptor activation
35/// - Short-term plasticity (depression/facilitation)
36/// - Long-term plasticity (STDP)
37/// - Calcium dynamics
38#[derive(Debug, Clone)]
39pub struct Synapse {
40    /// Synapse type.
41    pub synapse_type: SynapseType,
42
43    /// Synaptic weight (0 to w_max).
44    pub weight: f64,
45
46    /// Maximum synaptic weight.
47    pub w_max: f64,
48
49    /// Synaptic delay (ms).
50    pub delay: f64,
51
52    /// Presynaptic vesicle pool.
53    pub vesicle_pool: VesiclePool,
54
55    /// Neurotransmitter in synaptic cleft.
56    pub neurotransmitter: Neurotransmitter,
57
58    /// AMPA receptor (for excitatory synapses).
59    pub ampa: Option<AMPAReceptor>,
60
61    /// NMDA receptor (for excitatory synapses).
62    pub nmda: Option<NMDAReceptor>,
63
64    /// GABA-A receptor (for inhibitory synapses).
65    pub gabaa: Option<GABAAReceptor>,
66
67    /// STDP plasticity rule.
68    pub stdp: Option<STDP>,
69
70    /// Presynaptic calcium dynamics.
71    pub presynaptic_calcium: CalciumDynamics,
72
73    /// Postsynaptic calcium dynamics.
74    pub postsynaptic_calcium: CalciumDynamics,
75
76    /// Last presynaptic spike time (ms).
77    last_pre_spike_time: Option<f64>,
78
79    /// Last postsynaptic spike time (ms).
80    last_post_spike_time: Option<f64>,
81
82    /// Current postsynaptic voltage (mV).
83    postsynaptic_voltage: f64,
84
85    /// Pending spike (arrives after delay).
86    pending_spike: Option<f64>,
87}
88
89impl Synapse {
90    /// Create a new excitatory (glutamatergic) synapse.
91    pub fn excitatory(weight: f64, delay: f64) -> Result<Self> {
92        if weight < 0.0 {
93            return Err(SynapseError::InvalidWeight(weight, 0.0, f64::INFINITY));
94        }
95        if delay < 0.0 {
96            return Err(SynapseError::InvalidDelay(delay));
97        }
98
99        Ok(Self {
100            synapse_type: SynapseType::Excitatory,
101            weight,
102            w_max: 2.0,
103            delay,
104            vesicle_pool: VesiclePool::new(),
105            neurotransmitter: Neurotransmitter::glutamate(),
106            ampa: Some(AMPAReceptor::new()),
107            nmda: Some(NMDAReceptor::new()),
108            gabaa: None,
109            stdp: Some(STDP::new()),
110            presynaptic_calcium: CalciumDynamics::presynaptic(),
111            postsynaptic_calcium: CalciumDynamics::postsynaptic(),
112            last_pre_spike_time: None,
113            last_post_spike_time: None,
114            postsynaptic_voltage: -65.0,
115            pending_spike: None,
116        })
117    }
118
119    /// Create a new inhibitory (GABAergic) synapse.
120    pub fn inhibitory(weight: f64, delay: f64) -> Result<Self> {
121        if weight < 0.0 {
122            return Err(SynapseError::InvalidWeight(weight, 0.0, f64::INFINITY));
123        }
124        if delay < 0.0 {
125            return Err(SynapseError::InvalidDelay(delay));
126        }
127
128        Ok(Self {
129            synapse_type: SynapseType::Inhibitory,
130            weight,
131            w_max: 2.0,
132            delay,
133            vesicle_pool: VesiclePool::new(),
134            neurotransmitter: Neurotransmitter::gaba(),
135            ampa: None,
136            nmda: None,
137            gabaa: Some(GABAAReceptor::new()),
138            stdp: Some(STDP::new()),
139            presynaptic_calcium: CalciumDynamics::presynaptic(),
140            postsynaptic_calcium: CalciumDynamics::postsynaptic(),
141            last_pre_spike_time: None,
142            last_post_spike_time: None,
143            postsynaptic_voltage: -65.0,
144            pending_spike: None,
145        })
146    }
147
148    /// Create depressing excitatory synapse.
149    pub fn depressing_excitatory(weight: f64, delay: f64) -> Result<Self> {
150        let mut syn = Self::excitatory(weight, delay)?;
151        syn.vesicle_pool = VesiclePool::depressing();
152        Ok(syn)
153    }
154
155    /// Create facilitating excitatory synapse.
156    pub fn facilitating_excitatory(weight: f64, delay: f64) -> Result<Self> {
157        let mut syn = Self::excitatory(weight, delay)?;
158        syn.vesicle_pool = VesiclePool::facilitating();
159        Ok(syn)
160    }
161
162    /// Process presynaptic spike.
163    ///
164    /// # Arguments
165    /// * `time` - Current time (ms)
166    pub fn presynaptic_spike(&mut self, time: f64) -> Result<()> {
167        self.last_pre_spike_time = Some(time);
168
169        // Schedule spike to arrive after delay
170        self.pending_spike = Some(time + self.delay);
171
172        // Note: Presynaptic calcium spike happens when spike arrives (in update),
173        // not here, to properly model the delay in calcium influx
174
175        // Apply STDP if enabled
176        if let Some(stdp) = &mut self.stdp {
177            let _dw = stdp.pre_spike(time, self.weight);
178            self.weight = stdp.apply_update(self.weight);
179        }
180
181        Ok(())
182    }
183
184    /// Process postsynaptic spike.
185    ///
186    /// # Arguments
187    /// * `time` - Current time (ms)
188    pub fn postsynaptic_spike(&mut self, time: f64) -> Result<()> {
189        self.last_post_spike_time = Some(time);
190
191        // Update postsynaptic calcium
192        self.postsynaptic_calcium.spike();
193
194        // Apply STDP if enabled
195        if let Some(stdp) = &mut self.stdp {
196            let _dw = stdp.post_spike(time, self.weight);
197            self.weight = stdp.apply_update(self.weight);
198        }
199
200        Ok(())
201    }
202
203    /// Update synapse dynamics.
204    ///
205    /// # Arguments
206    /// * `time` - Current time (ms)
207    /// * `postsynaptic_voltage` - Current postsynaptic membrane voltage (mV)
208    /// * `dt` - Time step (ms)
209    pub fn update(&mut self, time: f64, postsynaptic_voltage: f64, dt: f64) -> Result<()> {
210        self.postsynaptic_voltage = postsynaptic_voltage;
211
212        // Check if delayed spike arrives
213        if let Some(spike_time) = self.pending_spike {
214            if time >= spike_time {
215                // Spike arrives - trigger calcium influx at presynaptic terminal
216                self.presynaptic_calcium.spike();
217
218                // Release vesicles based on calcium
219                let ca_conc = self.presynaptic_calcium.get_concentration();
220                let vesicles_released = self.vesicle_pool.release(ca_conc)?;
221
222                // Release neurotransmitter if vesicles were released
223                if vesicles_released > 0 {
224                    self.neurotransmitter.release();
225                }
226
227                self.pending_spike = None;
228            }
229        }
230
231        // Update vesicle pool dynamics
232        self.vesicle_pool.update(dt)?;
233
234        // Update neurotransmitter concentration
235        self.neurotransmitter.update(dt)?;
236
237        // Get neurotransmitter concentration
238        let nt_conc = self.neurotransmitter.get_concentration();
239
240        // Update receptors
241        if let Some(ampa) = &mut self.ampa {
242            ampa.update(nt_conc, postsynaptic_voltage, dt)?;
243        }
244        if let Some(nmda) = &mut self.nmda {
245            nmda.update(nt_conc, postsynaptic_voltage, dt)?;
246
247            // NMDA receptors allow calcium influx
248            let nmda_current = nmda.current(postsynaptic_voltage);
249            let ca_influx = nmda_current.abs() * 0.001; // Convert to calcium (simplified)
250            self.postsynaptic_calcium.add_influx(ca_influx)?;
251        }
252        if let Some(gabaa) = &mut self.gabaa {
253            gabaa.update(nt_conc, postsynaptic_voltage, dt)?;
254        }
255
256        // Update calcium dynamics
257        self.presynaptic_calcium.update(0.0, dt)?;
258        self.postsynaptic_calcium.update(0.0, dt)?;
259
260        Ok(())
261    }
262
263    /// Get total postsynaptic current.
264    ///
265    /// # Arguments
266    /// * `voltage` - Postsynaptic membrane voltage (mV)
267    ///
268    /// # Returns
269    /// Synaptic current (pA)
270    pub fn current(&self, voltage: f64) -> f64 {
271        let mut total_current = 0.0;
272
273        if let Some(ampa) = &self.ampa {
274            total_current += ampa.current(voltage);
275        }
276        if let Some(nmda) = &self.nmda {
277            total_current += nmda.current(voltage);
278        }
279        if let Some(gabaa) = &self.gabaa {
280            total_current += gabaa.current(voltage);
281        }
282
283        total_current * self.weight
284    }
285
286    /// Get total postsynaptic conductance.
287    ///
288    /// # Returns
289    /// Synaptic conductance (nS)
290    pub fn conductance(&self) -> f64 {
291        let mut total_conductance = 0.0;
292
293        if let Some(ampa) = &self.ampa {
294            total_conductance += ampa.get_conductance();
295        }
296        if let Some(nmda) = &self.nmda {
297            total_conductance += nmda.get_conductance();
298        }
299        if let Some(gabaa) = &self.gabaa {
300            total_conductance += gabaa.get_conductance();
301        }
302
303        total_conductance * self.weight
304    }
305
306    /// Get effective synaptic strength (includes short-term plasticity).
307    pub fn effective_weight(&self) -> f64 {
308        self.weight * self.vesicle_pool.release_probability()
309    }
310
311    /// Set synaptic weight.
312    pub fn set_weight(&mut self, weight: f64) -> Result<()> {
313        if weight < 0.0 || weight > self.w_max {
314            return Err(SynapseError::InvalidWeight(weight, 0.0, self.w_max));
315        }
316        self.weight = weight;
317        Ok(())
318    }
319
320    /// Enable STDP plasticity.
321    pub fn enable_stdp(&mut self, a_plus: f64, a_minus: f64, tau_plus: f64, tau_minus: f64) -> Result<()> {
322        self.stdp = Some(STDP::with_params(a_plus, a_minus, tau_plus, tau_minus)?);
323        Ok(())
324    }
325
326    /// Disable STDP plasticity.
327    pub fn disable_stdp(&mut self) {
328        self.stdp = None;
329    }
330
331    /// Reset synapse to initial state.
332    pub fn reset(&mut self) {
333        self.vesicle_pool.reset();
334        self.neurotransmitter.reset();
335
336        if let Some(ampa) = &mut self.ampa {
337            ampa.reset();
338        }
339        if let Some(nmda) = &mut self.nmda {
340            nmda.reset();
341        }
342        if let Some(gabaa) = &mut self.gabaa {
343            gabaa.reset();
344        }
345        if let Some(stdp) = &mut self.stdp {
346            stdp.reset();
347        }
348
349        self.presynaptic_calcium.reset();
350        self.postsynaptic_calcium.reset();
351        self.last_pre_spike_time = None;
352        self.last_post_spike_time = None;
353        self.pending_spike = None;
354    }
355}
356
357/// Builder for creating custom synapses.
358pub struct SynapseBuilder {
359    synapse_type: SynapseType,
360    weight: f64,
361    w_max: f64,
362    delay: f64,
363    vesicle_pool: VesiclePool,
364    neurotransmitter: Neurotransmitter,
365    enable_stdp: bool,
366}
367
368impl SynapseBuilder {
369    /// Create a new synapse builder.
370    pub fn new(synapse_type: SynapseType) -> Self {
371        Self {
372            synapse_type,
373            weight: 1.0,
374            w_max: 2.0,
375            delay: 1.0,
376            vesicle_pool: VesiclePool::new(),
377            neurotransmitter: Neurotransmitter::glutamate(),
378            enable_stdp: true,
379        }
380    }
381
382    /// Set synaptic weight.
383    pub fn weight(mut self, weight: f64) -> Self {
384        self.weight = weight;
385        self
386    }
387
388    /// Set maximum weight.
389    pub fn max_weight(mut self, w_max: f64) -> Self {
390        self.w_max = w_max;
391        self
392    }
393
394    /// Set synaptic delay.
395    pub fn delay(mut self, delay: f64) -> Self {
396        self.delay = delay;
397        self
398    }
399
400    /// Set vesicle pool dynamics.
401    pub fn vesicle_pool(mut self, pool: VesiclePool) -> Self {
402        self.vesicle_pool = pool;
403        self
404    }
405
406    /// Set neurotransmitter.
407    pub fn neurotransmitter(mut self, nt: Neurotransmitter) -> Self {
408        self.neurotransmitter = nt;
409        self
410    }
411
412    /// Enable or disable STDP.
413    pub fn stdp(mut self, enable: bool) -> Self {
414        self.enable_stdp = enable;
415        self
416    }
417
418    /// Build the synapse.
419    pub fn build(self) -> Result<Synapse> {
420        if self.weight < 0.0 {
421            return Err(SynapseError::InvalidWeight(self.weight, 0.0, self.w_max));
422        }
423        if self.delay < 0.0 {
424            return Err(SynapseError::InvalidDelay(self.delay));
425        }
426
427        let (ampa, nmda, gabaa) = match self.synapse_type {
428            SynapseType::Excitatory => (Some(AMPAReceptor::new()), Some(NMDAReceptor::new()), None),
429            SynapseType::Inhibitory => (None, None, Some(GABAAReceptor::new())),
430            SynapseType::Modulatory => (None, None, None),
431        };
432
433        Ok(Synapse {
434            synapse_type: self.synapse_type,
435            weight: self.weight,
436            w_max: self.w_max,
437            delay: self.delay,
438            vesicle_pool: self.vesicle_pool,
439            neurotransmitter: self.neurotransmitter,
440            ampa,
441            nmda,
442            gabaa,
443            stdp: if self.enable_stdp { Some(STDP::new()) } else { None },
444            presynaptic_calcium: CalciumDynamics::presynaptic(),
445            postsynaptic_calcium: CalciumDynamics::postsynaptic(),
446            last_pre_spike_time: None,
447            last_post_spike_time: None,
448            postsynaptic_voltage: -65.0,
449            pending_spike: None,
450        })
451    }
452}
453
454#[cfg(test)]
455mod tests {
456    use super::*;
457
458    #[test]
459    fn test_excitatory_synapse_creation() {
460        let syn = Synapse::excitatory(1.0, 1.0).unwrap();
461        assert_eq!(syn.synapse_type, SynapseType::Excitatory);
462        assert!(syn.ampa.is_some());
463        assert!(syn.nmda.is_some());
464        assert!(syn.gabaa.is_none());
465    }
466
467    #[test]
468    fn test_inhibitory_synapse_creation() {
469        let syn = Synapse::inhibitory(1.0, 1.0).unwrap();
470        assert_eq!(syn.synapse_type, SynapseType::Inhibitory);
471        assert!(syn.gabaa.is_some());
472        assert!(syn.ampa.is_none());
473        assert!(syn.nmda.is_none());
474    }
475
476    #[test]
477    fn test_synaptic_transmission() {
478        let mut syn = Synapse::excitatory(1.0, 1.0).unwrap();
479
480        // Presynaptic spike
481        syn.presynaptic_spike(0.0).unwrap();
482
483        // Update to allow spike to propagate and activate receptors
484        for t in 0..50 {
485            syn.update(t as f64 * 0.1, -65.0, 0.1).unwrap();
486        }
487
488        // Should have some conductance or neurotransmitter
489        let conductance = syn.conductance();
490        let nt_conc = syn.neurotransmitter.get_concentration();
491        assert!(conductance > 0.0 || nt_conc > 0.0, "conductance: {}, nt_conc: {}", conductance, nt_conc);
492    }
493
494    #[test]
495    fn test_synaptic_delay() {
496        let mut syn = Synapse::excitatory(1.0, 5.0).unwrap(); // 5 ms delay
497
498        syn.presynaptic_spike(0.0).unwrap();
499
500        // At t=1ms, no conductance yet
501        syn.update(1.0, -65.0, 1.0).unwrap();
502        assert_eq!(syn.conductance(), 0.0);
503
504        // At t=6ms and beyond, should have conductance after receptors activate
505        for t in 6..20 {
506            syn.update(t as f64, -65.0, 0.5).unwrap();
507        }
508        assert!(syn.conductance() > 0.0 || syn.neurotransmitter.get_concentration() > 0.0);
509    }
510
511    #[test]
512    fn test_short_term_depression() {
513        let mut syn = Synapse::depressing_excitatory(1.0, 1.0).unwrap();
514
515        let initial_prob = syn.vesicle_pool.release_probability();
516
517        // Multiple rapid spikes
518        for i in 0..5 {
519            syn.presynaptic_spike(i as f64 * 10.0).unwrap();
520            syn.update((i as f64 + 1.0) * 10.0, -65.0, 10.0).unwrap();
521        }
522
523        // Release probability should decrease
524        assert!(syn.vesicle_pool.release_probability() < initial_prob);
525    }
526
527    #[test]
528    fn test_stdp_potentiation() {
529        let mut syn = Synapse::excitatory(0.5, 1.0).unwrap();
530
531        let initial_weight = syn.weight;
532
533        // Pre before post -> potentiation
534        syn.presynaptic_spike(0.0).unwrap();
535        syn.postsynaptic_spike(10.0).unwrap();
536
537        // Weight should increase
538        assert!(syn.weight > initial_weight);
539    }
540
541    #[test]
542    fn test_stdp_depression() {
543        let mut syn = Synapse::excitatory(0.5, 1.0).unwrap();
544
545        let initial_weight = syn.weight;
546
547        // Post before pre -> depression
548        syn.postsynaptic_spike(0.0).unwrap();
549        syn.presynaptic_spike(10.0).unwrap();
550
551        // Weight should decrease
552        assert!(syn.weight < initial_weight);
553    }
554
555    #[test]
556    fn test_synapse_builder() {
557        let syn = SynapseBuilder::new(SynapseType::Excitatory)
558            .weight(1.5)
559            .delay(2.0)
560            .stdp(false)
561            .build()
562            .unwrap();
563
564        assert_eq!(syn.weight, 1.5);
565        assert_eq!(syn.delay, 2.0);
566        assert!(syn.stdp.is_none());
567    }
568
569    #[test]
570    fn test_synapse_reset() {
571        let mut syn = Synapse::excitatory(1.0, 1.0).unwrap();
572
573        syn.presynaptic_spike(0.0).unwrap();
574        syn.update(2.0, -65.0, 2.0).unwrap();
575
576        syn.reset();
577
578        assert_eq!(syn.conductance(), 0.0);
579        assert_eq!(syn.last_pre_spike_time, None);
580    }
581}