sc_neurocore_engine/neurons/interneurons/
chandelier_neuron.rs1use super::super::biophysical::safe_rate;
10
11#[derive(Clone, Debug)]
18pub struct ChandelierNeuron {
19 pub v: f64,
20 pub h: f64,
21 pub n: f64,
22 pub d: f64, pub p: f64, 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 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, g_kv3: 4.0, 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 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 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 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#[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 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 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}