sc_neurocore_engine/neurons/biophysical/
mainen_sejnowski.rs1#[derive(Clone, Debug)]
13pub struct MainenSejnowskiNeuron {
14 pub vs: f64,
15 pub va: f64,
16 pub m: f64,
17 pub h: f64,
18 pub n: f64,
19 pub kappa: f64,
20 pub g_na: f64,
21 pub g_k: f64,
22 pub g_l: f64,
23 pub e_na: f64,
24 pub e_k: f64,
25 pub e_l: f64,
26 pub c_s: f64,
27 pub c_a: f64,
28 pub dt: f64,
29 pub v_threshold: f64,
30 pub legacy_sequential: bool,
35}
36
37impl MainenSejnowskiNeuron {
38 pub fn new() -> Self {
39 Self {
40 vs: -65.0,
41 va: -65.0,
42 m: 0.05,
43 h: 0.6,
44 n: 0.3,
45 kappa: 10.0,
46 g_na: 3000.0,
47 g_k: 1500.0,
48 g_l: 1.0,
49 e_na: 50.0,
50 e_k: -90.0,
51 e_l: -70.0,
52 c_s: 1.0,
53 c_a: 0.1,
54 dt: 0.005,
55 v_threshold: -20.0,
56 legacy_sequential: false,
57 }
58 }
59
60 pub fn new_legacy_sequential() -> Self {
64 let mut neuron = Self::new();
65 neuron.legacy_sequential = true;
66 neuron
67 }
68
69 fn valid(&self) -> bool {
70 let finite = [
71 self.vs,
72 self.va,
73 self.m,
74 self.h,
75 self.n,
76 self.kappa,
77 self.g_na,
78 self.g_k,
79 self.g_l,
80 self.e_na,
81 self.e_k,
82 self.e_l,
83 self.c_s,
84 self.c_a,
85 self.dt,
86 self.v_threshold,
87 ]
88 .into_iter()
89 .all(f64::is_finite);
90 finite
91 && (-200.0..=200.0).contains(&self.vs)
92 && (-200.0..=200.0).contains(&self.va)
93 && [self.m, self.h, self.n]
94 .into_iter()
95 .all(|gate| (0.0..=1.0).contains(&gate))
96 && (0.0..=100.0).contains(&self.kappa)
97 && (0.0..=5000.0).contains(&self.g_na)
98 && (0.0..=3000.0).contains(&self.g_k)
99 && (0.0..=5.0).contains(&self.g_l)
100 && (30.0..=70.0).contains(&self.e_na)
101 && (-100.0..=-70.0).contains(&self.e_k)
102 && (-90.0..=-50.0).contains(&self.e_l)
103 && (0.5..=2.0).contains(&self.c_s)
104 && (0.05..=1.0).contains(&self.c_a)
105 && self.dt > 0.0
106 && self.dt <= 0.1
107 && (-40.0..=20.0).contains(&self.v_threshold)
108 }
109
110 fn linoid(x: f64, k: f64) -> f64 {
112 if x == 0.0 {
113 k
114 } else {
115 x / -(-x / k).exp_m1()
116 }
117 }
118
119 pub fn try_step(&mut self, current: f64) -> Result<i32, &'static str> {
126 if !current.is_finite() {
127 return Err("current must be finite");
128 }
129 if !self.valid() {
130 return Err("Mainen-Sejnowski state and parameters must satisfy the public bounds");
131 }
132
133 let mut candidate = self.clone();
134 let v_prev = candidate.vs;
135 for _ in 0..20 {
136 if candidate.legacy_sequential {
137 Self::legacy_sequential_substep(&mut candidate, current);
138 } else {
139 Self::canonical_substep(&mut candidate, current);
140 }
141
142 if ![
143 candidate.vs,
144 candidate.va,
145 candidate.m,
146 candidate.h,
147 candidate.n,
148 ]
149 .into_iter()
150 .all(f64::is_finite)
151 {
152 return Err("Mainen-Sejnowski candidate state became non-finite");
153 }
154 }
155
156 *self = candidate;
157 if self.vs >= self.v_threshold && v_prev < self.v_threshold {
158 Ok(1)
159 } else {
160 Ok(0)
161 }
162 }
163
164 fn canonical_substep(candidate: &mut Self, current: f64) {
167 let va = candidate.va;
168 let am = 0.182 * Self::linoid(va + 25.0, 9.0);
170 let bm = 0.124 * Self::linoid(-(va + 25.0), 9.0);
171 let ah = 0.024 * Self::linoid(va + 40.0, 5.0);
172 let bh = 0.0091 * Self::linoid(-(va + 65.0), 5.0);
173 let an = 0.02 * Self::linoid(va - 20.0, 9.0);
174 let bn = 0.002 * Self::linoid(-(va - 20.0), 9.0);
175
176 candidate.m = (candidate.m + (am * (1.0 - candidate.m) - bm * candidate.m) * candidate.dt)
177 .clamp(0.0, 1.0);
178 candidate.h = (candidate.h + (ah * (1.0 - candidate.h) - bh * candidate.h) * candidate.dt)
179 .clamp(0.0, 1.0);
180 candidate.n = (candidate.n + (an * (1.0 - candidate.n) - bn * candidate.n) * candidate.dt)
181 .clamp(0.0, 1.0);
182
183 let i_na = candidate.g_na * candidate.m.powi(3) * candidate.h * (va - candidate.e_na);
184 let i_k = candidate.g_k * candidate.n * (va - candidate.e_k);
185 let i_l_s = candidate.g_l * (candidate.vs - candidate.e_l);
186
187 let dvs = (-i_l_s + candidate.kappa * (va - candidate.vs) + current) / candidate.c_s
188 * candidate.dt;
189 let dva =
190 (-i_na - i_k + candidate.kappa * (candidate.vs - va)) / candidate.c_a * candidate.dt;
191 candidate.vs = (candidate.vs + dvs).clamp(-200.0, 200.0);
192 candidate.va = (va + dva).clamp(-200.0, 200.0);
193 }
194
195 fn legacy_sequential_substep(candidate: &mut Self, current: f64) {
200 let x_am = candidate.va + 25.0;
201 let am = if x_am.abs() < 1e-6 {
202 0.182 * 9.0
203 } else {
204 0.182 * x_am / (1.0 - (-(x_am) / 9.0).exp() + 1e-12)
205 };
206 let bm = if x_am.abs() < 1e-6 {
207 0.124 * 9.0
208 } else {
209 -0.124 * x_am / (1.0 - ((x_am) / 9.0).exp() + 1e-12)
210 };
211 let x_ah = candidate.va + 40.0;
212 let ah = if x_ah.abs() < 1e-6 {
213 0.024 * 5.0
214 } else {
215 0.024 * x_ah / (1.0 - (-(x_ah) / 5.0).exp() + 1e-12)
216 };
217 let x_bh = candidate.va + 65.0;
218 let bh = if x_bh.abs() < 1e-6 {
219 0.0091 * 5.0
220 } else {
221 -0.0091 * x_bh / (1.0 - ((x_bh) / 5.0).exp() + 1e-12)
222 };
223 let x_an = candidate.va - 20.0;
224 let an = if x_an.abs() < 1e-6 {
225 0.02 * 9.0
226 } else {
227 0.02 * x_an / (1.0 - (-(x_an) / 9.0).exp() + 1e-12)
228 };
229 let bn = if x_an.abs() < 1e-6 {
230 0.002 * 9.0
231 } else {
232 -0.002 * x_an / (1.0 - ((x_an) / 9.0).exp() + 1e-12)
233 };
234 candidate.m = (candidate.m + (am * (1.0 - candidate.m) - bm * candidate.m) * candidate.dt)
235 .clamp(0.0, 1.0);
236 candidate.h = (candidate.h + (ah * (1.0 - candidate.h) - bh * candidate.h) * candidate.dt)
237 .clamp(0.0, 1.0);
238 candidate.n = (candidate.n + (an * (1.0 - candidate.n) - bn * candidate.n) * candidate.dt)
239 .clamp(0.0, 1.0);
240 let i_na =
241 candidate.g_na * candidate.m.powi(3) * candidate.h * (candidate.va - candidate.e_na);
242 let i_k = candidate.g_k * candidate.n * (candidate.va - candidate.e_k);
243 let i_l_s = candidate.g_l * (candidate.vs - candidate.e_l);
244 candidate.vs = (candidate.vs
245 + (-i_l_s + candidate.kappa * (candidate.va - candidate.vs) + current) / candidate.c_s
246 * candidate.dt)
247 .clamp(-200.0, 200.0);
248 candidate.va = (candidate.va
249 + (-i_na - i_k + candidate.kappa * (candidate.vs - candidate.va)) / candidate.c_a
250 * candidate.dt)
251 .clamp(-200.0, 200.0);
252 }
253
254 pub fn step(&mut self, current: f64) -> i32 {
257 self.try_step(current).unwrap_or(0)
258 }
259
260 pub fn reset(&mut self) {
263 self.vs = -65.0;
264 self.va = -65.0;
265 self.m = 0.05;
266 self.h = 0.6;
267 self.n = 0.3;
268 }
269}
270impl Default for MainenSejnowskiNeuron {
271 fn default() -> Self {
272 Self::new()
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 #[test]
281 fn default_matches_constructor_state() {
282 let default = MainenSejnowskiNeuron::default();
283 let constructed = MainenSejnowskiNeuron::new();
284 assert_eq!(default.vs, constructed.vs);
285 }
286
287 #[test]
288 fn removable_rate_singularities_use_finite_limits() {
289 for voltage in [-25.0, -40.0, -65.0, 20.0] {
290 let mut n = MainenSejnowskiNeuron::new();
291 n.va = voltage;
292 let spike = n.step(0.0);
293 assert!(matches!(spike, 0 | 1));
294 }
295 }
296
297 #[test]
298 fn mainen_fires() {
299 let mut n = MainenSejnowskiNeuron::new();
300 let t: i32 = (0..5000).map(|_| n.step(500.0)).sum();
301 assert!(t > 0);
302 }
303
304 #[test]
306 fn mainen_stable_without_input() {
307 let mut n = MainenSejnowskiNeuron::new();
310 for _ in 0..500 {
311 n.step(0.0);
312 }
313 assert!(n.vs.is_finite());
314 assert!(n.va.is_finite());
315 }
316 #[test]
317 fn mainen_reset_clears_state() {
318 let mut n = MainenSejnowskiNeuron::new();
319 for _ in 0..100 {
320 n.step(500.0);
321 }
322 n.reset();
323 assert!((n.vs - (-65.0)).abs() < 1e-10);
324 assert!((n.va - (-65.0)).abs() < 1e-10);
325 }
326 #[test]
327 fn mainen_moderate_input_stable() {
328 let mut n = MainenSejnowskiNeuron::new();
330 for _ in 0..200 {
331 n.step(500.0);
332 }
333 let _ = n.vs; }
337 #[test]
338 fn mainen_two_compartments_coupled() {
339 let n = MainenSejnowskiNeuron::new();
340 assert!(n.kappa > 0.0, "coupling should be positive");
342 }
343 #[test]
344 fn mainen_weak_negative_no_crash() {
345 let mut n = MainenSejnowskiNeuron::new();
346 for _ in 0..200 {
347 n.step(-10.0);
348 }
349 assert!(n.vs.is_finite());
351 }
352 #[test]
353 fn mainen_nan_input_is_rejected_atomically() {
354 let mut n = MainenSejnowskiNeuron::new();
355 let before = n.clone();
356 assert!(n.try_step(f64::NAN).is_err());
357 assert!(n.try_step(f64::INFINITY).is_err());
358 assert_eq!(n.vs, before.vs);
359 assert_eq!(n.va, before.va);
360 assert_eq!(n.m, before.m);
361 assert_eq!(n.h, before.h);
362 assert_eq!(n.n, before.n);
363 }
364
365 #[test]
366 fn mainen_invalid_configuration_is_rejected_atomically() {
367 let mut n = MainenSejnowskiNeuron::new();
368 n.c_s = 0.0;
369 let before = n.clone();
370 assert!(n.try_step(1.0).is_err());
371 assert_eq!(n.vs, before.vs);
372 assert_eq!(n.c_s, before.c_s);
373 }
374
375 #[test]
376 fn mainen_nominal_step_matches_reference_anchor() {
377 let mut n = MainenSejnowskiNeuron::new();
378 assert_eq!(n.try_step(10.0), Ok(0));
379 assert!((n.vs - -32.668_480_035_293_555).abs() < 1.0e-12);
380 assert!((n.va - 200.0).abs() < 1.0e-12);
381 assert!((n.m - 0.600_794_256_701_580_5).abs() < 1.0e-12);
382 assert!((n.h - 0.658_132_236_592_029_5).abs() < 1.0e-12);
383 assert!((n.n - 0.398_198_621_809_121).abs() < 1.0e-12);
384 }
385
386 #[test]
387 fn mainen_rate_limits_are_exact_and_continuous_at_singular_voltages() {
388 assert_eq!(MainenSejnowskiNeuron::linoid(0.0, 9.0), 9.0);
389 assert_eq!(MainenSejnowskiNeuron::linoid(0.0, 5.0), 5.0);
390 for k in [9.0, 5.0] {
391 assert!((MainenSejnowskiNeuron::linoid(1e-9, k) - k).abs() < 1e-8);
392 assert!((MainenSejnowskiNeuron::linoid(-1e-9, k) - k).abs() < 1e-8);
393 }
394 for v_singular in [-25.0, -40.0, -65.0, 20.0] {
395 let mut exact = MainenSejnowskiNeuron::new();
396 exact.va = v_singular;
397 let mut near = MainenSejnowskiNeuron::new();
398 near.va = v_singular + 1e-9;
399 exact.try_step(0.0).expect("finite drive");
400 near.try_step(0.0).expect("finite drive");
401 let delta = (exact.vs - near.vs)
402 .abs()
403 .max((exact.va - near.va).abs())
404 .max((exact.m - near.m).abs())
405 .max((exact.h - near.h).abs())
406 .max((exact.n - near.n).abs());
407 assert!(
408 delta < 1e-6,
409 "public step must be continuous at va={v_singular}, delta={delta}"
410 );
411 }
412 }
413
414 #[test]
415 fn mainen_legacy_sequential_reproduces_the_original_engine_trajectory() {
416 let mut legacy = MainenSejnowskiNeuron::new_legacy_sequential();
419 assert!(legacy.legacy_sequential);
420 assert_eq!(legacy.try_step(10.0), Ok(0));
421 assert!((legacy.vs - -32.668_480_035_293_555).abs() < 1.0e-12);
422 assert!((legacy.va - 200.0).abs() < 1.0e-12);
423 assert!((legacy.m - 0.600_794_256_701_518_1).abs() < 1.0e-12);
424 assert!((legacy.h - 0.658_132_236_591_979_1).abs() < 1.0e-12);
425 assert!((legacy.n - 0.398_198_621_809_030_8).abs() < 1.0e-12);
426
427 let mut long_run = MainenSejnowskiNeuron::new_legacy_sequential();
428 for _ in 0..50 {
429 long_run.step(0.5);
430 }
431 assert!((long_run.vs - -11.459_569_992_989_016).abs() < 1.0e-12);
432 assert!((long_run.h - 0.823_316_701_674_941_8).abs() < 1.0e-12);
433 assert!((long_run.n - 0.890_852_351_095_750_2).abs() < 1.0e-12);
434 }
435}