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 et al. 2000 four-state phantom burster
8
9//! Source equations 1–10 with the authors' BJ_00 parameter set.
10
11#[path = "sc_three_state_phantom.rs"]
12mod sc_three_state_phantom;
13pub use sc_three_state_phantom::SCThreeStatePhantomBurster;
14
15#[derive(Clone, Debug)]
16pub struct BertramPhantomBurster {
17    pub v: f64,
18    pub n: f64,
19    pub s1: f64,
20    pub s2: f64,
21    pub lambda_n: f64,
22    pub g_ca: f64,
23    pub g_k: f64,
24    pub g_s1: f64,
25    pub g_s2: f64,
26    pub g_l: f64,
27    pub e_ca: f64,
28    pub e_k: f64,
29    pub e_l: f64,
30    pub c_m: f64,
31    pub v_m: f64,
32    pub s_m: f64,
33    pub v_n: f64,
34    pub s_n: f64,
35    pub v_s1: f64,
36    pub s_s1: f64,
37    pub v_s2: f64,
38    pub s_s2: f64,
39    pub tau_n_bar: f64,
40    pub tau_s1: f64,
41    pub tau_s2: f64,
42    pub dt: f64,
43    pub v_threshold: f64,
44}
45
46impl BertramPhantomBurster {
47    pub fn new() -> Self {
48        Self {
49            v: -43.0,
50            n: 0.03,
51            s1: 0.1,
52            s2: 0.434,
53            lambda_n: 1.1,
54            g_ca: 280.0,
55            g_k: 1300.0,
56            g_s1: 20.0,
57            g_s2: 32.0,
58            g_l: 25.0,
59            e_ca: 100.0,
60            e_k: -80.0,
61            e_l: -40.0,
62            c_m: 4524.0,
63            v_m: -22.0,
64            s_m: 7.5,
65            v_n: -9.0,
66            s_n: 10.0,
67            v_s1: -40.0,
68            s_s1: 0.5,
69            v_s2: -42.0,
70            s_s2: 0.4,
71            tau_n_bar: 9.09,
72            tau_s1: 1000.0,
73            tau_s2: 120_000.0,
74            dt: 0.5,
75            v_threshold: -20.0,
76        }
77    }
78
79    fn boltz(v: f64, midpoint: f64, slope: f64) -> f64 {
80        1.0 / (1.0 + ((midpoint - v) / slope).exp())
81    }
82
83    fn derivatives(&self, state: [f64; 4], current: f64) -> [f64; 4] {
84        let [v, n, s1, s2] = state;
85        let m_inf = Self::boltz(v, self.v_m, self.s_m);
86        let n_inf = Self::boltz(v, self.v_n, self.s_n);
87        let s1_inf = Self::boltz(v, self.v_s1, self.s_s1);
88        let s2_inf = Self::boltz(v, self.v_s2, self.s_s2);
89        let tau_n = self.tau_n_bar / (1.0 + ((v - self.v_n) / self.s_n).exp());
90        let i_ca = self.g_ca * m_inf * (v - self.e_ca);
91        let i_k = self.g_k * n * (v - self.e_k);
92        let i_s1 = self.g_s1 * s1 * (v - self.e_k);
93        let i_s2 = self.g_s2 * s2 * (v - self.e_k);
94        let i_l = self.g_l * (v - self.e_l);
95        [
96            (-i_ca - i_k - i_s1 - i_s2 - i_l + current) / self.c_m,
97            self.lambda_n * (n_inf - n) / tau_n,
98            (s1_inf - s1) / self.tau_s1,
99            (s2_inf - s2) / self.tau_s2,
100        ]
101    }
102
103    fn shifted(state: [f64; 4], derivative: [f64; 4], scale: f64) -> [f64; 4] {
104        std::array::from_fn(|index| state[index] + scale * derivative[index])
105    }
106
107    pub fn step(&mut self, current: f64) -> i32 {
108        let previous_v = self.v;
109        let state = [self.v, self.n, self.s1, self.s2];
110        let k1 = self.derivatives(state, current);
111        let k2 = self.derivatives(Self::shifted(state, k1, 0.5 * self.dt), current);
112        let k3 = self.derivatives(Self::shifted(state, k2, 0.5 * self.dt), current);
113        let k4 = self.derivatives(Self::shifted(state, k3, self.dt), current);
114        let next: [f64; 4] = std::array::from_fn(|index| {
115            state[index]
116                + self.dt * (k1[index] + 2.0 * k2[index] + 2.0 * k3[index] + k4[index]) / 6.0
117        });
118        self.v = next[0];
119        self.n = next[1];
120        self.s1 = next[2];
121        self.s2 = next[3];
122        i32::from(self.v >= self.v_threshold && previous_v < self.v_threshold)
123    }
124
125    pub fn reset(&mut self) {
126        self.v = -43.0;
127        self.n = 0.03;
128        self.s1 = 0.1;
129        self.s2 = 0.434;
130    }
131}
132
133impl Default for BertramPhantomBurster {
134    fn default() -> Self {
135        Self::new()
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn source_defaults_and_dynamic_n() {
145        let mut model = BertramPhantomBurster::new();
146        let previous_n = model.n;
147        model.step(0.0);
148        assert_ne!(model.n, previous_n);
149    }
150
151    #[test]
152    fn reset_restores_source_state() {
153        let mut model = BertramPhantomBurster::new();
154        model.step(200.0);
155        model.reset();
156        assert_eq!(
157            [model.v, model.n, model.s1, model.s2],
158            [-43.0, 0.03, 0.1, 0.434]
159        );
160    }
161}