Skip to main content

sc_neurocore_engine/neurons/biophysical/
bertram_phantom.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Commercial license available
3// © Concepts 1996–2026 Miroslav Šotek. All rights reserved.
4// © Code 2020–2026 Miroslav Šotek. All rights reserved.
5// ORCID: 0009-0009-3560-0851
6// Contact: www.anulum.li | protoscience@anulum.li
7// SC-NeuroCore — Bertram Phantom Burster Model
8
9//! Bertram dual-slow-potassium phantom-burster dynamics.
10
11/// Bertram phantom burster — dual slow K for phantom bursting. Bertram et al. 2000.
12#[derive(Clone, Debug)]
13pub struct BertramPhantomBurster {
14    pub v: f64,
15    pub s1: f64,
16    pub s2: f64,
17    pub g_ca: f64,
18    pub g_k: f64,
19    pub g_s1: f64,
20    pub g_s2: f64,
21    pub g_l: f64,
22    pub e_ca: f64,
23    pub e_k: f64,
24    pub e_l: f64,
25    pub c_m: f64,
26    pub v_m: f64,
27    pub s_m: f64,
28    pub v_n: f64,
29    pub s_n: f64,
30    pub v_s1: f64,
31    pub s_s1: f64,
32    pub v_s2: f64,
33    pub s_s2: f64,
34    pub tau_s1: f64,
35    pub tau_s2: f64,
36    pub dt: f64,
37    pub v_threshold: f64,
38}
39
40impl BertramPhantomBurster {
41    pub fn new() -> Self {
42        Self {
43            v: -50.0,
44            s1: 0.1,
45            s2: 0.1,
46            g_ca: 3.6,
47            g_k: 10.0,
48            g_s1: 4.0,
49            g_s2: 4.0,
50            g_l: 0.2,
51            e_ca: 25.0,
52            e_k: -75.0,
53            e_l: -40.0,
54            c_m: 5.3,
55            v_m: -20.0,
56            s_m: 12.0,
57            v_n: -16.0,
58            s_n: 5.6,
59            v_s1: -40.0,
60            s_s1: 10.0,
61            v_s2: -42.0,
62            s_s2: 0.4,
63            tau_s1: 20000.0,
64            tau_s2: 100000.0,
65            dt: 0.5,
66            v_threshold: -20.0,
67        }
68    }
69    pub fn step(&mut self, current: f64) -> i32 {
70        let v_prev = self.v;
71        let m_inf = 1.0 / (1.0 + (-(self.v - self.v_m) / self.s_m).exp());
72        let n_inf = 1.0 / (1.0 + (-(self.v - self.v_n) / self.s_n).exp());
73        let s1_inf = 1.0 / (1.0 + (-(self.v - self.v_s1) / self.s_s1).exp());
74        let s2_inf = 1.0 / (1.0 + (-(self.v - self.v_s2) / self.s_s2).exp());
75        let i_ca = self.g_ca * m_inf * (self.v - self.e_ca);
76        let i_k = self.g_k * n_inf * (self.v - self.e_k);
77        let i_s1 = self.g_s1 * self.s1 * (self.v - self.e_k);
78        let i_s2 = self.g_s2 * self.s2 * (self.v - self.e_k);
79        let i_l = self.g_l * (self.v - self.e_l);
80        self.v += (-i_ca - i_k - i_s1 - i_s2 - i_l + current) / self.c_m * self.dt;
81        self.s1 += (s1_inf - self.s1) / self.tau_s1 * self.dt;
82        self.s2 += (s2_inf - self.s2) / self.tau_s2 * self.dt;
83        if self.v >= self.v_threshold && v_prev < self.v_threshold {
84            1
85        } else {
86            0
87        }
88    }
89    pub fn reset(&mut self) {
90        self.v = -50.0;
91        self.s1 = 0.1;
92        self.s2 = 0.1;
93    }
94}
95impl Default for BertramPhantomBurster {
96    fn default() -> Self {
97        Self::new()
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn default_matches_constructor_state() {
107        let default = BertramPhantomBurster::default();
108        let constructed = BertramPhantomBurster::new();
109        assert_eq!(default.v, constructed.v);
110    }
111
112    #[test]
113    fn bertram_fires() {
114        let mut n = BertramPhantomBurster::new();
115        let t: i32 = (0..10000).map(|_| n.step(200.0)).sum();
116        assert!(t > 0);
117    }
118
119    // -- BertramPhantom --
120    #[test]
121    fn bertram_silent_without_input() {
122        let mut n = BertramPhantomBurster::new();
123        for _ in 0..500 {
124            n.step(0.0);
125        }
126        assert!(n.v.is_finite());
127    }
128    #[test]
129    fn bertram_reset_clears_state() {
130        let mut n = BertramPhantomBurster::new();
131        for _ in 0..1000 {
132            n.step(200.0);
133        }
134        n.reset();
135        assert!((n.v - (-50.0)).abs() < 1e-10);
136        assert!((n.s1 - 0.1).abs() < 1e-10);
137        assert!((n.s2 - 0.1).abs() < 1e-10);
138    }
139    #[test]
140    fn bertram_extreme_bounded() {
141        let mut n = BertramPhantomBurster::new();
142        for _ in 0..200 {
143            n.step(1e4);
144        }
145        assert!(n.v.is_finite());
146    }
147    #[test]
148    fn bertram_dual_slow_vars() {
149        let mut n = BertramPhantomBurster::new();
150        for _ in 0..10000 {
151            n.step(200.0);
152        }
153        // Both slow variables should evolve
154        assert!(n.s1 >= 0.0 && n.s1 <= 1.0, "s1={}", n.s1);
155        assert!(n.s2 >= 0.0 && n.s2 <= 1.0, "s2={}", n.s2);
156    }
157    #[test]
158    fn bertram_negative_no_crash() {
159        let mut n = BertramPhantomBurster::new();
160        for _ in 0..200 {
161            n.step(-100.0);
162        }
163        assert!(n.v.is_finite());
164    }
165    #[test]
166    fn bertram_nan_no_panic() {
167        let mut n = BertramPhantomBurster::new();
168        n.step(f64::NAN);
169    }
170}