Skip to main content

sc_neurocore_engine/neurons/interneurons/
chandelier_neuron.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 — Chandelier interneuron model
8
9use super::super::biophysical::safe_rate;
10
11/// Chandelier cell — axo-axonic fast-spiking interneuron.
12///
13/// Biophysics: Wang-Buzsáki core + Kv1 (D-type delay current) + Kv3.1.
14/// Kv1 creates a delay to first spike compared to PV+. Targets AIS.
15///
16/// Based on Woodruff et al. 2011 / Wang & Buzsáki 1996.
17#[derive(Clone, Debug)]
18pub struct ChandelierNeuron {
19    pub v: f64,
20    pub h: f64,
21    pub n: f64,
22    pub d: f64, // Kv1 (D-type) activation
23    pub p: f64, // Kv3.1 activation
24    // Conductances
25    pub g_na: f64,
26    pub g_k: f64,
27    pub g_kv1: f64,
28    pub g_kv3: f64,
29    pub g_l: f64,
30    // Reversal potentials
31    pub e_na: f64,
32    pub e_k: f64,
33    pub e_l: f64,
34    pub c_m: f64,
35    pub phi: f64,
36    pub dt: f64,
37    pub v_threshold: f64,
38}
39
40impl ChandelierNeuron {
41    pub fn new() -> Self {
42        Self {
43            v: -65.0,
44            h: 0.8,
45            n: 0.1,
46            d: 0.0,
47            p: 0.0,
48            g_na: 35.0,
49            g_k: 9.0,
50            g_kv1: 3.0, // Kv1 delay current (slower)
51            g_kv3: 4.0, // Kv3.1 for AP sharpening
52            g_l: 0.1,
53            e_na: 55.0,
54            e_k: -90.0,
55            e_l: -65.0,
56            c_m: 1.0,
57            phi: 5.0,
58            dt: 0.01,
59            v_threshold: -20.0,
60        }
61    }
62
63    pub fn step(&mut self, current: f64) -> i32 {
64        let v_prev = self.v;
65        let n_sub = (0.5 / self.dt.max(0.001)) as usize;
66        for _ in 0..n_sub {
67            // Wang-Buzsáki gating
68            let am = safe_rate(0.1, 35.0, self.v, 10.0, 1.0);
69            let bm = 4.0 * (-(self.v + 60.0) / 18.0).exp();
70            let m_inf = am / (am + bm);
71            let ah = 0.07 * (-(self.v + 58.0) / 20.0).exp();
72            let bh = 1.0 / (1.0 + (-(self.v + 28.0) / 10.0).exp());
73            let an = safe_rate(0.01, 34.0, self.v, 10.0, 0.1);
74            let bn = 0.125 * (-(self.v + 44.0) / 80.0).exp();
75
76            self.h += self.phi * (ah * (1.0 - self.h) - bh * self.h) * self.dt;
77            self.n += self.phi * (an * (1.0 - self.n) - bn * self.n) * self.dt;
78
79            // Kv1 (D-type): slow activation → first-spike delay
80            let d_inf = 1.0 / (1.0 + (-(self.v + 50.0) / 10.0).exp());
81            let tau_d = 150.0;
82            self.d += (d_inf - self.d) / tau_d * self.dt;
83
84            // Kv3.1: fast activation
85            let p_inf = 1.0 / (1.0 + (-(self.v + 10.0) / 10.0).exp());
86            self.p += self.phi * (p_inf - self.p) / 1.0 * self.dt;
87
88            let i_na = self.g_na * m_inf.powi(3) * self.h * (self.v - self.e_na);
89            let i_k = self.g_k * self.n.powi(4) * (self.v - self.e_k);
90            let i_kv1 = self.g_kv1 * self.d.powi(4) * (self.v - self.e_k);
91            let i_kv3 = self.g_kv3 * self.p * (self.v - self.e_k);
92            let i_l = self.g_l * (self.v - self.e_l);
93
94            self.v += (-i_na - i_k - i_kv1 - i_kv3 - i_l + current) / self.c_m * self.dt;
95        }
96        if self.v >= self.v_threshold && v_prev < self.v_threshold {
97            1
98        } else {
99            0
100        }
101    }
102
103    pub fn reset(&mut self) {
104        self.v = -65.0;
105        self.h = 0.8;
106        self.n = 0.1;
107        self.d = 0.0;
108        self.p = 0.0;
109    }
110}
111
112impl Default for ChandelierNeuron {
113    fn default() -> Self {
114        Self::new()
115    }
116}
117
118// ═══════════════════════════════════════════════════════════════════
119// Cerebellar Basket Cell
120// ═══════════════════════════════════════════════════════════════════
121
122#[cfg(test)]
123mod tests {
124    use super::super::PVFastSpikingNeuron;
125    use super::*;
126
127    #[test]
128    fn chandelier_fires_with_input() {
129        let mut n = ChandelierNeuron::new();
130        let spikes: i32 = (0..5000).map(|_| n.step(3.0)).sum();
131        assert!(spikes > 0, "Chandelier must fire with sustained input");
132    }
133
134    #[test]
135    fn chandelier_no_fire_without_input() {
136        let mut n = ChandelierNeuron::new();
137        let spikes: i32 = (0..2000).map(|_| n.step(0.0)).sum();
138        assert_eq!(spikes, 0);
139    }
140
141    #[test]
142    fn chandelier_has_kv1_delay_current() {
143        // Chandelier has Kv1 (D-type) which activates slowly.
144        // After sustained input, Kv1 contributes extra K+ current → lower steady-state rate.
145        let mut ch = ChandelierNeuron::new();
146        let mut pv = PVFastSpikingNeuron::new();
147        let ch_spikes: i32 = (0..5000).map(|_| ch.step(3.0)).sum();
148        let pv_spikes: i32 = (0..5000).map(|_| pv.step(3.0)).sum();
149        // Both should fire, Chandelier may fire fewer due to extra K+
150        assert!(ch_spikes > 0, "Chandelier must fire");
151        assert!(pv_spikes > 0, "PV+ must fire");
152        assert!(
153            ch_spikes <= pv_spikes + 10,
154            "Chandelier ({ch_spikes}) should fire <= PV+ ({pv_spikes}) due to Kv1"
155        );
156    }
157
158    #[test]
159    fn chandelier_reset_roundtrip() {
160        let mut n = ChandelierNeuron::new();
161        for _ in 0..1000 {
162            n.step(3.0);
163        }
164        n.reset();
165        let mut fresh = ChandelierNeuron::new();
166        let r1: i32 = (0..500).map(|_| n.step(3.0)).sum();
167        let r2: i32 = (0..500).map(|_| fresh.step(3.0)).sum();
168        assert_eq!(r1, r2);
169    }
170
171    #[test]
172    fn chandelier_voltage_bounded() {
173        let mut n = ChandelierNeuron::new();
174        for _ in 0..5000 {
175            n.step(5.0);
176        }
177        assert!(n.v.is_finite());
178    }
179
180    #[test]
181    #[ignore = "wall-clock performance smoke; use Criterion benches for timing evidence"]
182    fn chandelier_performance_5k_steps() {
183        let mut n = ChandelierNeuron::new();
184        let start = std::time::Instant::now();
185        for _ in 0..5_000 {
186            n.step(3.0);
187        }
188        assert!(
189            start.elapsed().as_millis() < 500,
190            "5k steps took {:?}",
191            start.elapsed()
192        );
193    }
194}