synapse_models/
calcium.rs

1//! Calcium dynamics in pre- and postsynaptic compartments.
2//!
3//! This module implements calcium dynamics including:
4//! - Influx through voltage-gated channels
5//! - Buffering by calcium-binding proteins
6//! - Extrusion by pumps and exchangers
7//! - Calcium stores (endoplasmic reticulum)
8//! - Calcium-induced calcium release (CICR)
9
10use crate::error::{Result, SynapseError};
11
12/// Calcium dynamics in a cellular compartment.
13///
14/// Models calcium concentration changes due to:
15/// - Influx (voltage-gated channels, NMDA receptors)
16/// - Buffering (calmodulin, calbindin, etc.)
17/// - Extrusion (PMCA, NCX)
18/// - Diffusion
19/// - ER uptake and release
20#[derive(Debug, Clone)]
21pub struct CalciumDynamics {
22    /// Free calcium concentration (μM).
23    pub concentration: f64,
24
25    /// Buffered calcium concentration (μM).
26    pub buffered: f64,
27
28    /// Resting calcium concentration (μM).
29    pub resting_concentration: f64,
30
31    /// Time constant for calcium removal (ms).
32    pub tau_removal: f64,
33
34    /// Calcium influx per spike (μM).
35    pub spike_influx: f64,
36
37    /// Buffer capacity (ratio of buffered to free calcium).
38    pub buffer_capacity: f64,
39
40    /// Buffer binding rate (1/(μM·ms)).
41    pub buffer_kon: f64,
42
43    /// Buffer unbinding rate (1/ms).
44    pub buffer_koff: f64,
45
46    /// Total buffer concentration (μM).
47    pub buffer_total: f64,
48
49    /// Compartment volume (μm³).
50    pub volume: f64,
51}
52
53impl Default for CalciumDynamics {
54    fn default() -> Self {
55        Self {
56            concentration: 0.05,      // 50 nM resting
57            buffered: 0.0,
58            resting_concentration: 0.05,
59            tau_removal: 20.0,        // 20 ms removal
60            spike_influx: 0.5,        // 0.5 μM per spike
61            buffer_capacity: 100.0,   // High buffering
62            buffer_kon: 0.1,          // Fast binding
63            buffer_koff: 0.001,       // Slow unbinding
64            buffer_total: 100.0,      // 100 μM total buffer
65            volume: 1.0,              // Normalized volume
66        }
67    }
68}
69
70impl CalciumDynamics {
71    /// Create new calcium dynamics with default parameters.
72    pub fn new() -> Self {
73        Self::default()
74    }
75
76    /// Create calcium dynamics for presynaptic terminal.
77    ///
78    /// Presynaptic terminals have rapid calcium dynamics with high buffering.
79    pub fn presynaptic() -> Self {
80        Self {
81            resting_concentration: 0.05,
82            tau_removal: 15.0,     // Fast removal
83            spike_influx: 1.0,     // Large influx
84            buffer_capacity: 200.0, // Very high buffering
85            ..Self::default()
86        }
87    }
88
89    /// Create calcium dynamics for postsynaptic spine.
90    ///
91    /// Spines have intermediate calcium dynamics.
92    pub fn postsynaptic() -> Self {
93        Self {
94            resting_concentration: 0.05,
95            tau_removal: 30.0,     // Moderate removal
96            spike_influx: 0.3,     // Moderate influx
97            buffer_capacity: 50.0, // Moderate buffering
98            ..Self::default()
99        }
100    }
101
102    /// Update calcium concentration over time.
103    ///
104    /// Implements first-order removal and buffering dynamics.
105    ///
106    /// # Arguments
107    /// * `influx` - Calcium influx rate (μM/ms)
108    /// * `dt` - Time step (ms)
109    pub fn update(&mut self, influx: f64, dt: f64) -> Result<()> {
110        if influx < 0.0 {
111            return Err(SynapseError::InvalidConcentration(influx));
112        }
113
114        // Free buffer concentration
115        let buffer_free = self.buffer_total - self.buffered;
116
117        // Buffering dynamics
118        // dB/dt = k_on * [Ca] * [B_free] - k_off * [B_bound]
119        let d_buffered = self.buffer_kon * self.concentration * buffer_free
120                         - self.buffer_koff * self.buffered;
121        self.buffered += d_buffered * dt;
122        self.buffered = self.buffered.clamp(0.0, self.buffer_total);
123
124        // Calcium dynamics with buffering
125        // d[Ca]/dt = influx - (([Ca] - [Ca]_rest) / τ) - dB/dt
126        let removal = (self.concentration - self.resting_concentration) / self.tau_removal;
127        let d_concentration = influx - removal - d_buffered;
128
129        self.concentration += d_concentration * dt;
130        self.concentration = self.concentration.max(0.0);
131
132        Ok(())
133    }
134
135    /// Add calcium influx from spike.
136    pub fn spike(&mut self) {
137        self.concentration += self.spike_influx;
138    }
139
140    /// Add calcium influx (e.g., from NMDA receptors).
141    ///
142    /// # Arguments
143    /// * `amount` - Amount of calcium to add (μM)
144    pub fn add_influx(&mut self, amount: f64) -> Result<()> {
145        if amount < 0.0 {
146            return Err(SynapseError::InvalidConcentration(amount));
147        }
148        self.concentration += amount;
149        Ok(())
150    }
151
152    /// Get current free calcium concentration.
153    pub fn get_concentration(&self) -> f64 {
154        self.concentration
155    }
156
157    /// Reset to resting state.
158    pub fn reset(&mut self) {
159        self.concentration = self.resting_concentration;
160        self.buffered = 0.0;
161    }
162}
163
164/// Calcium store (endoplasmic reticulum) dynamics.
165///
166/// Models calcium release from and uptake into the ER.
167#[derive(Debug, Clone)]
168pub struct CalciumStore {
169    /// ER calcium concentration (μM).
170    pub store_concentration: f64,
171
172    /// Cytoplasmic calcium concentration (μM).
173    pub cytoplasmic_concentration: f64,
174
175    /// Maximum store concentration (μM).
176    pub max_store_concentration: f64,
177
178    /// SERCA pump rate (μM/ms).
179    pub pump_rate: f64,
180
181    /// SERCA pump affinity (μM).
182    pub pump_km: f64,
183
184    /// IP3 receptor open probability.
185    pub ip3r_open_probability: f64,
186
187    /// RyR (ryanodine receptor) open probability.
188    pub ryr_open_probability: f64,
189
190    /// Maximum release rate (μM/ms).
191    pub max_release_rate: f64,
192
193    /// IP3 concentration (μM).
194    pub ip3_concentration: f64,
195
196    /// Calcium threshold for CICR (μM).
197    pub cicr_threshold: f64,
198}
199
200impl Default for CalciumStore {
201    fn default() -> Self {
202        Self {
203            store_concentration: 400.0, // ~400 μM in ER
204            cytoplasmic_concentration: 0.05,
205            max_store_concentration: 500.0,
206            pump_rate: 0.5,
207            pump_km: 0.3,
208            ip3r_open_probability: 0.0,
209            ryr_open_probability: 0.0,
210            max_release_rate: 10.0,
211            ip3_concentration: 0.0,
212            cicr_threshold: 0.3, // 0.3 μM threshold
213        }
214    }
215}
216
217impl CalciumStore {
218    /// Create new calcium store with default parameters.
219    pub fn new() -> Self {
220        Self::default()
221    }
222
223    /// Update calcium store dynamics.
224    ///
225    /// # Arguments
226    /// * `cytoplasmic_ca` - Current cytoplasmic calcium (μM)
227    /// * `dt` - Time step (ms)
228    ///
229    /// # Returns
230    /// Net calcium release from store (positive = release, negative = uptake)
231    pub fn update(&mut self, cytoplasmic_ca: f64, dt: f64) -> Result<f64> {
232        self.cytoplasmic_concentration = cytoplasmic_ca;
233
234        // SERCA pump (uptake into ER)
235        let pump_flux = self.pump_rate * cytoplasmic_ca / (cytoplasmic_ca + self.pump_km);
236
237        // IP3 receptor release
238        let ip3r_flux = self.ip3r_open_probability * self.max_release_rate
239                        * (self.store_concentration / self.max_store_concentration);
240
241        // Ryanodine receptor (CICR)
242        self.update_ryr_probability();
243        let ryr_flux = self.ryr_open_probability * self.max_release_rate
244                       * (self.store_concentration / self.max_store_concentration);
245
246        // Net flux (positive = release from ER)
247        let net_flux = ip3r_flux + ryr_flux - pump_flux;
248
249        // Update store concentration
250        self.store_concentration -= net_flux * dt;
251        self.store_concentration = self.store_concentration.clamp(0.0, self.max_store_concentration);
252
253        Ok(net_flux)
254    }
255
256    /// Update ryanodine receptor open probability for CICR.
257    ///
258    /// RyR opens when cytoplasmic calcium exceeds threshold.
259    fn update_ryr_probability(&mut self) {
260        // Simplified Hill equation for RyR activation
261        let ca = self.cytoplasmic_concentration;
262        if ca > self.cicr_threshold {
263            let hill_coeff = 4.0;
264            let k_half = 0.5_f64; // μM
265            self.ryr_open_probability = ca.powf(hill_coeff) / (ca.powf(hill_coeff) + k_half.powf(hill_coeff));
266        } else {
267            self.ryr_open_probability = 0.0;
268        }
269    }
270
271    /// Trigger IP3-mediated calcium release.
272    ///
273    /// # Arguments
274    /// * `ip3_level` - IP3 concentration (μM)
275    pub fn trigger_ip3_release(&mut self, ip3_level: f64) {
276        self.ip3_concentration = ip3_level;
277
278        // IP3R activation depends on both IP3 and calcium
279        let ip3_term = ip3_level / (ip3_level + 0.5); // IP3 binding
280        let ca_term = self.cytoplasmic_concentration / (self.cytoplasmic_concentration + 0.3); // Ca binding
281
282        self.ip3r_open_probability = ip3_term * ca_term * 0.8; // Max 80% open
283    }
284
285    /// Check if CICR is active.
286    pub fn is_cicr_active(&self) -> bool {
287        self.cytoplasmic_concentration > self.cicr_threshold && self.ryr_open_probability > 0.01
288    }
289
290    /// Reset store to resting state.
291    pub fn reset(&mut self) {
292        self.store_concentration = 400.0;
293        self.cytoplasmic_concentration = 0.05;
294        self.ip3r_open_probability = 0.0;
295        self.ryr_open_probability = 0.0;
296        self.ip3_concentration = 0.0;
297    }
298}
299
300/// Calcium-dependent processes.
301///
302/// Helper functions for calcium-dependent synaptic processes.
303pub struct CalciumDependent;
304
305impl CalciumDependent {
306    /// Calculate calcium-dependent plasticity signal.
307    ///
308    /// Used for CaMKII activation, calcineurin activation, etc.
309    ///
310    /// # Arguments
311    /// * `calcium` - Calcium concentration (μM)
312    /// * `threshold_low` - Low threshold for depression (μM)
313    /// * `threshold_high` - High threshold for potentiation (μM)
314    pub fn plasticity_signal(calcium: f64, threshold_low: f64, threshold_high: f64) -> f64 {
315        if calcium < threshold_low {
316            0.0
317        } else if calcium < threshold_high {
318            // Depression range
319            -(calcium - threshold_low) / (threshold_high - threshold_low)
320        } else {
321            // Potentiation range
322            (calcium - threshold_high) / threshold_high
323        }
324    }
325
326    /// Calculate CaMKII activation.
327    ///
328    /// CaMKII is activated by calcium-calmodulin and drives LTP.
329    ///
330    /// # Arguments
331    /// * `calcium` - Calcium concentration (μM)
332    pub fn camkii_activation(calcium: f64) -> f64 {
333        let k_half = 0.7_f64; // μM
334        let hill = 3.0;
335        calcium.powf(hill) / (calcium.powf(hill) + k_half.powf(hill))
336    }
337
338    /// Calculate calcineurin activation.
339    ///
340    /// Calcineurin is activated by moderate calcium and drives LTD.
341    ///
342    /// # Arguments
343    /// * `calcium` - Calcium concentration (μM)
344    pub fn calcineurin_activation(calcium: f64) -> f64 {
345        let k_half = 0.3_f64; // μM (lower threshold than CaMKII)
346        let hill = 2.0;
347        calcium.powf(hill) / (calcium.powf(hill) + k_half.powf(hill))
348    }
349}
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354
355    #[test]
356    fn test_calcium_dynamics_creation() {
357        let ca = CalciumDynamics::new();
358        assert_eq!(ca.concentration, 0.05);
359        assert_eq!(ca.resting_concentration, 0.05);
360    }
361
362    #[test]
363    fn test_calcium_spike() {
364        let mut ca = CalciumDynamics::new();
365        let initial = ca.concentration;
366
367        ca.spike();
368        assert!(ca.concentration > initial);
369    }
370
371    #[test]
372    fn test_calcium_decay() {
373        let mut ca = CalciumDynamics::new();
374        ca.concentration = 5.0; // Elevated calcium
375
376        // Let it decay
377        for _ in 0..100 {
378            ca.update(0.0, 1.0).unwrap();
379        }
380
381        // Should return toward resting
382        assert!(ca.concentration < 5.0);
383        assert!(ca.concentration > ca.resting_concentration);
384    }
385
386    #[test]
387    fn test_calcium_buffering() {
388        let mut ca = CalciumDynamics::new();
389        ca.concentration = 1.0;
390
391        // Update to allow buffering
392        for _ in 0..50 {
393            ca.update(0.0, 0.1).unwrap();
394        }
395
396        // Some calcium should be buffered
397        assert!(ca.buffered > 0.0);
398    }
399
400    #[test]
401    fn test_presynaptic_calcium() {
402        let ca_pre = CalciumDynamics::presynaptic();
403        assert!(ca_pre.spike_influx > 0.5);
404        assert!(ca_pre.buffer_capacity > 100.0);
405    }
406
407    #[test]
408    fn test_postsynaptic_calcium() {
409        let ca_post = CalciumDynamics::postsynaptic();
410        assert!(ca_post.spike_influx < 0.5);
411    }
412
413    #[test]
414    fn test_calcium_store() {
415        let store = CalciumStore::new();
416        assert!(store.store_concentration > 0.0);
417        assert_eq!(store.cytoplasmic_concentration, 0.05);
418    }
419
420    #[test]
421    fn test_cicr_activation() {
422        let mut store = CalciumStore::new();
423
424        // Low calcium - no CICR
425        store.update(0.1, 1.0).unwrap();
426        assert!(!store.is_cicr_active());
427
428        // High calcium - triggers CICR
429        store.update(1.0, 1.0).unwrap();
430        assert!(store.ryr_open_probability > 0.0);
431    }
432
433    #[test]
434    fn test_ip3_release() {
435        let mut store = CalciumStore::new();
436        store.trigger_ip3_release(1.0);
437
438        assert!(store.ip3_concentration > 0.0);
439        assert!(store.ip3r_open_probability > 0.0);
440    }
441
442    #[test]
443    fn test_camkii_activation() {
444        let low_ca = CalciumDependent::camkii_activation(0.1);
445        let medium_ca = CalciumDependent::camkii_activation(0.7);
446        let high_ca = CalciumDependent::camkii_activation(2.0);
447
448        assert!(low_ca < medium_ca);
449        assert!(medium_ca < high_ca);
450    }
451
452    #[test]
453    fn test_plasticity_signal() {
454        let signal_low = CalciumDependent::plasticity_signal(0.2, 0.3, 0.8);
455        let signal_medium = CalciumDependent::plasticity_signal(0.5, 0.3, 0.8);
456        let signal_high = CalciumDependent::plasticity_signal(1.5, 0.3, 0.8);
457
458        assert_eq!(signal_low, 0.0); // Below threshold
459        assert!(signal_medium < 0.0); // Depression range
460        assert!(signal_high > 0.0);   // Potentiation range
461    }
462}