sc_neurocore_engine/neurons/channels/
sk.rs1use crate::neurons::biophysical::safe_rate;
10
11#[derive(Clone, Debug)]
29pub struct SKNeuron {
30 pub v: f64,
31 pub h: f64,
32 pub n: f64,
33 pub ca: f64,
34 pub g_na: f64,
35 pub g_k: f64,
36 pub g_sk: f64,
37 pub g_l: f64,
38 pub e_na: f64,
39 pub e_k: f64,
40 pub e_l: f64,
41 pub c_m: f64,
42 pub phi: f64,
43 pub tau_ca: f64,
44 pub dt: f64,
45 pub v_threshold: f64,
46 pub gain: f64,
47}
48
49impl Default for SKNeuron {
50 fn default() -> Self {
51 Self::new()
52 }
53}
54
55impl SKNeuron {
56 pub fn new() -> Self {
57 Self {
58 v: -65.0,
59 h: 0.6,
60 n: 0.32,
61 ca: 0.0,
62 g_na: 35.0,
63 g_k: 9.0,
64 g_sk: 2.0,
65 g_l: 0.1,
66 e_na: 55.0,
67 e_k: -90.0,
68 e_l: -65.0,
69 c_m: 1.0,
70 phi: 5.0,
71 tau_ca: 150.0, dt: 0.5,
73 v_threshold: -20.0,
74 gain: 1.0,
75 }
76 }
77
78 fn valid(&self) -> bool {
79 let finite = [
80 self.v,
81 self.h,
82 self.n,
83 self.ca,
84 self.g_na,
85 self.g_k,
86 self.g_sk,
87 self.g_l,
88 self.e_na,
89 self.e_k,
90 self.e_l,
91 self.c_m,
92 self.phi,
93 self.tau_ca,
94 self.dt,
95 self.v_threshold,
96 self.gain,
97 ]
98 .into_iter()
99 .all(f64::is_finite);
100 finite
101 && (-100.0..=60.0).contains(&self.v)
102 && [self.h, self.n]
103 .into_iter()
104 .all(|gate| (0.0..=1.0).contains(&gate))
105 && self.ca >= 0.0
106 && (0.0..=200.0).contains(&self.g_na)
107 && (0.0..=100.0).contains(&self.g_k)
108 && (0.0..=50.0).contains(&self.g_sk)
109 && (0.0..=5.0).contains(&self.g_l)
110 && (30.0..=70.0).contains(&self.e_na)
111 && (-100.0..=-70.0).contains(&self.e_k)
112 && (-80.0..=-40.0).contains(&self.e_l)
113 && (0.5..=2.0).contains(&self.c_m)
114 && (0.5..=10.0).contains(&self.phi)
115 && (10.0..=2000.0).contains(&self.tau_ca)
116 && self.dt > 0.0
117 && self.dt <= 1.0
118 && (-20.0..=20.0).contains(&self.v_threshold)
119 && (0.0..=10.0).contains(&self.gain)
120 }
121
122 pub fn try_step(&mut self, current: f64) -> Result<i32, &'static str> {
129 if !current.is_finite() {
130 return Err("current must be finite");
131 }
132 if !self.valid() {
133 return Err("SK state and parameters must satisfy the public bounds");
134 }
135
136 let mut candidate = self.clone();
137 let input = candidate.gain * current;
138 let sub_steps = 50;
139 let sub_dt = candidate.dt / sub_steps as f64;
140 let mut fired = 0i32;
141
142 for _ in 0..sub_steps {
143 let v = candidate.v;
144
145 let alpha_m = safe_rate(0.1, 35.0, v, 10.0, 1.0);
146 let beta_m = 4.0 * (-(v + 60.0) / 18.0).exp();
147 let m_inf = alpha_m / (alpha_m + beta_m);
148
149 let alpha_h = 0.07 * (-(v + 58.0) / 20.0).exp();
150 let beta_h = 1.0 / (1.0 + (-(v + 28.0) / 10.0).exp());
151
152 let alpha_n = safe_rate(0.01, 34.0, v, 10.0, 0.1);
153 let beta_n = 0.125 * (-(v + 44.0) / 80.0).exp();
154
155 let ca2 = candidate.ca * candidate.ca;
157 let sk_inf = ca2 / (ca2 + 0.25); candidate.ca += sub_dt * (-candidate.ca / candidate.tau_ca);
161
162 candidate.h +=
163 sub_dt * candidate.phi * (alpha_h * (1.0 - candidate.h) - beta_h * candidate.h);
164 candidate.n +=
165 sub_dt * candidate.phi * (alpha_n * (1.0 - candidate.n) - beta_n * candidate.n);
166
167 let i_na = candidate.g_na * m_inf.powi(3) * candidate.h * (v - candidate.e_na);
168 let i_k = candidate.g_k * candidate.n.powi(4) * (v - candidate.e_k);
169 let i_sk = candidate.g_sk * sk_inf * (v - candidate.e_k);
170 let i_l = candidate.g_l * (v - candidate.e_l);
171
172 let dv = (-i_na - i_k - i_sk - i_l + input) / candidate.c_m;
173 candidate.v += sub_dt * dv;
174 if ![candidate.v, candidate.h, candidate.n, candidate.ca]
175 .into_iter()
176 .all(f64::is_finite)
177 {
178 return Err("SK candidate state became non-finite");
179 }
180
181 if candidate.v >= candidate.v_threshold {
182 fired = 1;
183 candidate.v = -65.0;
184 candidate.ca += 0.2;
185 }
186 }
187
188 candidate.v = candidate.v.clamp(-100.0, 60.0);
189 candidate.h = candidate.h.clamp(0.0, 1.0);
190 candidate.n = candidate.n.clamp(0.0, 1.0);
191 candidate.ca = candidate.ca.max(0.0);
192 *self = candidate;
193
194 Ok(fired)
195 }
196
197 pub fn step(&mut self, current: f64) -> i32 {
200 self.try_step(current).unwrap_or(0)
201 }
202
203 pub fn reset(&mut self) {
206 self.v = -65.0;
207 self.h = 0.6;
208 self.n = 0.32;
209 self.ca = 0.0;
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
220 fn sk_fires_with_input() {
221 let mut n = SKNeuron::new();
222 let mut spikes = 0;
223 for _ in 0..2_000 {
224 spikes += n.step(2.0);
225 }
226 assert!(spikes > 5, "SK neuron must fire with input, got {spikes}");
227 }
228
229 #[test]
230 fn sk_silent_without_input() {
231 let mut n = SKNeuron::new();
232 let mut spikes = 0;
233 for _ in 0..10_000 {
234 spikes += n.step(0.0);
235 }
236 assert_eq!(
237 spikes, 0,
238 "SK neuron must be silent without input, got {spikes}"
239 );
240 }
241
242 #[test]
243 fn sk_nominal_step_matches_reference_anchor() {
244 let mut n = SKNeuron::new();
245 assert_eq!(n.try_step(5.0), Ok(0));
246 assert!((n.v - -63.180_064_213_072_19).abs() < 1.0e-12);
247 assert!((n.h - 0.648_122_835_749_998_1).abs() < 1.0e-12);
248 assert!((n.n - 0.237_186_365_946_861_5).abs() < 1.0e-12);
249 assert_eq!(n.ca, 0.0);
250 }
251
252 #[test]
253 fn sk_adaptation() {
254 let mut n = SKNeuron::new();
256 let input = 5.0;
257 let mut early = 0;
258 for _ in 0..2000 {
259 early += n.step(input);
260 }
261 let mut late = 0;
262 for _ in 0..2000 {
263 late += n.step(input);
264 }
265 assert!(
266 early >= late,
267 "SK should cause adaptation: early={early}, late={late}"
268 );
269 }
270
271 #[test]
272 fn sk_ca_dependent_only() {
273 let n = SKNeuron::new();
275 let ca2 = n.ca * n.ca;
276 let sk_inf = ca2 / (ca2 + 0.25);
277 assert!(
278 sk_inf < 0.001,
279 "SK must be inactive at ca=0, sk_inf={sk_inf}"
280 );
281 }
282
283 #[test]
284 fn sk_reduces_firing_rate() {
285 let mut with_sk = SKNeuron::new();
286 let mut no_sk = SKNeuron::new();
287 no_sk.g_sk = 0.0;
288
289 let input = 3.0;
290 let mut spikes_sk = 0;
291 let mut spikes_no = 0;
292 for _ in 0..10_000 {
293 spikes_sk += with_sk.step(input);
294 spikes_no += no_sk.step(input);
295 }
296 assert!(
297 spikes_no >= spikes_sk,
298 "SK should reduce firing: SK={spikes_sk} vs none={spikes_no}"
299 );
300 }
301
302 #[test]
303 fn sk_negative_input_no_crash() {
304 let mut n = SKNeuron::new();
305 for _ in 0..10_000 {
306 n.step(-100.0);
307 }
308 assert!(n.v.is_finite());
309 }
310
311 #[test]
312 fn sk_nan_input_is_rejected_atomically() {
313 let mut n = SKNeuron::new();
314 let before = n.clone();
315 assert!(n.try_step(f64::NAN).is_err());
316 assert_eq!(n.v, before.v);
317 assert_eq!(n.h, before.h);
318 assert_eq!(n.n, before.n);
319 assert_eq!(n.ca, before.ca);
320 }
321
322 #[test]
323 fn sk_infinite_input_is_rejected_atomically() {
324 let mut n = SKNeuron::new();
325 let before = n.clone();
326 assert!(n.try_step(f64::INFINITY).is_err());
327 assert!(n.try_step(f64::NEG_INFINITY).is_err());
328 assert_eq!(n.v, before.v);
329 assert_eq!(n.ca, before.ca);
330 }
331
332 #[test]
333 fn sk_invalid_configuration_is_rejected_atomically() {
334 let mut n = SKNeuron::new();
335 n.c_m = 0.0;
336 let before = n.clone();
337 assert!(n.try_step(1.0).is_err());
338 assert_eq!(n.v, before.v);
339 assert_eq!(n.c_m, before.c_m);
340 }
341
342 #[test]
343 fn sk_extreme_input_bounded() {
344 let mut n = SKNeuron::new();
345 for _ in 0..1000 {
346 n.step(1e6);
347 }
348 assert!(n.v.is_finite() && n.v <= 60.0);
349 }
350
351 #[test]
352 fn sk_reset_clears_state() {
353 let mut n = SKNeuron::new();
354 for _ in 0..1000 {
355 n.step(10.0);
356 }
357 n.reset();
358 assert_eq!(n.v, -65.0);
359 assert_eq!(n.ca, 0.0);
360 }
361
362 #[test]
363 fn sk_reset_preserves_parameters() {
364 let mut n = SKNeuron::new();
365 n.g_sk = 4.0;
366 for _ in 0..100 {
367 n.step(5.0);
368 }
369 n.reset();
370 assert_eq!(n.v, -65.0);
371 assert_eq!(n.g_sk, 4.0);
372 }
373
374 #[test]
375 fn sk_performance_1k_steps() {
376 let start = std::time::Instant::now();
377 let mut n = SKNeuron::new();
378 for _ in 0..1_000 {
379 std::hint::black_box(n.step(3.0));
380 }
381 let elapsed = start.elapsed();
382 assert!(
383 elapsed.as_millis() < 200,
384 "1k steps must complete in <200ms"
385 );
386 }
387}