sc_neurocore_engine/neurons/channels/
sk.rs1use crate::neurons::biophysical::safe_rate;
10
11#[derive(Clone, Debug)]
25pub struct SKNeuron {
26 pub v: f64,
27 pub h: f64,
28 pub n: f64,
29 pub ca: f64,
30 pub g_na: f64,
31 pub g_k: f64,
32 pub g_sk: f64,
33 pub g_l: f64,
34 pub e_na: f64,
35 pub e_k: f64,
36 pub e_l: f64,
37 pub c_m: f64,
38 pub phi: f64,
39 pub tau_ca: f64,
40 pub dt: f64,
41 pub v_threshold: f64,
42 pub gain: f64,
43}
44
45impl Default for SKNeuron {
46 fn default() -> Self {
47 Self::new()
48 }
49}
50
51impl SKNeuron {
52 pub fn new() -> Self {
53 Self {
54 v: -65.0,
55 h: 0.6,
56 n: 0.32,
57 ca: 0.0,
58 g_na: 35.0,
59 g_k: 9.0,
60 g_sk: 2.0,
61 g_l: 0.1,
62 e_na: 55.0,
63 e_k: -90.0,
64 e_l: -65.0,
65 c_m: 1.0,
66 phi: 5.0,
67 tau_ca: 150.0, dt: 0.5,
69 v_threshold: -20.0,
70 gain: 1.0,
71 }
72 }
73
74 pub fn step(&mut self, current: f64) -> i32 {
75 let input = self.gain * current;
76 let sub_steps = 50;
77 let sub_dt = self.dt / sub_steps as f64;
78 let mut fired = 0i32;
79
80 for _ in 0..sub_steps {
81 let v = self.v;
82
83 let alpha_m = safe_rate(0.1, 35.0, v, 10.0, 1.0);
84 let beta_m = 4.0 * (-(v + 60.0) / 18.0).exp();
85 let m_inf = alpha_m / (alpha_m + beta_m);
86
87 let alpha_h = 0.07 * (-(v + 58.0) / 20.0).exp();
88 let beta_h = 1.0 / (1.0 + (-(v + 28.0) / 10.0).exp());
89
90 let alpha_n = safe_rate(0.01, 34.0, v, 10.0, 0.1);
91 let beta_n = 0.125 * (-(v + 44.0) / 80.0).exp();
92
93 let ca2 = self.ca * self.ca;
95 let sk_inf = ca2 / (ca2 + 0.25); self.ca += sub_dt * (-self.ca / self.tau_ca);
99
100 self.h += sub_dt * self.phi * (alpha_h * (1.0 - self.h) - beta_h * self.h);
101 self.n += sub_dt * self.phi * (alpha_n * (1.0 - self.n) - beta_n * self.n);
102
103 let i_na = self.g_na * m_inf.powi(3) * self.h * (v - self.e_na);
104 let i_k = self.g_k * self.n.powi(4) * (v - self.e_k);
105 let i_sk = self.g_sk * sk_inf * (v - self.e_k);
106 let i_l = self.g_l * (v - self.e_l);
107
108 let dv = (-i_na - i_k - i_sk - i_l + input) / self.c_m;
109 self.v += sub_dt * dv;
110
111 if self.v >= self.v_threshold {
112 fired = 1;
113 self.v = -65.0;
114 self.ca += 0.2;
115 }
116 }
117
118 self.v = self.v.clamp(-100.0, 60.0);
119 if !self.v.is_finite() {
120 self.v = -65.0;
121 self.h = 0.6;
122 self.n = 0.32;
123 }
124 if !self.ca.is_finite() {
125 self.ca = 0.0;
126 }
127 self.h = self.h.clamp(0.0, 1.0);
128 self.n = self.n.clamp(0.0, 1.0);
129 self.ca = self.ca.max(0.0);
130
131 fired
132 }
133
134 pub fn reset(&mut self) {
135 *self = Self::new();
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
146 fn sk_fires_with_input() {
147 let mut n = SKNeuron::new();
148 let mut spikes = 0;
149 for _ in 0..2_000 {
150 spikes += n.step(2.0);
151 }
152 assert!(spikes > 5, "SK neuron must fire with input, got {spikes}");
153 }
154
155 #[test]
156 fn sk_silent_without_input() {
157 let mut n = SKNeuron::new();
158 let mut spikes = 0;
159 for _ in 0..10_000 {
160 spikes += n.step(0.0);
161 }
162 assert_eq!(
163 spikes, 0,
164 "SK neuron must be silent without input, got {spikes}"
165 );
166 }
167
168 #[test]
169 fn sk_adaptation() {
170 let mut n = SKNeuron::new();
172 let input = 5.0;
173 let mut early = 0;
174 for _ in 0..2000 {
175 early += n.step(input);
176 }
177 let mut late = 0;
178 for _ in 0..2000 {
179 late += n.step(input);
180 }
181 assert!(
182 early >= late,
183 "SK should cause adaptation: early={early}, late={late}"
184 );
185 }
186
187 #[test]
188 fn sk_ca_dependent_only() {
189 let n = SKNeuron::new();
191 let ca2 = n.ca * n.ca;
192 let sk_inf = ca2 / (ca2 + 0.25);
193 assert!(
194 sk_inf < 0.001,
195 "SK must be inactive at ca=0, sk_inf={sk_inf}"
196 );
197 }
198
199 #[test]
200 fn sk_reduces_firing_rate() {
201 let mut with_sk = SKNeuron::new();
202 let mut no_sk = SKNeuron::new();
203 no_sk.g_sk = 0.0;
204
205 let input = 3.0;
206 let mut spikes_sk = 0;
207 let mut spikes_no = 0;
208 for _ in 0..10_000 {
209 spikes_sk += with_sk.step(input);
210 spikes_no += no_sk.step(input);
211 }
212 assert!(
213 spikes_no >= spikes_sk,
214 "SK should reduce firing: SK={spikes_sk} vs none={spikes_no}"
215 );
216 }
217
218 #[test]
219 fn sk_negative_input_no_crash() {
220 let mut n = SKNeuron::new();
221 for _ in 0..10_000 {
222 n.step(-100.0);
223 }
224 assert!(n.v.is_finite());
225 }
226
227 #[test]
228 fn sk_nan_input_stays_finite() {
229 let mut n = SKNeuron::new();
230 n.step(f64::NAN);
231 assert!(n.v.is_finite());
232 }
233
234 #[test]
235 fn sk_extreme_input_bounded() {
236 let mut n = SKNeuron::new();
237 for _ in 0..1000 {
238 n.step(1e6);
239 }
240 assert!(n.v.is_finite() && n.v <= 60.0);
241 }
242
243 #[test]
244 fn sk_reset_clears_state() {
245 let mut n = SKNeuron::new();
246 for _ in 0..1000 {
247 n.step(10.0);
248 }
249 n.reset();
250 assert_eq!(n.v, -65.0);
251 assert_eq!(n.ca, 0.0);
252 }
253
254 #[test]
255 fn sk_performance_1k_steps() {
256 let start = std::time::Instant::now();
257 let mut n = SKNeuron::new();
258 for _ in 0..1_000 {
259 std::hint::black_box(n.step(3.0));
260 }
261 let elapsed = start.elapsed();
262 assert!(
263 elapsed.as_millis() < 200,
264 "1k steps must complete in <200ms"
265 );
266 }
267}