synapse_models/
vesicle.rs

1//! Vesicle pool dynamics and neurotransmitter release mechanisms.
2//!
3//! This module implements the dynamics of synaptic vesicle pools including:
4//! - Ready Releasable Pool (RRP)
5//! - Reserve pool
6//! - Recycling pool
7//! - Calcium-dependent release
8//! - Short-term depression and facilitation
9
10use crate::error::{Result, SynapseError};
11
12/// Vesicle pool dynamics implementing the Tsodyks-Markram model.
13///
14/// The model tracks three pools of vesicles:
15/// 1. Ready Releasable Pool (RRP) - immediately available for release
16/// 2. Reserve pool - can be recruited to RRP
17/// 3. Released/recycling pool - recovering from release
18///
19/// Key equations:
20/// - dx/dt = (1 - x)/τ_rec - U*x*δ(t_spike)  (recovery from depression)
21/// - du/dt = (U₀ - u)/τ_facil + U₀(1-u)δ(t_spike)  (facilitation)
22#[derive(Debug, Clone)]
23pub struct VesiclePool {
24    /// Fraction of available vesicles (0 to 1).
25    pub available_fraction: f64,
26
27    /// Utilization parameter (effective release probability, 0 to 1).
28    pub utilization: f64,
29
30    /// Baseline utilization (U₀).
31    pub baseline_utilization: f64,
32
33    /// Recovery time constant from depression (ms).
34    pub tau_recovery: f64,
35
36    /// Facilitation time constant (ms).
37    pub tau_facilitation: f64,
38
39    /// Total number of vesicles in readily releasable pool.
40    pub total_vesicles: usize,
41
42    /// Number of docked vesicles ready for release.
43    pub docked_vesicles: usize,
44
45    /// Reserve pool size.
46    pub reserve_pool: usize,
47
48    /// Recycling pool size.
49    pub recycling_pool: usize,
50
51    /// Calcium sensitivity exponent.
52    pub calcium_cooperativity: f64,
53
54    /// Half-maximal calcium concentration for release (μM).
55    pub calcium_half_max: f64,
56}
57
58impl Default for VesiclePool {
59    fn default() -> Self {
60        Self {
61            available_fraction: 1.0,
62            utilization: 0.5,
63            baseline_utilization: 0.5,
64            tau_recovery: 800.0,      // 800 ms recovery
65            tau_facilitation: 1000.0, // 1000 ms facilitation decay
66            total_vesicles: 100,
67            docked_vesicles: 10,
68            reserve_pool: 80,
69            recycling_pool: 0,
70            calcium_cooperativity: 4.0, // Hill coefficient
71            calcium_half_max: 1.0,      // 1 μM
72        }
73    }
74}
75
76impl VesiclePool {
77    /// Create a new vesicle pool with default parameters.
78    pub fn new() -> Self {
79        Self::default()
80    }
81
82    /// Create a vesicle pool with custom parameters.
83    ///
84    /// # Arguments
85    /// * `baseline_u` - Baseline utilization (0 to 1)
86    /// * `tau_rec` - Recovery time constant (ms)
87    /// * `tau_facil` - Facilitation time constant (ms)
88    pub fn with_params(baseline_u: f64, tau_rec: f64, tau_facil: f64) -> Result<Self> {
89        if !(0.0..=1.0).contains(&baseline_u) {
90            return Err(SynapseError::InvalidProbability(baseline_u));
91        }
92        if tau_rec <= 0.0 {
93            return Err(SynapseError::InvalidTimeConstant(tau_rec));
94        }
95        if tau_facil <= 0.0 {
96            return Err(SynapseError::InvalidTimeConstant(tau_facil));
97        }
98
99        Ok(Self {
100            baseline_utilization: baseline_u,
101            utilization: baseline_u,
102            tau_recovery: tau_rec,
103            tau_facilitation: tau_facil,
104            ..Self::default()
105        })
106    }
107
108    /// Create a depressing synapse (high U, fast recovery).
109    ///
110    /// Typical for cortical excitatory synapses.
111    pub fn depressing() -> Self {
112        Self {
113            baseline_utilization: 0.6,
114            utilization: 0.6,
115            tau_recovery: 130.0,
116            tau_facilitation: 530.0,
117            ..Self::default()
118        }
119    }
120
121    /// Create a facilitating synapse (low U, slow recovery).
122    ///
123    /// Typical for some cortical and hippocampal synapses.
124    pub fn facilitating() -> Self {
125        Self {
126            baseline_utilization: 0.15,
127            utilization: 0.15,
128            tau_recovery: 670.0,
129            tau_facilitation: 17.0,
130            ..Self::default()
131        }
132    }
133
134    /// Update vesicle pool dynamics over time.
135    ///
136    /// Implements continuous recovery and facilitation decay.
137    ///
138    /// # Arguments
139    /// * `dt` - Time step (ms)
140    pub fn update(&mut self, dt: f64) -> Result<()> {
141        // Recovery from depression: dx/dt = (1 - x)/τ_rec
142        let dx = (1.0 - self.available_fraction) / self.tau_recovery;
143        self.available_fraction += dx * dt;
144        self.available_fraction = self.available_fraction.clamp(0.0, 1.0);
145
146        // Facilitation decay: du/dt = (U₀ - u)/τ_facil
147        let du = (self.baseline_utilization - self.utilization) / self.tau_facilitation;
148        self.utilization += du * dt;
149        self.utilization = self.utilization.clamp(0.0, 1.0);
150
151        // Update vesicle pool sizes
152        let total = self.total_vesicles as f64;
153        self.docked_vesicles = (self.available_fraction * total * 0.1) as usize;
154        self.reserve_pool = ((1.0 - self.available_fraction) * total * 0.8) as usize;
155        self.recycling_pool = self.total_vesicles - self.docked_vesicles - self.reserve_pool;
156
157        Ok(())
158    }
159
160    /// Calculate release probability given calcium concentration.
161    ///
162    /// Uses Hill equation: P = [Ca]^n / ([Ca]^n + K^n)
163    ///
164    /// # Arguments
165    /// * `calcium_concentration` - Presynaptic calcium concentration (μM)
166    pub fn calcium_release_probability(&self, calcium_concentration: f64) -> f64 {
167        let ca_n = calcium_concentration.powf(self.calcium_cooperativity);
168        let k_n = self.calcium_half_max.powf(self.calcium_cooperativity);
169        ca_n / (ca_n + k_n)
170    }
171
172    /// Trigger vesicle release (spike arrives).
173    ///
174    /// Updates both depression and facilitation according to Tsodyks-Markram model.
175    ///
176    /// # Arguments
177    /// * `calcium_concentration` - Presynaptic calcium concentration (μM)
178    ///
179    /// # Returns
180    /// Number of vesicles released
181    pub fn release(&mut self, calcium_concentration: f64) -> Result<usize> {
182        if calcium_concentration < 0.0 {
183            return Err(SynapseError::InvalidConcentration(calcium_concentration));
184        }
185
186        // Calculate calcium-dependent release probability
187        let ca_prob = self.calcium_release_probability(calcium_concentration);
188
189        // Effective release probability combines utilization and calcium
190        let release_prob = self.utilization * ca_prob;
191
192        // Number of vesicles released
193        let vesicles_released = (self.available_fraction * release_prob * self.total_vesicles as f64) as usize;
194
195        // Update depression: x → x - U*x
196        self.available_fraction *= 1.0 - self.utilization;
197        self.available_fraction = self.available_fraction.clamp(0.0, 1.0);
198
199        // Update facilitation: u → u + U₀(1-u)
200        self.utilization += self.baseline_utilization * (1.0 - self.utilization);
201        self.utilization = self.utilization.clamp(0.0, 1.0);
202
203        // Update pool counts
204        if vesicles_released <= self.docked_vesicles {
205            self.docked_vesicles -= vesicles_released;
206            self.recycling_pool += vesicles_released;
207        } else {
208            self.recycling_pool += self.docked_vesicles;
209            self.docked_vesicles = 0;
210        }
211
212        Ok(vesicles_released)
213    }
214
215    /// Get current release probability (without calcium dependence).
216    pub fn release_probability(&self) -> f64 {
217        self.available_fraction * self.utilization
218    }
219
220    /// Reset vesicle pool to initial state.
221    pub fn reset(&mut self) {
222        self.available_fraction = 1.0;
223        self.utilization = self.baseline_utilization;
224        self.docked_vesicles = (self.total_vesicles as f64 * 0.1) as usize;
225        self.reserve_pool = (self.total_vesicles as f64 * 0.8) as usize;
226        self.recycling_pool = 0;
227    }
228}
229
230/// Quantal release model for vesicle fusion.
231///
232/// Models individual vesicle release events with binomial statistics.
233#[derive(Debug, Clone)]
234pub struct QuantalRelease {
235    /// Number of release sites (N).
236    pub n_sites: usize,
237
238    /// Release probability per site (p).
239    pub release_probability: f64,
240
241    /// Quantal size (postsynaptic response per vesicle, pA or mV).
242    pub quantal_size: f64,
243
244    /// Variance in quantal size.
245    pub quantal_variance: f64,
246}
247
248impl QuantalRelease {
249    /// Create a new quantal release model.
250    pub fn new(n_sites: usize, release_probability: f64, quantal_size: f64) -> Result<Self> {
251        if !(0.0..=1.0).contains(&release_probability) {
252            return Err(SynapseError::InvalidProbability(release_probability));
253        }
254
255        Ok(Self {
256            n_sites,
257            release_probability,
258            quantal_size,
259            quantal_variance: quantal_size * 0.3, // 30% CV
260        })
261    }
262
263    /// Calculate expected number of vesicles released.
264    pub fn expected_release(&self) -> f64 {
265        self.n_sites as f64 * self.release_probability
266    }
267
268    /// Calculate variance in number of vesicles released (binomial).
269    pub fn release_variance(&self) -> f64 {
270        self.n_sites as f64 * self.release_probability * (1.0 - self.release_probability)
271    }
272
273    /// Calculate expected postsynaptic response amplitude.
274    pub fn expected_amplitude(&self) -> f64 {
275        self.expected_release() * self.quantal_size
276    }
277
278    /// Calculate coefficient of variation (CV) of response.
279    pub fn coefficient_of_variation(&self) -> f64 {
280        if self.expected_release() == 0.0 {
281            return f64::INFINITY;
282        }
283
284        let release_cv = (self.release_variance() / self.expected_release().powi(2)).sqrt();
285        let quantal_cv = self.quantal_variance / self.quantal_size;
286
287        (release_cv.powi(2) + quantal_cv.powi(2)).sqrt()
288    }
289}
290
291/// Multi-vesicular release (MVR) model.
292///
293/// Models the probability of releasing multiple vesicles from a single release site.
294#[derive(Debug, Clone)]
295pub struct MultiVesicularRelease {
296    /// Probability of releasing 0, 1, 2, ... vesicles per site.
297    pub release_probabilities: Vec<f64>,
298
299    /// Maximum number of vesicles per site.
300    pub max_vesicles_per_site: usize,
301}
302
303impl MultiVesicularRelease {
304    /// Create MVR model with Poisson distribution.
305    ///
306    /// # Arguments
307    /// * `mean_vesicles` - Mean number of vesicles released per site
308    /// * `max_vesicles` - Maximum vesicles to consider per site
309    pub fn poisson(mean_vesicles: f64, max_vesicles: usize) -> Self {
310        let mut probs = Vec::with_capacity(max_vesicles + 1);
311
312        // Calculate Poisson probabilities
313        for k in 0..=max_vesicles {
314            let p = (mean_vesicles.powi(k as i32) * (-mean_vesicles).exp())
315                    / Self::factorial(k) as f64;
316            probs.push(p);
317        }
318
319        // Normalize
320        let sum: f64 = probs.iter().sum();
321        probs.iter_mut().for_each(|p| *p /= sum);
322
323        Self {
324            release_probabilities: probs,
325            max_vesicles_per_site: max_vesicles,
326        }
327    }
328
329    /// Calculate factorial.
330    fn factorial(n: usize) -> usize {
331        (1..=n).product()
332    }
333
334    /// Get mean number of vesicles released per site.
335    pub fn mean_release(&self) -> f64 {
336        self.release_probabilities
337            .iter()
338            .enumerate()
339            .map(|(k, &p)| k as f64 * p)
340            .sum()
341    }
342
343    /// Get probability of releasing exactly k vesicles.
344    pub fn probability(&self, k: usize) -> f64 {
345        if k <= self.max_vesicles_per_site {
346            self.release_probabilities[k]
347        } else {
348            0.0
349        }
350    }
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356
357    #[test]
358    fn test_vesicle_pool_creation() {
359        let pool = VesiclePool::new();
360        assert_eq!(pool.available_fraction, 1.0);
361        assert_eq!(pool.utilization, 0.5);
362    }
363
364    #[test]
365    fn test_depressing_synapse() {
366        let mut pool = VesiclePool::depressing();
367        let initial_prob = pool.release_probability();
368
369        // First release
370        pool.release(10.0).unwrap(); // High calcium
371        let prob_after_first = pool.release_probability();
372
373        // Depression: probability should decrease
374        assert!(prob_after_first < initial_prob);
375
376        // Second release immediately after
377        pool.release(10.0).unwrap();
378        let prob_after_second = pool.release_probability();
379
380        // Further depression
381        assert!(prob_after_second < prob_after_first);
382    }
383
384    #[test]
385    fn test_facilitating_synapse() {
386        let mut pool = VesiclePool::facilitating();
387        let initial_u = pool.utilization;
388
389        // Release causes facilitation
390        pool.release(10.0).unwrap();
391
392        // Utilization should increase
393        assert!(pool.utilization > initial_u);
394    }
395
396    #[test]
397    fn test_vesicle_pool_recovery() {
398        let mut pool = VesiclePool::new();
399
400        // Deplete pool
401        pool.release(10.0).unwrap();
402        let depleted_fraction = pool.available_fraction;
403
404        // Recovery over time
405        for _ in 0..100 {
406            pool.update(10.0).unwrap(); // 10 ms steps
407        }
408
409        assert!(pool.available_fraction > depleted_fraction);
410    }
411
412    #[test]
413    fn test_calcium_release_probability() {
414        let pool = VesiclePool::new();
415
416        let low_ca = pool.calcium_release_probability(0.1);
417        let medium_ca = pool.calcium_release_probability(1.0);
418        let high_ca = pool.calcium_release_probability(10.0);
419
420        assert!(low_ca < medium_ca);
421        assert!(medium_ca < high_ca);
422        assert!(high_ca > 0.9); // Should be near saturation
423    }
424
425    #[test]
426    fn test_quantal_release() {
427        let qr = QuantalRelease::new(10, 0.5, 20.0).unwrap();
428
429        assert_eq!(qr.expected_release(), 5.0); // 10 * 0.5
430        assert_eq!(qr.expected_amplitude(), 100.0); // 5 * 20
431        assert!(qr.coefficient_of_variation() > 0.0);
432    }
433
434    #[test]
435    fn test_multi_vesicular_release() {
436        let mvr = MultiVesicularRelease::poisson(2.0, 10);
437
438        // Probabilities should sum to ~1
439        let sum: f64 = mvr.release_probabilities.iter().sum();
440        assert!((sum - 1.0).abs() < 1e-6);
441
442        // Mean should be close to 2.0
443        assert!((mvr.mean_release() - 2.0).abs() < 0.1);
444    }
445
446    #[test]
447    fn test_vesicle_pool_reset() {
448        let mut pool = VesiclePool::new();
449        pool.release(10.0).unwrap();
450
451        pool.reset();
452        assert_eq!(pool.available_fraction, 1.0);
453        assert_eq!(pool.utilization, pool.baseline_utilization);
454    }
455}