Skip to main content

sc_neurocore_engine/neurons/biophysical/
wang_buzsaki.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 — Wang-Buzsaki Neuron Model
8
9//! Wang-Buzsaki fast-spiking interneuron dynamics.
10
11use super::safe_rate;
12
13/// Wang-Buzsaki — fast-spiking GABAergic interneuron. Wang & Buzsáki 1996.
14#[derive(Clone, Debug)]
15pub struct WangBuzsakiNeuron {
16    pub v: f64,
17    pub h: f64,
18    pub n: f64,
19    pub g_na: f64,
20    pub g_k: f64,
21    pub g_l: f64,
22    pub e_na: f64,
23    pub e_k: f64,
24    pub e_l: f64,
25    pub c_m: f64,
26    pub phi: f64,
27    pub dt: f64,
28    pub v_threshold: f64,
29}
30
31impl WangBuzsakiNeuron {
32    pub fn new() -> Self {
33        Self {
34            v: -65.0,
35            h: 0.8,
36            n: 0.1,
37            g_na: 35.0,
38            g_k: 9.0,
39            g_l: 0.1,
40            e_na: 55.0,
41            e_k: -90.0,
42            e_l: -65.0,
43            c_m: 1.0,
44            phi: 5.0,
45            dt: 0.01,
46            v_threshold: -20.0,
47        }
48    }
49    pub fn step(&mut self, current: f64) -> i32 {
50        let v_prev = self.v;
51        let n_sub = (0.5 / self.dt.max(0.001)) as usize;
52        for _ in 0..n_sub {
53            let am = safe_rate(0.1, 35.0, self.v, 10.0, 1.0);
54            let bm = 4.0 * (-(self.v + 60.0) / 18.0).exp();
55            let m_inf = am / (am + bm);
56            let ah = 0.07 * (-(self.v + 58.0) / 20.0).exp();
57            let bh = 1.0 / (1.0 + (-(self.v + 28.0) / 10.0).exp());
58            let an = safe_rate(0.01, 34.0, self.v, 10.0, 0.1);
59            let bn = 0.125 * (-(self.v + 44.0) / 80.0).exp();
60            self.h += self.phi * (ah * (1.0 - self.h) - bh * self.h) * self.dt;
61            self.n += self.phi * (an * (1.0 - self.n) - bn * self.n) * self.dt;
62            let i_na = self.g_na * m_inf.powi(3) * self.h * (self.v - self.e_na);
63            let i_k = self.g_k * self.n.powi(4) * (self.v - self.e_k);
64            let i_l = self.g_l * (self.v - self.e_l);
65            self.v += (-i_na - i_k - i_l + current) / self.c_m * self.dt;
66        }
67        if self.v >= self.v_threshold && v_prev < self.v_threshold {
68            1
69        } else {
70            0
71        }
72    }
73    pub fn reset(&mut self) {
74        self.v = -65.0;
75        self.h = 0.8;
76        self.n = 0.1;
77    }
78}
79impl Default for WangBuzsakiNeuron {
80    fn default() -> Self {
81        Self::new()
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn default_matches_constructor_state() {
91        let default = WangBuzsakiNeuron::default();
92        let constructed = WangBuzsakiNeuron::new();
93        assert_eq!(default.v, constructed.v);
94    }
95
96    #[test]
97    fn wb_fires() {
98        let mut n = WangBuzsakiNeuron::new();
99        let t: i32 = (0..200).map(|_| n.step(2.0)).sum();
100        assert!(t > 0);
101    }
102
103    // -- WangBuzsaki --
104    #[test]
105    fn wb_silent_without_input() {
106        let mut n = WangBuzsakiNeuron::new();
107        let t: i32 = (0..200).map(|_| n.step(0.0)).sum();
108        assert_eq!(t, 0);
109    }
110    #[test]
111    fn wb_reset_clears_state() {
112        let mut n = WangBuzsakiNeuron::new();
113        for _ in 0..100 {
114            n.step(2.0);
115        }
116        n.reset();
117        assert!((n.v - (-65.0)).abs() < 1e-10);
118    }
119    #[test]
120    fn wb_extreme_bounded() {
121        let mut n = WangBuzsakiNeuron::new();
122        for _ in 0..200 {
123            n.step(1e4);
124        }
125        assert!(n.v.is_finite());
126    }
127    #[test]
128    fn wb_fast_spiking_high_rate() {
129        // WB model is fast-spiking — should achieve high rates
130        let mut n = WangBuzsakiNeuron::new();
131        let t: i32 = (0..500).map(|_| n.step(5.0)).sum();
132        assert!(t > 10, "WB FS should produce many spikes, got {}", t);
133    }
134    #[test]
135    fn wb_gates_bounded() {
136        let mut n = WangBuzsakiNeuron::new();
137        for _ in 0..500 {
138            n.step(2.0);
139        }
140        assert!(n.h >= 0.0 && n.h <= 1.0);
141        assert!(n.n >= 0.0 && n.n <= 1.0);
142    }
143    #[test]
144    fn wb_negative_no_crash() {
145        let mut n = WangBuzsakiNeuron::new();
146        for _ in 0..200 {
147            n.step(-10.0);
148        }
149        assert!(n.v.is_finite());
150    }
151    #[test]
152    fn wb_nan_no_panic() {
153        let mut n = WangBuzsakiNeuron::new();
154        n.step(f64::NAN);
155    }
156}