synapse_models/
receptor.rs

1//! Receptor dynamics and kinetic models.
2//!
3//! This module implements various postsynaptic receptor types with detailed
4//! kinetic models based on experimental data.
5
6use crate::error::{Result, SynapseError};
7
8/// Trait for receptor dynamics.
9pub trait ReceptorDynamics {
10    /// Update receptor state given neurotransmitter concentration and membrane voltage.
11    ///
12    /// # Arguments
13    /// * `nt_concentration` - Neurotransmitter concentration (mM)
14    /// * `voltage` - Membrane voltage (mV)
15    /// * `dt` - Time step (ms)
16    fn update(&mut self, nt_concentration: f64, voltage: f64, dt: f64) -> Result<()>;
17
18    /// Get the current open probability or conductance state.
19    fn get_conductance(&self) -> f64;
20
21    /// Get the reversal potential for this receptor (mV).
22    fn reversal_potential(&self) -> f64;
23
24    /// Reset receptor to resting state.
25    fn reset(&mut self);
26}
27
28/// AMPA receptor - fast excitatory glutamate receptor.
29///
30/// AMPA receptors mediate the majority of fast excitatory synaptic transmission.
31/// They have rapid kinetics with rise time ~0.2 ms and decay time ~2 ms.
32///
33/// Model: Two-state kinetic scheme
34/// C <-> O (Closed <-> Open)
35/// dr/dt = α[NT](1-r) - βr  (rise)
36/// do/dt = r/τ_rise - o/τ_decay  (opening and decay)
37#[derive(Debug, Clone)]
38pub struct AMPAReceptor {
39    /// Open probability (0 to 1).
40    pub open_probability: f64,
41
42    /// Rising phase variable (0 to 1).
43    rise_state: f64,
44
45    /// Rise time constant (ms).
46    pub tau_rise: f64,
47
48    /// Decay time constant (ms).
49    pub tau_decay: f64,
50
51    /// Forward binding rate (1/(mM·ms)).
52    pub alpha: f64,
53
54    /// Unbinding rate (1/ms).
55    pub beta: f64,
56
57    /// Reversal potential (mV).
58    pub e_rev: f64,
59
60    /// Maximum conductance (nS).
61    pub g_max: f64,
62}
63
64impl Default for AMPAReceptor {
65    fn default() -> Self {
66        Self {
67            open_probability: 0.0,
68            rise_state: 0.0,
69            tau_rise: 0.2,   // 0.2 ms rise time
70            tau_decay: 2.0,  // 2 ms decay time
71            alpha: 1.1,      // Fast binding
72            beta: 0.19,      // Relatively slow unbinding
73            e_rev: 0.0,      // Non-selective cation channel
74            g_max: 1.0,      // Normalized conductance
75        }
76    }
77}
78
79impl AMPAReceptor {
80    /// Create a new AMPA receptor with default parameters.
81    pub fn new() -> Self {
82        Self::default()
83    }
84
85    /// Create AMPA receptor with custom parameters.
86    pub fn with_params(tau_rise: f64, tau_decay: f64, g_max: f64) -> Result<Self> {
87        if tau_rise <= 0.0 {
88            return Err(SynapseError::InvalidTimeConstant(tau_rise));
89        }
90        if tau_decay <= 0.0 {
91            return Err(SynapseError::InvalidTimeConstant(tau_decay));
92        }
93
94        Ok(Self {
95            tau_rise,
96            tau_decay,
97            g_max,
98            ..Self::default()
99        })
100    }
101
102    /// Get the synaptic current (pA).
103    pub fn current(&self, voltage: f64) -> f64 {
104        self.g_max * self.open_probability * (voltage - self.e_rev)
105    }
106}
107
108impl ReceptorDynamics for AMPAReceptor {
109    fn update(&mut self, nt_concentration: f64, _voltage: f64, dt: f64) -> Result<()> {
110        // Update rise state: dr/dt = α[NT](1-r) - βr
111        let dr = self.alpha * nt_concentration * (1.0 - self.rise_state)
112                 - self.beta * self.rise_state;
113        self.rise_state += dr * dt;
114        self.rise_state = self.rise_state.clamp(0.0, 1.0);
115
116        // Update open probability using exponential Euler for stability
117        // do/dt = r/τ_rise - o/τ_decay
118        let target = self.rise_state * self.tau_decay / (self.tau_rise + self.tau_decay);
119        let tau_eff = self.tau_decay;
120        self.open_probability += (target - self.open_probability) * (1.0 - (-dt / tau_eff).exp());
121        self.open_probability = self.open_probability.clamp(0.0, 1.0);
122
123        Ok(())
124    }
125
126    fn get_conductance(&self) -> f64 {
127        self.g_max * self.open_probability
128    }
129
130    fn reversal_potential(&self) -> f64 {
131        self.e_rev
132    }
133
134    fn reset(&mut self) {
135        self.open_probability = 0.0;
136        self.rise_state = 0.0;
137    }
138}
139
140/// NMDA receptor - slow excitatory glutamate receptor with voltage dependence.
141///
142/// NMDA receptors have slower kinetics and are blocked by Mg2+ at resting potentials.
143/// They are critical for synaptic plasticity and learning.
144///
145/// Key features:
146/// - Voltage-dependent Mg2+ block
147/// - Slow kinetics (rise ~2 ms, decay ~100 ms)
148/// - High Ca2+ permeability
149#[derive(Debug, Clone)]
150pub struct NMDAReceptor {
151    /// Open probability (0 to 1).
152    pub open_probability: f64,
153
154    /// Rising phase variable (0 to 1).
155    rise_state: f64,
156
157    /// Rise time constant (ms).
158    pub tau_rise: f64,
159
160    /// Decay time constant (ms).
161    pub tau_decay: f64,
162
163    /// Forward binding rate (1/(mM·ms)).
164    pub alpha: f64,
165
166    /// Unbinding rate (1/ms).
167    pub beta: f64,
168
169    /// Reversal potential (mV).
170    pub e_rev: f64,
171
172    /// Maximum conductance (nS).
173    pub g_max: f64,
174
175    /// Mg2+ concentration (mM).
176    pub mg_concentration: f64,
177}
178
179impl Default for NMDAReceptor {
180    fn default() -> Self {
181        Self {
182            open_probability: 0.0,
183            rise_state: 0.0,
184            tau_rise: 2.0,    // 2 ms rise time
185            tau_decay: 100.0, // 100 ms decay time
186            alpha: 0.72,      // Slower binding than AMPA
187            beta: 0.0066,     // Very slow unbinding
188            e_rev: 0.0,       // Non-selective cation channel
189            g_max: 1.0,       // Normalized conductance
190            mg_concentration: 1.0, // 1 mM external Mg2+
191        }
192    }
193}
194
195impl NMDAReceptor {
196    /// Create a new NMDA receptor with default parameters.
197    pub fn new() -> Self {
198        Self::default()
199    }
200
201    /// Create NMDA receptor with custom parameters.
202    pub fn with_params(tau_rise: f64, tau_decay: f64, g_max: f64) -> Result<Self> {
203        if tau_rise <= 0.0 {
204            return Err(SynapseError::InvalidTimeConstant(tau_rise));
205        }
206        if tau_decay <= 0.0 {
207            return Err(SynapseError::InvalidTimeConstant(tau_decay));
208        }
209
210        Ok(Self {
211            tau_rise,
212            tau_decay,
213            g_max,
214            ..Self::default()
215        })
216    }
217
218    /// Calculate voltage-dependent Mg2+ block.
219    ///
220    /// Year and Stevens (1990) model:
221    /// B(V) = 1 / (1 + [Mg2+]/3.57 * exp(-0.062 * V))
222    pub fn mg_block(&self, voltage: f64) -> f64 {
223        1.0 / (1.0 + (self.mg_concentration / 3.57) * (-0.062 * voltage).exp())
224    }
225
226    /// Get the synaptic current (pA).
227    pub fn current(&self, voltage: f64) -> f64 {
228        let mg_block = self.mg_block(voltage);
229        self.g_max * self.open_probability * mg_block * (voltage - self.e_rev)
230    }
231}
232
233impl ReceptorDynamics for NMDAReceptor {
234    fn update(&mut self, nt_concentration: f64, _voltage: f64, dt: f64) -> Result<()> {
235        // Update rise state
236        let dr = self.alpha * nt_concentration * (1.0 - self.rise_state)
237                 - self.beta * self.rise_state;
238        self.rise_state += dr * dt;
239        self.rise_state = self.rise_state.clamp(0.0, 1.0);
240
241        // Update open probability with exponential Euler
242        let target = self.rise_state * self.tau_decay / (self.tau_rise + self.tau_decay);
243        let tau_eff = self.tau_decay;
244        self.open_probability += (target - self.open_probability) * (1.0 - (-dt / tau_eff).exp());
245        self.open_probability = self.open_probability.clamp(0.0, 1.0);
246
247        Ok(())
248    }
249
250    fn get_conductance(&self) -> f64 {
251        self.g_max * self.open_probability
252    }
253
254    fn reversal_potential(&self) -> f64 {
255        self.e_rev
256    }
257
258    fn reset(&mut self) {
259        self.open_probability = 0.0;
260        self.rise_state = 0.0;
261    }
262}
263
264/// GABA-A receptor - fast inhibitory receptor.
265///
266/// GABA-A receptors are ionotropic chloride channels that mediate fast
267/// inhibitory transmission with time constants similar to AMPA receptors.
268#[derive(Debug, Clone)]
269pub struct GABAAReceptor {
270    /// Open probability (0 to 1).
271    pub open_probability: f64,
272
273    /// Rising phase variable (0 to 1).
274    rise_state: f64,
275
276    /// Rise time constant (ms).
277    pub tau_rise: f64,
278
279    /// Decay time constant (ms).
280    pub tau_decay: f64,
281
282    /// Forward binding rate (1/(mM·ms)).
283    pub alpha: f64,
284
285    /// Unbinding rate (1/ms).
286    pub beta: f64,
287
288    /// Reversal potential (mV) - depends on Cl- gradient.
289    pub e_rev: f64,
290
291    /// Maximum conductance (nS).
292    pub g_max: f64,
293}
294
295impl Default for GABAAReceptor {
296    fn default() -> Self {
297        Self {
298            open_probability: 0.0,
299            rise_state: 0.0,
300            tau_rise: 0.5,   // 0.5 ms rise time
301            tau_decay: 5.0,  // 5 ms decay time
302            alpha: 5.0,      // Fast binding
303            beta: 0.18,      // Moderate unbinding
304            e_rev: -70.0,    // Chloride reversal (can vary)
305            g_max: 1.0,      // Normalized conductance
306        }
307    }
308}
309
310impl GABAAReceptor {
311    /// Create a new GABA-A receptor with default parameters.
312    pub fn new() -> Self {
313        Self::default()
314    }
315
316    /// Get the synaptic current (pA).
317    pub fn current(&self, voltage: f64) -> f64 {
318        self.g_max * self.open_probability * (voltage - self.e_rev)
319    }
320}
321
322impl ReceptorDynamics for GABAAReceptor {
323    fn update(&mut self, nt_concentration: f64, _voltage: f64, dt: f64) -> Result<()> {
324        // Update rise state
325        let dr = self.alpha * nt_concentration * (1.0 - self.rise_state)
326                 - self.beta * self.rise_state;
327        self.rise_state += dr * dt;
328        self.rise_state = self.rise_state.clamp(0.0, 1.0);
329
330        // Update open probability
331        let target = self.rise_state * self.tau_decay / (self.tau_rise + self.tau_decay);
332        let tau_eff = self.tau_decay;
333        self.open_probability += (target - self.open_probability) * (1.0 - (-dt / tau_eff).exp());
334        self.open_probability = self.open_probability.clamp(0.0, 1.0);
335
336        Ok(())
337    }
338
339    fn get_conductance(&self) -> f64 {
340        self.g_max * self.open_probability
341    }
342
343    fn reversal_potential(&self) -> f64 {
344        self.e_rev
345    }
346
347    fn reset(&mut self) {
348        self.open_probability = 0.0;
349        self.rise_state = 0.0;
350    }
351}
352
353/// GABA-B receptor - slow inhibitory metabotropic receptor.
354///
355/// GABA-B receptors are G-protein coupled receptors that activate K+ channels,
356/// producing slow, long-lasting inhibition.
357#[derive(Debug, Clone)]
358pub struct GABABReceptor {
359    /// Receptor activation state (0 to 1).
360    pub activation: f64,
361
362    /// G-protein activation state (0 to 1).
363    pub g_protein: f64,
364
365    /// Rise time constant (ms).
366    pub tau_rise: f64,
367
368    /// Decay time constant (ms).
369    pub tau_decay: f64,
370
371    /// G-protein activation time constant (ms).
372    pub tau_gprotein: f64,
373
374    /// Forward binding rate (1/(mM·ms)).
375    pub alpha: f64,
376
377    /// Unbinding rate (1/ms).
378    pub beta: f64,
379
380    /// Reversal potential (mV) - K+ reversal.
381    pub e_rev: f64,
382
383    /// Maximum conductance (nS).
384    pub g_max: f64,
385}
386
387impl Default for GABABReceptor {
388    fn default() -> Self {
389        Self {
390            activation: 0.0,
391            g_protein: 0.0,
392            tau_rise: 50.0,   // 50 ms rise time
393            tau_decay: 200.0, // 200 ms decay time
394            tau_gprotein: 100.0, // G-protein time constant
395            alpha: 0.09,      // Slow binding
396            beta: 0.0012,     // Very slow unbinding
397            e_rev: -90.0,     // K+ reversal potential
398            g_max: 1.0,       // Normalized conductance
399        }
400    }
401}
402
403impl GABABReceptor {
404    /// Create a new GABA-B receptor with default parameters.
405    pub fn new() -> Self {
406        Self::default()
407    }
408
409    /// Get the synaptic current (pA).
410    pub fn current(&self, voltage: f64) -> f64 {
411        // Current depends on G-protein activation
412        self.g_max * self.g_protein * (voltage - self.e_rev)
413    }
414}
415
416impl ReceptorDynamics for GABABReceptor {
417    fn update(&mut self, nt_concentration: f64, _voltage: f64, dt: f64) -> Result<()> {
418        // Update receptor activation
419        let dr = self.alpha * nt_concentration * (1.0 - self.activation)
420                 - self.beta * self.activation;
421        self.activation += dr * dt;
422        self.activation = self.activation.clamp(0.0, 1.0);
423
424        // G-protein activation follows receptor activation with delay
425        let dg = (self.activation - self.g_protein) / self.tau_gprotein;
426        self.g_protein += dg * dt;
427        self.g_protein = self.g_protein.clamp(0.0, 1.0);
428
429        Ok(())
430    }
431
432    fn get_conductance(&self) -> f64 {
433        self.g_max * self.g_protein
434    }
435
436    fn reversal_potential(&self) -> f64 {
437        self.e_rev
438    }
439
440    fn reset(&mut self) {
441        self.activation = 0.0;
442        self.g_protein = 0.0;
443    }
444}
445
446/// Metabotropic glutamate receptor (mGluR).
447///
448/// mGluRs are G-protein coupled receptors that modulate neuronal excitability
449/// through various second messenger pathways.
450#[derive(Debug, Clone)]
451pub struct MetabotropicGlutamateReceptor {
452    /// Receptor activation state (0 to 1).
453    pub activation: f64,
454
455    /// G-protein activation state (0 to 1).
456    pub g_protein: f64,
457
458    /// Activation time constant (ms).
459    pub tau_activation: f64,
460
461    /// Deactivation time constant (ms).
462    pub tau_deactivation: f64,
463
464    /// Forward binding rate (1/(mM·ms)).
465    pub alpha: f64,
466
467    /// Unbinding rate (1/ms).
468    pub beta: f64,
469}
470
471impl Default for MetabotropicGlutamateReceptor {
472    fn default() -> Self {
473        Self {
474            activation: 0.0,
475            g_protein: 0.0,
476            tau_activation: 100.0,   // 100 ms activation
477            tau_deactivation: 500.0, // 500 ms deactivation
478            alpha: 0.05,             // Slow binding
479            beta: 0.002,             // Very slow unbinding
480        }
481    }
482}
483
484impl MetabotropicGlutamateReceptor {
485    /// Create a new mGluR with default parameters.
486    pub fn new() -> Self {
487        Self::default()
488    }
489
490    /// Get G-protein activation level.
491    pub fn get_gprotein_activation(&self) -> f64 {
492        self.g_protein
493    }
494}
495
496impl ReceptorDynamics for MetabotropicGlutamateReceptor {
497    fn update(&mut self, nt_concentration: f64, _voltage: f64, dt: f64) -> Result<()> {
498        // Update receptor activation
499        let dr = self.alpha * nt_concentration * (1.0 - self.activation)
500                 - self.beta * self.activation;
501        self.activation += dr * dt;
502        self.activation = self.activation.clamp(0.0, 1.0);
503
504        // G-protein activation/deactivation
505        let tau = if self.activation > self.g_protein {
506            self.tau_activation
507        } else {
508            self.tau_deactivation
509        };
510        let dg = (self.activation - self.g_protein) / tau;
511        self.g_protein += dg * dt;
512        self.g_protein = self.g_protein.clamp(0.0, 1.0);
513
514        Ok(())
515    }
516
517    fn get_conductance(&self) -> f64 {
518        // Metabotropic receptors don't directly contribute to conductance
519        0.0
520    }
521
522    fn reversal_potential(&self) -> f64 {
523        0.0
524    }
525
526    fn reset(&mut self) {
527        self.activation = 0.0;
528        self.g_protein = 0.0;
529    }
530}
531
532#[cfg(test)]
533mod tests {
534    use super::*;
535
536    #[test]
537    fn test_ampa_receptor_creation() {
538        let ampa = AMPAReceptor::new();
539        assert_eq!(ampa.open_probability, 0.0);
540        assert_eq!(ampa.e_rev, 0.0);
541    }
542
543    #[test]
544    fn test_ampa_receptor_activation() {
545        let mut ampa = AMPAReceptor::new();
546        let nt_conc = 1.0; // 1 mM
547        let voltage = -65.0;
548        let dt = 0.1;
549
550        // Simulate activation
551        for _ in 0..100 {
552            ampa.update(nt_conc, voltage, dt).unwrap();
553        }
554
555        assert!(ampa.open_probability > 0.0);
556        assert!(ampa.open_probability <= 1.0);
557    }
558
559    #[test]
560    fn test_nmda_mg_block() {
561        let nmda = NMDAReceptor::new();
562
563        // At -70 mV, strong block
564        let block_hyperpol = nmda.mg_block(-70.0);
565        assert!(block_hyperpol < 0.1);
566
567        // At 0 mV, partial relief
568        let block_depol = nmda.mg_block(0.0);
569        assert!(block_depol > block_hyperpol);
570
571        // At +40 mV, nearly complete relief
572        let block_strong_depol = nmda.mg_block(40.0);
573        assert!(block_strong_depol > 0.8);
574    }
575
576    #[test]
577    fn test_nmda_receptor_kinetics() {
578        let mut nmda = NMDAReceptor::new();
579        let nt_conc = 0.5;
580        let voltage = 0.0;
581        let dt = 0.1;
582
583        // Simulate activation
584        for _ in 0..1000 {
585            nmda.update(nt_conc, voltage, dt).unwrap();
586        }
587
588        assert!(nmda.open_probability > 0.0);
589    }
590
591    #[test]
592    fn test_gabaa_receptor() {
593        let mut gabaa = GABAAReceptor::new();
594        assert_eq!(gabaa.e_rev, -70.0);
595
596        gabaa.update(1.0, -65.0, 0.1).unwrap();
597        assert!(gabaa.open_probability >= 0.0);
598    }
599
600    #[test]
601    fn test_gabab_receptor_gprotein() {
602        let mut gabab = GABABReceptor::new();
603
604        // Activate with GABA
605        for _ in 0..1000 {
606            gabab.update(1.0, -65.0, 0.1).unwrap();
607        }
608
609        // G-protein should be activated
610        assert!(gabab.g_protein > 0.0);
611        assert!(gabab.activation > 0.0);
612    }
613
614    #[test]
615    fn test_receptor_reset() {
616        let mut ampa = AMPAReceptor::new();
617        ampa.update(1.0, -65.0, 0.1).unwrap();
618
619        ampa.reset();
620        assert_eq!(ampa.open_probability, 0.0);
621        assert_eq!(ampa.rise_state, 0.0);
622    }
623}