1use super::biophysical::safe_rate;
16
17#[derive(Clone, Debug)]
42pub struct GranuleCell {
43 pub v: f64, pub m: f64, pub h: f64, pub n: f64, pub a: f64, pub b: f64, pub m_t: f64, pub s: f64, pub ca: f64, pub r: f64, pub c_m: f64, pub g_na: f64, pub g_kdr: f64, pub g_ka: f64, pub g_t: f64, pub g_kca: f64, pub g_h: f64, pub g_l: f64, pub g_tonic: f64, pub e_na: f64,
63 pub e_k: f64,
64 pub e_ca: f64,
65 pub e_h: f64, pub e_l: f64,
67 pub e_gaba: f64,
68 pub tau_ca: f64, pub kd_kca: f64, pub dt: f64,
71 pub sub_steps: usize,
72 pub gain: f64,
73}
74
75impl Default for GranuleCell {
76 fn default() -> Self {
77 Self::new()
78 }
79}
80
81impl GranuleCell {
82 pub fn new() -> Self {
83 Self {
84 v: -70.0,
85 m: 0.02,
86 h: 0.85,
87 n: 0.05,
88 a: 0.1,
89 b: 0.8,
90 m_t: 0.01,
91 s: 0.95,
92 ca: 0.05,
93 r: 0.1,
94 c_m: 1.0,
95 g_na: 17.0, g_kdr: 9.0, g_ka: 1.0, g_t: 0.5, g_kca: 3.5, g_h: 0.03, g_l: 0.1, g_tonic: 0.2, e_na: 87.4, e_k: -84.7,
105 e_ca: 129.3,
106 e_h: -40.0, e_l: -58.0, e_gaba: -75.0,
109 tau_ca: 10.0, kd_kca: 0.2, dt: 0.5,
112 sub_steps: 4, gain: 1.0,
114 }
115 }
116
117 #[inline]
119 fn boltz(v: f64, vh: f64, k: f64) -> f64 {
120 1.0 / (1.0 + (-(v - vh) / k).exp())
121 }
122
123 pub fn step(&mut self, current: f64) -> i32 {
124 let input = self.gain * current;
125 let dt_sub = self.dt / self.sub_steps as f64;
126 let v_prev = self.v;
127
128 for _ in 0..self.sub_steps {
129 let v = self.v;
130
131 let m_inf = Self::boltz(v, -30.0, 7.0);
133 let tau_m = 0.1 + 0.3 / (1.0 + ((v + 30.0) / 10.0).powi(2)).max(0.01);
134 self.m += dt_sub * (m_inf - self.m) / tau_m;
135
136 let h_inf = Self::boltz(v, -52.0, -6.0);
138 let tau_h = 0.5 + 5.0 / (1.0 + ((v + 50.0) / 15.0).powi(2)).max(0.01);
139 self.h += dt_sub * (h_inf - self.h) / tau_h;
140
141 let n_inf = Self::boltz(v, -35.0, 8.0);
143 let tau_n = 1.0 + 5.0 / (1.0 + ((v + 35.0) / 15.0).powi(2)).max(0.01);
144 self.n += dt_sub * (n_inf - self.n) / tau_n;
145
146 let a_inf = Self::boltz(v, -50.0, 20.0);
148 let tau_a = 2.0;
149 self.a += dt_sub * (a_inf - self.a) / tau_a;
150
151 let b_inf = Self::boltz(v, -70.0, -6.0);
153 let tau_b = 50.0;
154 self.b += dt_sub * (b_inf - self.b) / tau_b;
155
156 let mt_inf = Self::boltz(v, -52.0, 5.0);
158 let tau_mt = 1.0;
159 self.m_t += dt_sub * (mt_inf - self.m_t) / tau_mt;
160
161 let s_inf = Self::boltz(v, -60.0, -6.5);
163 let tau_s = 20.0 + 50.0 / (1.0 + ((v + 65.0) / 10.0).powi(2)).max(0.01);
164 self.s += dt_sub * (s_inf - self.s) / tau_s;
165
166 let r_inf = Self::boltz(v, -80.0, -10.0);
168 let tau_r = 50.0 + 200.0 / (1.0 + ((v + 80.0) / 20.0).powi(2)).max(0.01);
169 self.r += dt_sub * (r_inf - self.r) / tau_r;
170
171 self.m = self.m.clamp(0.0, 1.0);
173 self.h = self.h.clamp(0.0, 1.0);
174 self.n = self.n.clamp(0.0, 1.0);
175 self.a = self.a.clamp(0.0, 1.0);
176 self.b = self.b.clamp(0.0, 1.0);
177 self.m_t = self.m_t.clamp(0.0, 1.0);
178 self.s = self.s.clamp(0.0, 1.0);
179 self.r = self.r.clamp(0.0, 1.0);
180
181 let i_ca_t = self.g_t * self.m_t * self.m_t * self.s * (v - self.e_ca);
183 let ca_entry = if i_ca_t < 0.0 { -i_ca_t * 0.001 } else { 0.0 }; self.ca += dt_sub * (-self.ca / self.tau_ca + ca_entry);
185 self.ca = self.ca.max(0.0);
186
187 let kca_inf = self.ca * self.ca / (self.ca * self.ca + self.kd_kca * self.kd_kca);
189
190 let i_na = self.g_na * self.m.powi(3) * self.h * (v - self.e_na);
192 let i_kdr = self.g_kdr * self.n.powi(4) * (v - self.e_k);
193 let i_ka = self.g_ka * self.a.powi(3) * self.b * (v - self.e_k);
194 let i_kca = self.g_kca * kca_inf * (v - self.e_k);
195 let i_h = self.g_h * self.r * (v - self.e_h);
196 let i_l = self.g_l * (v - self.e_l);
197 let i_gaba = self.g_tonic * (v - self.e_gaba);
198
199 let dv =
200 (-(i_na + i_kdr + i_ka + i_ca_t + i_kca + i_h + i_l + i_gaba) + input) / self.c_m;
201 self.v += dt_sub * dv;
202 }
203
204 self.v = self.v.clamp(-100.0, 60.0);
206 if !self.v.is_finite() {
207 self.v = -70.0;
208 }
209 if !self.m.is_finite() {
210 self.m = 0.02;
211 }
212 if !self.h.is_finite() {
213 self.h = 0.85;
214 }
215 if !self.n.is_finite() {
216 self.n = 0.05;
217 }
218 if !self.ca.is_finite() {
219 self.ca = 0.05;
220 }
221
222 if self.v >= 0.0 && v_prev < 0.0 {
224 1
225 } else {
226 0
227 }
228 }
229
230 pub fn reset(&mut self) {
231 *self = Self::new();
232 }
233}
234
235#[derive(Clone, Debug)]
262pub struct GolgiCell {
263 pub v: f64,
264 pub m: f64, pub h: f64, pub p_na: f64, pub n: f64, pub a: f64, pub b: f64, pub w: f64, pub m_t: f64, pub s: f64, pub c_n: f64, pub r: f64, pub ca: f64, pub g_na_t: f64,
278 pub g_na_p: f64,
279 pub g_kdr: f64,
280 pub g_ka: f64,
281 pub g_km: f64,
282 pub g_cat: f64,
283 pub g_can: f64,
284 pub g_bk: f64,
285 pub g_sk: f64,
286 pub g_h: f64,
287 pub g_l: f64,
288 pub e_na: f64,
290 pub e_k: f64,
291 pub e_ca: f64,
292 pub e_h: f64,
293 pub e_l: f64,
294 pub c_m: f64,
295 pub tau_ca: f64,
296 pub kd_bk: f64,
297 pub kd_sk: f64,
298 pub dt: f64,
299 pub sub_steps: usize,
300 pub gain: f64,
301}
302
303impl Default for GolgiCell {
304 fn default() -> Self {
305 Self::new()
306 }
307}
308
309impl GolgiCell {
310 pub fn new() -> Self {
311 Self {
312 v: -60.0,
313 m: 0.02,
314 h: 0.85,
315 p_na: 0.01,
316 n: 0.05,
317 a: 0.1,
318 b: 0.8,
319 w: 0.01,
320 m_t: 0.01,
321 s: 0.9,
322 c_n: 0.01,
323 r: 0.1,
324 ca: 0.05,
325 g_na_t: 48.0, g_na_p: 0.2, g_kdr: 16.0,
328 g_ka: 8.0, g_km: 1.0, g_cat: 0.5, g_can: 1.0, g_bk: 3.0, g_sk: 1.0, g_h: 0.1, g_l: 0.05,
336 e_na: 55.0,
337 e_k: -90.0,
338 e_ca: 120.0,
339 e_h: -40.0,
340 e_l: -55.0, c_m: 1.0,
342 tau_ca: 200.0,
343 kd_bk: 1.0,
344 kd_sk: 0.5,
345 dt: 0.5,
346 sub_steps: 10,
347 gain: 1.0,
348 }
349 }
350
351 #[inline]
352 fn boltz(v: f64, vh: f64, k: f64) -> f64 {
353 1.0 / (1.0 + (-(v - vh) / k).exp())
354 }
355
356 pub fn step(&mut self, current: f64) -> i32 {
357 let input = self.gain * current;
358 let dt_sub = self.dt / self.sub_steps as f64;
359 let v_prev = self.v;
360
361 for _ in 0..self.sub_steps {
362 let v = self.v;
363
364 let alpha_m = safe_rate(0.1, 35.0, v, 10.0, 1.0);
366 let beta_m = 4.0 * (-(v + 60.0) / 18.0).exp();
367 let alpha_h = 0.07 * (-(v + 58.0) / 20.0).exp();
368 let beta_h = 1.0 / (1.0 + (-(v + 28.0) / 10.0).exp());
369 self.m += dt_sub * 5.0 * (alpha_m * (1.0 - self.m) - beta_m * self.m);
370 self.h += dt_sub * 5.0 * (alpha_h * (1.0 - self.h) - beta_h * self.h);
371
372 let pna_inf = Self::boltz(v, -48.0, 5.0);
374 let tau_pna = 5.0 + 20.0 / (1.0 + ((v + 48.0) / 10.0).powi(2)).max(0.01);
375 self.p_na += dt_sub * (pna_inf - self.p_na) / tau_pna;
376
377 let alpha_n = safe_rate(0.01, 34.0, v, 10.0, 0.1);
379 let beta_n = 0.125 * (-(v + 44.0) / 80.0).exp();
380 self.n += dt_sub * 5.0 * (alpha_n * (1.0 - self.n) - beta_n * self.n);
381
382 let a_inf = Self::boltz(v, -27.0, 16.0);
384 self.a += dt_sub * (a_inf - self.a) / 2.0;
385 let b_inf = Self::boltz(v, -80.0, -6.0);
386 self.b += dt_sub * (b_inf - self.b) / 15.0;
387
388 let w_inf = Self::boltz(v, -35.0, 10.0);
390 let tau_w = 100.0 / (3.3 * ((v + 35.0) / 20.0).exp() + (-(v + 35.0) / 20.0).exp());
391 self.w += dt_sub * (w_inf - self.w) / tau_w;
392
393 let mt_inf = Self::boltz(v, -52.0, 5.0);
395 self.m_t += dt_sub * (mt_inf - self.m_t) / 1.0;
396 let s_inf = Self::boltz(v, -60.0, -6.5);
397 let tau_s = 20.0 + 50.0 / (1.0 + ((v + 65.0) / 10.0).powi(2)).max(0.01);
398 self.s += dt_sub * (s_inf - self.s) / tau_s;
399
400 let cn_inf = Self::boltz(v, -20.0, 5.0);
402 let tau_cn = 2.0 + 10.0 / (1.0 + ((v + 20.0) / 10.0).powi(2)).max(0.01);
403 self.c_n += dt_sub * (cn_inf - self.c_n) / tau_cn;
404
405 let r_inf = Self::boltz(v, -80.0, -10.0);
407 let tau_r = 50.0 + 200.0 / (1.0 + ((v + 80.0) / 20.0).powi(2)).max(0.01);
408 self.r += dt_sub * (r_inf - self.r) / tau_r;
409
410 self.m = self.m.clamp(0.0, 1.0);
412 self.h = self.h.clamp(0.0, 1.0);
413 self.p_na = self.p_na.clamp(0.0, 1.0);
414 self.n = self.n.clamp(0.0, 1.0);
415 self.a = self.a.clamp(0.0, 1.0);
416 self.b = self.b.clamp(0.0, 1.0);
417 self.w = self.w.clamp(0.0, 1.0);
418 self.m_t = self.m_t.clamp(0.0, 1.0);
419 self.s = self.s.clamp(0.0, 1.0);
420 self.c_n = self.c_n.clamp(0.0, 1.0);
421 self.r = self.r.clamp(0.0, 1.0);
422
423 let i_cat = self.g_cat * self.m_t.powi(2) * self.s * (v - self.e_ca);
425 let i_can = self.g_can * self.c_n.powi(2) * (v - self.e_ca);
426 let ca_entry = if i_cat + i_can < 0.0 {
427 -(i_cat + i_can) * 0.001
428 } else {
429 0.0
430 };
431 self.ca += dt_sub * (ca_entry - self.ca / self.tau_ca);
432 self.ca = self.ca.max(0.0);
433
434 let ca2 = self.ca * self.ca;
437 let kd2 = self.kd_bk * self.kd_bk;
438 let bk_v = Self::boltz(v, 100.0 - 120.0 * ca2 / (ca2 + kd2), 15.0);
439 let sk_inf = self.ca.powi(2) / (self.ca.powi(2) + self.kd_sk.powi(2));
441
442 let i_na_t = self.g_na_t * self.m.powi(3) * self.h * (v - self.e_na);
444 let i_na_p = self.g_na_p * self.p_na * (v - self.e_na);
445 let i_kdr = self.g_kdr * self.n.powi(4) * (v - self.e_k);
446 let i_ka = self.g_ka * self.a.powi(3) * self.b * (v - self.e_k);
447 let i_km = self.g_km * self.w * (v - self.e_k);
448 let i_bk = self.g_bk * bk_v * (v - self.e_k);
449 let i_sk = self.g_sk * sk_inf * (v - self.e_k);
450 let i_h = self.g_h * self.r * (v - self.e_h);
451 let i_l = self.g_l * (v - self.e_l);
452
453 let dv = (-(i_na_t
454 + i_na_p
455 + i_kdr
456 + i_ka
457 + i_km
458 + i_cat
459 + i_can
460 + i_bk
461 + i_sk
462 + i_h
463 + i_l)
464 + input)
465 / self.c_m;
466 self.v += dt_sub * dv;
467 }
468
469 self.v = self.v.clamp(-100.0, 60.0);
471 if !self.v.is_finite() {
472 self.v = -60.0;
473 }
474 if !self.m.is_finite() {
475 self.m = 0.02;
476 }
477 if !self.h.is_finite() {
478 self.h = 0.85;
479 }
480 if !self.ca.is_finite() {
481 self.ca = 0.05;
482 }
483
484 if self.v >= 0.0 && v_prev < 0.0 {
486 1
487 } else {
488 0
489 }
490 }
491
492 pub fn reset(&mut self) {
493 *self = Self::new();
494 }
495}
496
497#[derive(Clone, Debug)]
514pub struct StellateCell {
515 pub v: f64,
516 pub h: f64, pub n: f64, pub p: f64, pub g_na: f64,
521 pub g_k: f64,
522 pub g_kv3: f64,
523 pub g_l: f64,
524 pub e_na: f64,
526 pub e_k: f64,
527 pub e_l: f64,
528 pub c_m: f64,
529 pub phi: f64,
530 pub dt: f64,
531 pub v_threshold: f64,
532 pub gain: f64,
533}
534
535impl Default for StellateCell {
536 fn default() -> Self {
537 Self::new()
538 }
539}
540
541impl StellateCell {
542 pub fn new() -> Self {
543 Self {
544 v: -65.0,
545 h: 0.6,
546 n: 0.32,
547 p: 0.0,
548 g_na: 35.0,
549 g_k: 9.0,
550 g_kv3: 3.0, g_l: 0.1,
552 e_na: 55.0,
553 e_k: -90.0,
554 e_l: -65.0,
555 c_m: 0.5, phi: 5.0,
557 dt: 0.5,
558 v_threshold: -20.0,
559 gain: 1.0,
560 }
561 }
562
563 pub fn step(&mut self, current: f64) -> i32 {
564 let input = self.gain * current;
565 let sub_steps = 50;
566 let sub_dt = self.dt / sub_steps as f64;
567 let mut fired = 0i32;
568
569 for _ in 0..sub_steps {
570 let v = self.v;
571
572 let alpha_m = safe_rate(0.1, 35.0, v, 10.0, 1.0);
574 let beta_m = 4.0 * (-(v + 60.0) / 18.0).exp();
575 let m_inf = alpha_m / (alpha_m + beta_m);
576
577 let alpha_h = 0.07 * (-(v + 58.0) / 20.0).exp();
578 let beta_h = 1.0 / (1.0 + (-(v + 28.0) / 10.0).exp());
579
580 let alpha_n = safe_rate(0.01, 34.0, v, 10.0, 0.1);
581 let beta_n = 0.125 * (-(v + 44.0) / 80.0).exp();
582
583 let p_inf = 1.0 / (1.0 + (-(v + 10.0) / 10.0).exp());
585 let tau_p = 1.0 + 4.0 / (1.0 + ((v + 20.0) / 15.0).exp());
586
587 self.h += sub_dt * self.phi * (alpha_h * (1.0 - self.h) - beta_h * self.h);
589 self.n += sub_dt * self.phi * (alpha_n * (1.0 - self.n) - beta_n * self.n);
590 self.p += sub_dt * (p_inf - self.p) / tau_p;
591
592 let i_na = self.g_na * m_inf.powi(3) * self.h * (v - self.e_na);
594 let i_k = self.g_k * self.n.powi(4) * (v - self.e_k);
595 let i_kv3 = self.g_kv3 * self.p.powi(2) * (v - self.e_k);
596 let i_l = self.g_l * (v - self.e_l);
597
598 let dv = (-i_na - i_k - i_kv3 - i_l + input) / self.c_m;
599 self.v += sub_dt * dv;
600
601 if self.v >= self.v_threshold {
602 fired = 1;
603 self.v = -65.0;
604 }
605 }
606
607 self.v = self.v.clamp(-100.0, 60.0);
609 if !self.v.is_finite() {
610 self.v = -65.0;
611 self.h = 0.6;
612 self.n = 0.32;
613 }
614 self.h = self.h.clamp(0.0, 1.0);
615 self.n = self.n.clamp(0.0, 1.0);
616 self.p = self.p.clamp(0.0, 1.0);
617
618 fired
619 }
620
621 pub fn reset(&mut self) {
622 *self = Self::new();
623 }
624}
625
626#[derive(Clone, Debug)]
642pub struct LugaroCell {
643 pub v: f64,
644 pub adapt: f64, pub v_rest: f64,
646 pub v_reset: f64,
647 pub v_threshold: f64,
648 pub tau_m: f64,
649 pub tau_adapt: f64,
650 pub a_adapt: f64, pub gain: f64,
652 pub serotonin: f64, pub dt: f64,
654}
655
656impl Default for LugaroCell {
657 fn default() -> Self {
658 Self::new()
659 }
660}
661
662impl LugaroCell {
663 pub fn new() -> Self {
664 Self {
665 v: -55.0,
666 adapt: 0.0,
667 v_rest: -55.0, v_reset: -65.0,
669 v_threshold: -48.0,
670 tau_m: 10.0,
671 tau_adapt: 150.0,
672 a_adapt: 0.05,
673 gain: 2.0,
674 serotonin: 0.0, dt: 0.5,
676 }
677 }
678
679 pub fn with_serotonin(serotonin_level: f64) -> Self {
681 let mut n = Self::new();
682 n.serotonin = serotonin_level.clamp(0.0, 1.0);
683 n
684 }
685
686 pub fn step(&mut self, current: f64) -> i32 {
687 let effective_gain = self.gain * (1.0 + 0.5 * self.serotonin);
689 let input = effective_gain * current;
690
691 let dv = (-(self.v - self.v_rest) - self.adapt + input) / self.tau_m;
693 self.v += self.dt * dv;
694
695 let da = (self.a_adapt * (self.v - self.v_rest) - self.adapt) / self.tau_adapt;
697 self.adapt += self.dt * da;
698
699 if self.v >= self.v_threshold {
701 self.v = self.v_reset;
702 self.adapt += 1.0; return 1;
704 }
705
706 self.v = self.v.clamp(-100.0, 60.0);
708 if !self.v.is_finite() {
709 self.v = self.v_reset;
710 }
711 if !self.adapt.is_finite() {
712 self.adapt = 0.0;
713 }
714
715 0
716 }
717
718 pub fn reset(&mut self) {
719 *self = Self::new();
720 }
721}
722
723#[derive(Clone, Debug)]
740pub struct UnipolarBrushCell {
741 pub v: f64,
742 pub persistent: f64, pub v_rest: f64,
744 pub v_reset: f64,
745 pub v_threshold: f64,
746 pub tau_m: f64,
747 pub tau_persistent: f64, pub persistent_gain: f64, pub gain: f64,
750 pub dt: f64,
751}
752
753impl Default for UnipolarBrushCell {
754 fn default() -> Self {
755 Self::new()
756 }
757}
758
759impl UnipolarBrushCell {
760 pub fn new() -> Self {
761 Self {
762 v: -65.0,
763 persistent: 0.0,
764 v_rest: -65.0,
765 v_reset: -70.0,
766 v_threshold: -50.0,
767 tau_m: 8.0,
768 tau_persistent: 200.0,
769 persistent_gain: 0.5,
770 gain: 2.5,
771 dt: 0.5,
772 }
773 }
774
775 pub fn step(&mut self, current: f64) -> i32 {
776 let input = self.gain * current.max(0.0);
777
778 let dp = (self.persistent_gain * input - self.persistent) / self.tau_persistent;
780 self.persistent += self.dt * dp;
781 if self.persistent < 0.0 {
782 self.persistent = 0.0;
783 }
784
785 let dv = (-(self.v - self.v_rest) + input + self.persistent) / self.tau_m;
787 self.v += self.dt * dv;
788
789 if self.v >= self.v_threshold {
791 self.v = self.v_reset;
792 return 1;
793 }
794
795 self.v = self.v.clamp(-100.0, 60.0);
797 if !self.v.is_finite() {
798 self.v = self.v_reset;
799 }
800 if !self.persistent.is_finite() {
801 self.persistent = 0.0;
802 }
803
804 0
805 }
806
807 pub fn reset(&mut self) {
808 *self = Self::new();
809 }
810}
811
812#[derive(Clone, Debug)]
831pub struct DCNNeuron {
832 pub v: f64,
833 pub h: f64, pub n: f64, pub p: f64, pub s: f64, pub r: f64, pub ca: f64, pub g_na: f64,
841 pub g_nap: f64, pub g_k: f64,
843 pub g_t: f64, pub g_ahp: f64, pub g_h: f64, pub g_l: f64,
847 pub e_na: f64,
849 pub e_k: f64,
850 pub e_ca: f64,
851 pub e_h: f64,
852 pub e_l: f64,
853 pub c_m: f64,
854 pub phi: f64,
855 pub tau_ca: f64,
856 pub kd_ahp: f64,
857 pub dt: f64,
858 pub v_threshold: f64,
859 pub gain: f64,
860}
861
862impl Default for DCNNeuron {
863 fn default() -> Self {
864 Self::new()
865 }
866}
867
868impl DCNNeuron {
869 pub fn new() -> Self {
870 Self {
871 v: -60.0,
872 h: 0.6,
873 n: 0.32,
874 p: 0.01, s: 0.8, r: 0.1, ca: 0.05, g_na: 35.0,
879 g_nap: 0.5, g_k: 9.0,
881 g_t: 0.1, g_ahp: 2.0, g_h: 0.02, g_l: 0.2, e_na: 55.0,
886 e_k: -90.0,
887 e_ca: 120.0,
888 e_h: -40.0,
889 e_l: -65.0,
890 c_m: 1.0,
891 phi: 5.0,
892 tau_ca: 150.0,
893 kd_ahp: 0.5,
894 dt: 0.5,
895 v_threshold: -20.0,
896 gain: 1.0,
897 }
898 }
899
900 pub fn step(&mut self, current: f64) -> i32 {
901 let input = self.gain * current;
902 let sub_steps = 20;
903 let sub_dt = self.dt / sub_steps as f64;
904 let mut fired = 0i32;
905
906 for _ in 0..sub_steps {
907 let v = self.v;
908
909 let alpha_m = safe_rate(0.1, 35.0, v, 10.0, 1.0);
911 let beta_m = 4.0 * (-(v + 60.0) / 18.0).exp();
912 let m_inf = alpha_m / (alpha_m + beta_m);
913
914 let alpha_h = 0.07 * (-(v + 58.0) / 20.0).exp();
915 let beta_h = 1.0 / (1.0 + (-(v + 28.0) / 10.0).exp());
916
917 let alpha_n = safe_rate(0.01, 34.0, v, 10.0, 0.1);
919 let beta_n = 0.125 * (-(v + 44.0) / 80.0).exp();
920
921 let p_inf = 1.0 / (1.0 + (-(v + 48.0) / 5.0).exp());
923 let tau_p = 5.0 + 15.0 / (1.0 + ((v + 48.0) / 10.0).powi(2)).max(0.01);
924
925 let m_t_inf = 1.0 / (1.0 + (-(v + 52.0) / 5.0).exp());
927 let s_inf = 1.0 / (1.0 + ((v + 60.0) / 6.5).exp());
928 let tau_s = 20.0 + 50.0 / (1.0 + ((v + 65.0) / 10.0).exp());
929
930 let r_inf = 1.0 / (1.0 + ((v + 80.0) / 10.0).exp());
932 let tau_r = 100.0 + 200.0 / (1.0 + ((v + 70.0) / 10.0).exp());
933
934 self.h += sub_dt * self.phi * (alpha_h * (1.0 - self.h) - beta_h * self.h);
936 self.n += sub_dt * self.phi * (alpha_n * (1.0 - self.n) - beta_n * self.n);
937 self.p += sub_dt * (p_inf - self.p) / tau_p;
938 self.s += sub_dt * (s_inf - self.s) / tau_s;
939 self.r += sub_dt * (r_inf - self.r) / tau_r;
940
941 let i_t = self.g_t * m_t_inf.powi(2) * self.s * (v - self.e_ca);
943 let ca_entry = if i_t < 0.0 { -i_t * 0.001 } else { 0.0 };
944 self.ca += sub_dt * (ca_entry - self.ca / self.tau_ca);
945 self.ca = self.ca.max(0.0);
946
947 let ahp_inf = self.ca.powi(2) / (self.ca.powi(2) + self.kd_ahp.powi(2));
949
950 let i_na = self.g_na * m_inf.powi(3) * self.h * (v - self.e_na);
952 let i_nap = self.g_nap * self.p * (v - self.e_na);
953 let i_k = self.g_k * self.n.powi(4) * (v - self.e_k);
954 let i_ahp = self.g_ahp * ahp_inf * (v - self.e_k);
955 let i_h = self.g_h * self.r * (v - self.e_h);
956 let i_l = self.g_l * (v - self.e_l);
957
958 let dv = (-i_na - i_nap - i_k - i_t - i_ahp - i_h - i_l + input) / self.c_m;
959 self.v += sub_dt * dv;
960
961 if self.v >= self.v_threshold {
962 fired = 1;
963 self.v = -60.0;
964 self.s *= 0.5; self.ca += 0.5; }
967 }
968
969 self.v = self.v.clamp(-100.0, 60.0);
971 if !self.v.is_finite() {
972 self.v = -60.0;
973 self.h = 0.6;
974 self.n = 0.32;
975 }
976 self.h = self.h.clamp(0.0, 1.0);
977 self.n = self.n.clamp(0.0, 1.0);
978 self.p = self.p.clamp(0.0, 1.0);
979 self.s = self.s.clamp(0.0, 1.0);
980 self.r = self.r.clamp(0.0, 1.0);
981
982 fired
983 }
984
985 pub fn reset(&mut self) {
986 *self = Self::new();
987 }
988}
989
990#[cfg(test)]
995mod tests {
996 use super::*;
997
998 #[test]
1001 fn granule_fires_with_strong_input() {
1002 let mut n = GranuleCell::new();
1003 let mut spikes = 0;
1004 for _ in 0..10_000 {
1005 spikes += n.step(15.0);
1006 }
1007 assert!(
1008 spikes > 10,
1009 "Granule cell must fire with strong excitatory input, got {spikes}"
1010 );
1011 }
1012
1013 #[test]
1014 fn granule_silent_at_rest() {
1015 let mut n = GranuleCell::new();
1016 let mut spikes = 0;
1017 for _ in 0..10_000 {
1018 spikes += n.step(0.0);
1019 }
1020 assert_eq!(
1021 spikes, 0,
1022 "Granule cell must be silent without input (tonic GABA inhibition)"
1023 );
1024 }
1025
1026 #[test]
1027 fn granule_no_fire_weak_input() {
1028 let mut n = GranuleCell::new();
1030 let mut spikes = 0;
1031 for _ in 0..10_000 {
1032 spikes += n.step(1.0);
1033 }
1034 assert!(
1035 spikes == 0,
1036 "Weak input should not overcome tonic GABA, got {spikes}"
1037 );
1038 }
1039
1040 #[test]
1041 fn granule_tonic_gaba_raises_threshold() {
1042 let mut with_gaba = GranuleCell::new();
1044 let mut no_gaba = GranuleCell::new();
1045 no_gaba.g_tonic = 0.0;
1046
1047 let input = 8.0;
1048 let mut spikes_gaba = 0;
1049 let mut spikes_no_gaba = 0;
1050 for _ in 0..10_000 {
1051 spikes_gaba += with_gaba.step(input);
1052 spikes_no_gaba += no_gaba.step(input);
1053 }
1054 assert!(
1055 spikes_no_gaba > spikes_gaba,
1056 "Removing tonic GABA must increase firing: no_gaba={spikes_no_gaba} vs gaba={spikes_gaba}"
1057 );
1058 }
1059
1060 #[test]
1061 fn granule_has_seven_currents() {
1062 let n = GranuleCell::new();
1064 assert!(n.g_na > 0.0, "Must have INa");
1065 assert!(n.g_kdr > 0.0, "Must have IK_dr");
1066 assert!(n.g_ka > 0.0, "Must have IK_A");
1067 assert!(n.g_t > 0.0, "Must have ICa_T");
1068 assert!(n.g_kca > 0.0, "Must have IK_Ca");
1069 assert!(n.g_h > 0.0, "Must have Ih");
1070 assert!(n.g_l > 0.0, "Must have IL");
1071 }
1072
1073 #[test]
1074 fn granule_t_type_deinactivates_at_rest() {
1075 let mut n = GranuleCell::new();
1077 for _ in 0..5000 {
1078 n.step(0.0);
1079 }
1080 assert!(
1081 n.s > 0.5,
1082 "T-type must be partially de-inactivated at rest, s={}",
1083 n.s
1084 );
1085 }
1086
1087 #[test]
1088 fn granule_ca_rises_with_spiking() {
1089 let mut n = GranuleCell::new();
1091 let ca0 = n.ca;
1092 for _ in 0..5000 {
1093 n.step(15.0);
1094 }
1095 assert!(
1096 n.ca > ca0,
1097 "Ca²⁺ should rise during spiking: ca0={ca0}, ca_now={}",
1098 n.ca
1099 );
1100 }
1101
1102 #[test]
1103 fn granule_negative_input_no_crash() {
1104 let mut n = GranuleCell::new();
1105 for _ in 0..10_000 {
1106 n.step(-100.0);
1107 }
1108 assert!(n.v.is_finite(), "Must stay finite with negative input");
1109 assert!(n.v >= -100.0, "Must be bounded");
1110 }
1111
1112 #[test]
1113 fn granule_nan_input_stays_finite() {
1114 let mut n = GranuleCell::new();
1115 n.step(f64::NAN);
1116 assert!(n.v.is_finite(), "NaN input must not corrupt state");
1117 }
1118
1119 #[test]
1120 fn granule_extreme_input_bounded() {
1121 let mut n = GranuleCell::new();
1122 for _ in 0..1000 {
1123 n.step(1e6);
1124 }
1125 assert!(
1126 n.v.is_finite() && n.v <= 60.0,
1127 "Extreme input must stay bounded"
1128 );
1129 }
1130
1131 #[test]
1132 fn granule_reset_clears_state() {
1133 let mut n = GranuleCell::new();
1134 for _ in 0..1000 {
1135 n.step(20.0);
1136 }
1137 n.reset();
1138 assert_eq!(n.v, -70.0);
1139 assert_eq!(n.s, 0.95);
1140 assert_eq!(n.m, 0.02);
1141 }
1142
1143 #[test]
1144 fn granule_high_input_resistance() {
1145 let mut n = GranuleCell::new();
1147 let v_before = n.v;
1148 n.step(5.0);
1150 let dv = n.v - v_before;
1151 assert!(
1152 dv > 0.5,
1153 "High Rin should give large voltage change, got dv={dv}"
1154 );
1155 }
1156
1157 #[test]
1158 fn granule_performance_10k_steps() {
1159 let start = std::time::Instant::now();
1160 let mut n = GranuleCell::new();
1161 for _ in 0..10_000 {
1162 std::hint::black_box(n.step(10.0));
1163 }
1164 let elapsed = start.elapsed();
1165 assert!(
1166 elapsed.as_millis() < 50,
1167 "10k steps must complete in <50ms, took {}ms",
1168 elapsed.as_millis()
1169 );
1170 }
1171
1172 #[test]
1175 fn golgi_fires_with_input() {
1176 let mut n = GolgiCell::new();
1177 let mut spikes = 0;
1178 for _ in 0..10_000 {
1179 spikes += n.step(15.0);
1180 }
1181 assert!(
1182 spikes > 10,
1183 "Golgi cell must fire with excitatory input, got {spikes}"
1184 );
1185 }
1186
1187 #[test]
1188 fn golgi_spontaneous_firing() {
1189 let mut n = GolgiCell::new();
1191 let _spikes: i32 = (0..20_000).map(|_| n.step(0.0)).sum();
1192 let mut n2 = GolgiCell::new();
1195 let mut spikes_small = 0;
1196 for _ in 0..20_000 {
1197 spikes_small += n2.step(0.5);
1198 }
1199 assert!(
1200 spikes_small > 0,
1201 "Golgi cell should fire with minimal input (near-threshold), got {spikes_small}"
1202 );
1203 }
1204
1205 #[test]
1206 fn golgi_ahp_reduces_rate_at_high_drive() {
1207 let mut with_ahp = GolgiCell::new();
1209 let mut no_ahp = GolgiCell::new();
1210 no_ahp.g_bk = 0.0;
1211 no_ahp.g_sk = 0.0;
1212 let mut spikes_with = 0;
1213 let mut spikes_no = 0;
1214 for _ in 0..10_000 {
1215 spikes_with += with_ahp.step(10.0);
1216 spikes_no += no_ahp.step(10.0);
1217 }
1218 assert!(
1219 spikes_no >= spikes_with,
1220 "AHP removal should increase firing: with={spikes_with}, without={spikes_no}"
1221 );
1222 }
1223
1224 #[test]
1225 fn golgi_ka_is_transient() {
1226 let mut with_a = GolgiCell::new();
1229 let mut no_a = GolgiCell::new();
1230 no_a.g_ka = 0.0;
1231 let mut spikes_with = 0;
1232 let mut spikes_no = 0;
1233 for _ in 0..10_000 {
1234 spikes_with += with_a.step(5.0);
1235 spikes_no += no_a.step(5.0);
1236 }
1237 assert!(spikes_with > 0, "Must fire with K_A");
1239 assert!(
1241 spikes_with != spikes_no,
1242 "K_A should affect firing rate: with={spikes_with}, without={spikes_no}"
1243 );
1244 }
1245
1246 #[test]
1247 fn golgi_ca_accumulates_during_spiking() {
1248 let mut n = GolgiCell::new();
1249 let ca_init = n.ca;
1250 for _ in 0..5000 {
1251 n.step(10.0);
1252 }
1253 assert!(
1254 n.ca > ca_init,
1255 "Ca²⁺ must rise during spiking: init={ca_init}, now={}",
1256 n.ca
1257 );
1258 }
1259
1260 #[test]
1261 fn golgi_negative_input_no_crash() {
1262 let mut n = GolgiCell::new();
1263 for _ in 0..10_000 {
1264 n.step(-100.0);
1265 }
1266 assert!(n.v.is_finite(), "Must stay finite with negative input");
1267 assert!(n.v >= -100.0);
1268 }
1269
1270 #[test]
1271 fn golgi_nan_input_stays_finite() {
1272 let mut n = GolgiCell::new();
1273 n.step(f64::NAN);
1274 assert!(n.v.is_finite(), "NaN input must not corrupt state");
1275 }
1276
1277 #[test]
1278 fn golgi_extreme_input_bounded() {
1279 let mut n = GolgiCell::new();
1280 for _ in 0..1000 {
1281 n.step(1e6);
1282 }
1283 assert!(
1284 n.v.is_finite() && n.v <= 60.0,
1285 "Extreme input must stay bounded"
1286 );
1287 }
1288
1289 #[test]
1290 fn golgi_reset_clears_state() {
1291 let mut n = GolgiCell::new();
1292 for _ in 0..5000 {
1293 n.step(10.0);
1294 }
1295 n.reset();
1296 let fresh = GolgiCell::new();
1297 assert_eq!(n.v, fresh.v);
1298 assert_eq!(n.ca, fresh.ca);
1299 assert_eq!(n.m, fresh.m);
1300 assert_eq!(n.h, fresh.h);
1301 assert_eq!(n.p_na, fresh.p_na);
1302 assert_eq!(n.w, fresh.w);
1303 assert_eq!(n.r, fresh.r);
1304 }
1305
1306 #[test]
1307 fn golgi_gates_bounded() {
1308 let mut n = GolgiCell::new();
1309 for _ in 0..10_000 {
1310 n.step(15.0);
1311 }
1312 for (name, val) in [
1314 ("m", n.m),
1315 ("h", n.h),
1316 ("p_na", n.p_na),
1317 ("n", n.n),
1318 ("a", n.a),
1319 ("b", n.b),
1320 ("w", n.w),
1321 ("m_t", n.m_t),
1322 ("s", n.s),
1323 ("c_n", n.c_n),
1324 ("r", n.r),
1325 ] {
1326 assert!((0.0..=1.0).contains(&val), "{name} out of bounds: {val}");
1327 }
1328 assert!(n.ca >= 0.0, "Ca²⁺ must be non-negative: {}", n.ca);
1329 }
1330
1331 #[test]
1332 fn golgi_has_eleven_currents() {
1333 let n = GolgiCell::new();
1335 assert!(n.g_na_t > 0.0, "Na_t missing");
1336 assert!(n.g_na_p > 0.0, "Na_p missing");
1337 assert!(n.g_kdr > 0.0, "K_dr missing");
1338 assert!(n.g_ka > 0.0, "K_A missing");
1339 assert!(n.g_km > 0.0, "K_M missing");
1340 assert!(n.g_cat > 0.0, "Ca_T missing");
1341 assert!(n.g_can > 0.0, "Ca_N missing");
1342 assert!(n.g_bk > 0.0, "BK missing");
1343 assert!(n.g_sk > 0.0, "SK missing");
1344 assert!(n.g_h > 0.0, "Ih missing");
1345 assert!(n.g_l > 0.0, "Leak missing");
1346 }
1347
1348 #[test]
1349 fn golgi_persistent_na_depolarises() {
1350 let mut with_nap = GolgiCell::new();
1352 let mut no_nap = GolgiCell::new();
1353 no_nap.g_na_p = 0.0;
1354 let mut spikes_with = 0;
1355 let mut spikes_no = 0;
1356 for _ in 0..10_000 {
1357 spikes_with += with_nap.step(2.0);
1358 spikes_no += no_nap.step(2.0);
1359 }
1360 assert!(
1361 spikes_with >= spikes_no,
1362 "Na_p should increase excitability: with={spikes_with} vs without={spikes_no}"
1363 );
1364 }
1365
1366 #[test]
1367 fn golgi_km_slows_firing() {
1368 let mut with_km = GolgiCell::new();
1370 let mut no_km = GolgiCell::new();
1371 no_km.g_km = 0.0;
1372 let mut spikes_with = 0;
1373 let mut spikes_no = 0;
1374 for _ in 0..10_000 {
1375 spikes_with += with_km.step(10.0);
1376 spikes_no += no_km.step(10.0);
1377 }
1378 assert!(
1379 spikes_no >= spikes_with,
1380 "K_M should reduce firing rate: with_km={spikes_with}, without={spikes_no}"
1381 );
1382 }
1383
1384 #[test]
1385 fn golgi_ih_sag() {
1386 let mut with_h = GolgiCell::new();
1388 let mut no_h = GolgiCell::new();
1389 no_h.g_h = 0.0;
1390 for _ in 0..10_000 {
1392 with_h.step(-1.0);
1393 no_h.step(-1.0);
1394 }
1395 assert!(
1397 with_h.v > no_h.v,
1398 "Ih should cause sag (less hyperpolarised): with_h={:.1} vs no_h={:.1}",
1399 with_h.v,
1400 no_h.v
1401 );
1402 }
1403
1404 #[test]
1405 fn golgi_bk_fast_ahp() {
1406 let mut with_bk = GolgiCell::new();
1408 let mut no_bk = GolgiCell::new();
1409 no_bk.g_bk = 0.0;
1410 let mut spikes_with = 0;
1412 let mut spikes_no = 0;
1413 for _ in 0..10_000 {
1414 spikes_with += with_bk.step(10.0);
1415 spikes_no += no_bk.step(10.0);
1416 }
1417 assert!(
1419 spikes_with > 0 && spikes_no > 0,
1420 "Both should fire: with_bk={spikes_with}, no_bk={spikes_no}"
1421 );
1422 }
1423
1424 #[test]
1425 fn golgi_sk_slow_adaptation() {
1426 let mut with_sk = GolgiCell::new();
1428 let mut no_sk = GolgiCell::new();
1429 no_sk.g_sk = 0.0;
1430 let mut spikes_with = 0;
1431 let mut spikes_no = 0;
1432 for _ in 0..20_000 {
1433 spikes_with += with_sk.step(8.0);
1434 spikes_no += no_sk.step(8.0);
1435 }
1436 assert!(
1437 spikes_no >= spikes_with,
1438 "SK removal should increase firing: with_sk={spikes_with}, no_sk={spikes_no}"
1439 );
1440 }
1441
1442 #[test]
1443 fn golgi_performance_1k_steps() {
1444 let start = std::time::Instant::now();
1445 let mut n = GolgiCell::new();
1446 for _ in 0..1_000 {
1447 std::hint::black_box(n.step(5.0));
1448 }
1449 let elapsed = start.elapsed();
1450 assert!(
1451 elapsed.as_millis() < 50,
1452 "1k steps must complete in <50ms, took {}ms",
1453 elapsed.as_millis()
1454 );
1455 }
1456
1457 #[test]
1460 fn stellate_fires_with_input() {
1461 let mut n = StellateCell::new();
1462 let mut spikes = 0;
1463 for _ in 0..2_000 {
1464 spikes += n.step(2.0);
1465 }
1466 assert!(
1467 spikes > 5,
1468 "Stellate cell must fire with input, got {spikes}"
1469 );
1470 }
1471
1472 #[test]
1473 fn stellate_silent_without_input() {
1474 let mut n = StellateCell::new();
1475 let mut spikes = 0;
1476 for _ in 0..10_000 {
1477 spikes += n.step(0.0);
1478 }
1479 assert_eq!(
1480 spikes, 0,
1481 "Stellate cell must be silent without input, got {spikes}"
1482 );
1483 }
1484
1485 #[test]
1486 fn stellate_high_frequency() {
1487 let mut n = StellateCell::new();
1489 let mut spikes = 0;
1490 for _ in 0..2_000 {
1491 spikes += n.step(20.0);
1492 }
1493 assert!(
1495 spikes > 50,
1496 "FS stellate should fire at high rate, got {spikes}"
1497 );
1498 }
1499
1500 #[test]
1501 fn stellate_minimal_adaptation() {
1502 let mut n = StellateCell::new();
1504 let input = 10.0;
1505 let mut spikes_early = 0;
1506 for _ in 0..2000 {
1507 spikes_early += n.step(input);
1508 }
1509 let mut spikes_late = 0;
1510 for _ in 0..2000 {
1511 spikes_late += n.step(input);
1512 }
1513 let diff = (spikes_early - spikes_late).abs();
1515 assert!(
1516 diff < 20,
1517 "FS should have minimal adaptation: early={spikes_early}, late={spikes_late}"
1518 );
1519 }
1520
1521 #[test]
1522 fn stellate_kv3_narrows_spikes() {
1523 let mut with_kv3 = StellateCell::new();
1525 let mut no_kv3 = StellateCell::new();
1526 no_kv3.g_kv3 = 0.0;
1527
1528 let mut spikes_kv3 = 0;
1529 let mut spikes_no = 0;
1530 for _ in 0..2000 {
1531 spikes_kv3 += with_kv3.step(15.0);
1532 spikes_no += no_kv3.step(15.0);
1533 }
1534 assert!(spikes_kv3 > 0, "With Kv3.1 must fire, got {spikes_kv3}");
1536 assert!(
1537 spikes_no >= 0,
1538 "No-Kv3.1 baseline must not panic, got {spikes_no}"
1539 );
1540 }
1541
1542 #[test]
1543 fn stellate_negative_input_no_crash() {
1544 let mut n = StellateCell::new();
1545 for _ in 0..10_000 {
1546 n.step(-100.0);
1547 }
1548 assert!(n.v.is_finite());
1549 assert!(n.v >= -100.0);
1550 }
1551
1552 #[test]
1553 fn stellate_nan_input_stays_finite() {
1554 let mut n = StellateCell::new();
1555 n.step(f64::NAN);
1556 assert!(n.v.is_finite());
1557 }
1558
1559 #[test]
1560 fn stellate_extreme_input_bounded() {
1561 let mut n = StellateCell::new();
1562 for _ in 0..1000 {
1563 n.step(1e6);
1564 }
1565 assert!(n.v.is_finite() && n.v <= 60.0);
1566 }
1567
1568 #[test]
1569 fn stellate_reset_clears_state() {
1570 let mut n = StellateCell::new();
1571 for _ in 0..1000 {
1572 n.step(20.0);
1573 }
1574 n.reset();
1575 assert_eq!(n.v, -65.0);
1576 assert_eq!(n.p, 0.0);
1577 }
1578
1579 #[test]
1580 fn stellate_gates_bounded() {
1581 let mut n = StellateCell::new();
1582 for _ in 0..10_000 {
1583 n.step(15.0);
1584 }
1585 assert!(n.h >= 0.0 && n.h <= 1.0);
1586 assert!(n.n >= 0.0 && n.n <= 1.0);
1587 assert!(n.p >= 0.0 && n.p <= 1.0);
1588 }
1589
1590 #[test]
1591 fn stellate_performance_1k_steps() {
1592 let start = std::time::Instant::now();
1593 let mut n = StellateCell::new();
1594 for _ in 0..1_000 {
1595 std::hint::black_box(n.step(10.0));
1596 }
1597 let elapsed = start.elapsed();
1598 assert!(
1599 elapsed.as_millis() < 200,
1600 "1k steps must complete in <200ms, took {}ms",
1601 elapsed.as_millis()
1602 );
1603 }
1604
1605 #[test]
1608 fn lugaro_fires_with_input() {
1609 let mut n = LugaroCell::new();
1610 let mut spikes = 0;
1611 for _ in 0..10_000 {
1612 spikes += n.step(5.0);
1613 }
1614 assert!(
1615 spikes > 10,
1616 "Lugaro must fire with excitatory input, got {spikes}"
1617 );
1618 }
1619
1620 #[test]
1621 fn lugaro_low_threshold() {
1622 let mut n = LugaroCell::new();
1624 let mut spikes = 0;
1625 for _ in 0..10_000 {
1626 spikes += n.step(4.0);
1627 }
1628 assert!(
1629 spikes > 10,
1630 "Lugaro should fire easily with moderate input, got {spikes}"
1631 );
1632 }
1633
1634 #[test]
1635 fn lugaro_adaptation() {
1636 let mut n = LugaroCell::new();
1637 let input = 10.0;
1638 let mut spikes_early = 0;
1639 for _ in 0..2000 {
1640 spikes_early += n.step(input);
1641 }
1642 let mut spikes_late = 0;
1643 for _ in 0..2000 {
1644 spikes_late += n.step(input);
1645 }
1646 assert!(
1647 spikes_early >= spikes_late,
1648 "Adaptation should slow firing: early={spikes_early}, late={spikes_late}"
1649 );
1650 }
1651
1652 #[test]
1653 fn lugaro_serotonin_increases_firing() {
1654 let mut no_5ht = LugaroCell::new();
1655 let mut with_5ht = LugaroCell::with_serotonin(1.0);
1656
1657 let input = 3.0;
1658 let mut spikes_no = 0;
1659 let mut spikes_5ht = 0;
1660 for _ in 0..10_000 {
1661 spikes_no += no_5ht.step(input);
1662 spikes_5ht += with_5ht.step(input);
1663 }
1664 assert!(
1665 spikes_5ht >= spikes_no,
1666 "5-HT must increase firing: 5HT={spikes_5ht} vs none={spikes_no}"
1667 );
1668 }
1669
1670 #[test]
1671 fn lugaro_negative_input_no_crash() {
1672 let mut n = LugaroCell::new();
1673 for _ in 0..10_000 {
1674 n.step(-100.0);
1675 }
1676 assert!(n.v.is_finite());
1677 assert!(n.v >= -100.0);
1678 }
1679
1680 #[test]
1681 fn lugaro_nan_input_stays_finite() {
1682 let mut n = LugaroCell::new();
1683 n.step(f64::NAN);
1684 assert!(n.v.is_finite());
1685 }
1686
1687 #[test]
1688 fn lugaro_extreme_input_bounded() {
1689 let mut n = LugaroCell::new();
1690 for _ in 0..1000 {
1691 n.step(1e6);
1692 }
1693 assert!(n.v.is_finite() && n.v <= 60.0);
1694 }
1695
1696 #[test]
1697 fn lugaro_reset_clears_state() {
1698 let mut n = LugaroCell::new();
1699 for _ in 0..1000 {
1700 n.step(10.0);
1701 }
1702 n.reset();
1703 assert_eq!(n.v, -55.0);
1704 assert_eq!(n.adapt, 0.0);
1705 assert_eq!(n.serotonin, 0.0);
1706 }
1707
1708 #[test]
1709 fn lugaro_adapt_increases_during_spiking() {
1710 let mut n = LugaroCell::new();
1711 let initial = n.adapt;
1712 for _ in 0..5000 {
1713 n.step(10.0);
1714 }
1715 assert!(
1716 n.adapt > initial,
1717 "Adaptation must increase during spiking, adapt={}",
1718 n.adapt
1719 );
1720 }
1721
1722 #[test]
1723 fn lugaro_performance_10k_steps() {
1724 let start = std::time::Instant::now();
1725 let mut n = LugaroCell::new();
1726 for _ in 0..10_000 {
1727 std::hint::black_box(n.step(5.0));
1728 }
1729 let elapsed = start.elapsed();
1730 assert!(
1731 elapsed.as_millis() < 50,
1732 "10k steps must complete in <50ms, took {}ms",
1733 elapsed.as_millis()
1734 );
1735 }
1736
1737 #[test]
1740 fn ubc_fires_with_input() {
1741 let mut n = UnipolarBrushCell::new();
1742 let mut spikes = 0;
1743 for _ in 0..10_000 {
1744 spikes += n.step(5.0);
1745 }
1746 assert!(
1747 spikes > 10,
1748 "UBC must fire with excitatory input, got {spikes}"
1749 );
1750 }
1751
1752 #[test]
1753 fn ubc_silent_without_input() {
1754 let mut n = UnipolarBrushCell::new();
1755 let mut spikes = 0;
1756 for _ in 0..10_000 {
1757 spikes += n.step(0.0);
1758 }
1759 assert_eq!(spikes, 0, "UBC must be silent without input");
1760 }
1761
1762 #[test]
1763 fn ubc_persistent_activity() {
1764 let mut n = UnipolarBrushCell::new();
1766 for _ in 0..2000 {
1768 n.step(10.0);
1769 }
1770 assert!(
1771 n.persistent > 0.0,
1772 "Persistent current must build during input"
1773 );
1774
1775 let persistent_before = n.persistent;
1777 for _ in 0..100 {
1778 n.step(0.0);
1779 }
1780 assert!(
1781 n.persistent > 0.0,
1782 "Persistent current must persist after input removal"
1783 );
1784 assert!(
1785 n.persistent < persistent_before,
1786 "Persistent current must decay"
1787 );
1788 }
1789
1790 #[test]
1791 fn ubc_persistent_spikes_after_input() {
1792 let mut n = UnipolarBrushCell::new();
1794 for _ in 0..5000 {
1796 n.step(10.0);
1797 }
1798 let post_spikes: i32 = (0..500).map(|_| n.step(0.0)).sum();
1800 assert!(post_spikes >= 0, "post_spikes must be non-negative");
1802 assert!(n.v.is_finite());
1803 }
1804
1805 #[test]
1806 fn ubc_negative_input_no_crash() {
1807 let mut n = UnipolarBrushCell::new();
1808 for _ in 0..10_000 {
1809 n.step(-100.0);
1810 }
1811 assert!(n.v.is_finite());
1812 }
1813
1814 #[test]
1815 fn ubc_nan_input_stays_finite() {
1816 let mut n = UnipolarBrushCell::new();
1817 n.step(f64::NAN);
1818 assert!(n.v.is_finite());
1819 }
1820
1821 #[test]
1822 fn ubc_extreme_input_bounded() {
1823 let mut n = UnipolarBrushCell::new();
1824 for _ in 0..1000 {
1825 n.step(1e6);
1826 }
1827 assert!(n.v.is_finite() && n.v <= 60.0);
1828 }
1829
1830 #[test]
1831 fn ubc_reset_clears_state() {
1832 let mut n = UnipolarBrushCell::new();
1833 for _ in 0..1000 {
1834 n.step(10.0);
1835 }
1836 n.reset();
1837 assert_eq!(n.v, -65.0);
1838 assert_eq!(n.persistent, 0.0);
1839 }
1840
1841 #[test]
1842 fn ubc_performance_10k_steps() {
1843 let start = std::time::Instant::now();
1844 let mut n = UnipolarBrushCell::new();
1845 for _ in 0..10_000 {
1846 std::hint::black_box(n.step(5.0));
1847 }
1848 let elapsed = start.elapsed();
1849 assert!(elapsed.as_millis() < 50, "10k steps must complete in <50ms");
1850 }
1851
1852 #[test]
1855 fn dcn_fires_with_input() {
1856 let mut n = DCNNeuron::new();
1857 let mut spikes = 0;
1858 for _ in 0..2_000 {
1859 spikes += n.step(5.0);
1860 }
1861 assert!(
1862 spikes > 3,
1863 "DCN must fire with excitatory input, got {spikes}"
1864 );
1865 }
1866
1867 #[test]
1868 fn dcn_spontaneous_activity() {
1869 let mut n = DCNNeuron::new();
1872 let mut spikes = 0;
1873 for _ in 0..20_000 {
1874 spikes += n.step(0.0);
1875 }
1876 let mut no_nap = DCNNeuron::new();
1879 no_nap.g_nap = 0.0;
1880 let mut spikes_no = 0;
1881 for _ in 0..20_000 {
1882 spikes_no += no_nap.step(0.0);
1883 }
1884 assert!(
1885 spikes >= spikes_no,
1886 "INaP should contribute to spontaneous firing: with={spikes}, without={spikes_no}"
1887 );
1888 }
1889
1890 #[test]
1891 fn dcn_rebound_burst() {
1892 let mut n = DCNNeuron::new();
1894 for _ in 0..2000 {
1896 n.step(-5.0);
1897 }
1898 assert!(
1899 n.s > 0.5,
1900 "T-type must de-inactivate during hyperpolarisation, s={}",
1901 n.s
1902 );
1903
1904 let mut spikes = 0;
1906 for _ in 0..200 {
1907 spikes += n.step(3.0);
1908 }
1909 let mut n2 = DCNNeuron::new();
1911 n2.s = 0.05; let mut spikes2 = 0;
1913 for _ in 0..200 {
1914 spikes2 += n2.step(3.0);
1915 }
1916 assert!(
1917 spikes >= spikes2,
1918 "De-inactivated T-type should facilitate rebound: rebound={spikes} vs inact={spikes2}"
1919 );
1920 }
1921
1922 #[test]
1923 fn dcn_ih_depolarises() {
1924 let mut with_ih = DCNNeuron::new();
1926 with_ih.v = -80.0;
1927 let mut no_ih = DCNNeuron::new();
1928 no_ih.v = -80.0;
1929 no_ih.g_h = 0.0;
1930
1931 for _ in 0..1000 {
1932 with_ih.step(0.0);
1933 no_ih.step(0.0);
1934 }
1935 assert!(
1936 with_ih.v > no_ih.v,
1937 "Ih should depolarise from hyperpolarised state: Ih={:.1} vs no_Ih={:.1}",
1938 with_ih.v,
1939 no_ih.v
1940 );
1941 }
1942
1943 #[test]
1944 fn dcn_negative_input_no_crash() {
1945 let mut n = DCNNeuron::new();
1946 for _ in 0..10_000 {
1947 n.step(-100.0);
1948 }
1949 assert!(n.v.is_finite());
1950 assert!(n.v >= -100.0);
1951 }
1952
1953 #[test]
1954 fn dcn_nan_input_stays_finite() {
1955 let mut n = DCNNeuron::new();
1956 n.step(f64::NAN);
1957 assert!(n.v.is_finite());
1958 }
1959
1960 #[test]
1961 fn dcn_extreme_input_bounded() {
1962 let mut n = DCNNeuron::new();
1963 for _ in 0..1000 {
1964 n.step(1e6);
1965 }
1966 assert!(n.v.is_finite() && n.v <= 60.0);
1967 }
1968
1969 #[test]
1970 fn dcn_reset_clears_state() {
1971 let mut n = DCNNeuron::new();
1972 for _ in 0..1000 {
1973 n.step(10.0);
1974 }
1975 n.reset();
1976 assert_eq!(n.v, -60.0);
1977 assert_eq!(n.s, 0.8);
1978 assert_eq!(n.r, 0.1);
1979 }
1980
1981 #[test]
1982 fn dcn_gates_bounded() {
1983 let mut n = DCNNeuron::new();
1984 for _ in 0..10_000 {
1985 n.step(10.0);
1986 }
1987 for (name, val) in [("h", n.h), ("n", n.n), ("p", n.p), ("s", n.s), ("r", n.r)] {
1988 assert!((0.0..=1.0).contains(&val), "{name} out of bounds: {val}");
1989 }
1990 assert!(n.ca >= 0.0, "Ca²⁺ must be non-negative: {}", n.ca);
1991 }
1992
1993 #[test]
1994 fn dcn_nap_increases_excitability() {
1995 let mut with_nap = DCNNeuron::new();
1997 let mut no_nap = DCNNeuron::new();
1998 no_nap.g_nap = 0.0;
1999 let mut spikes_with = 0;
2000 let mut spikes_no = 0;
2001 for _ in 0..5_000 {
2002 spikes_with += with_nap.step(3.0);
2003 spikes_no += no_nap.step(3.0);
2004 }
2005 assert!(
2006 spikes_with >= spikes_no,
2007 "INaP should increase excitability: with={spikes_with}, without={spikes_no}"
2008 );
2009 }
2010
2011 #[test]
2012 fn dcn_ahp_limits_rate() {
2013 let mut with_ahp = DCNNeuron::new();
2015 let mut no_ahp = DCNNeuron::new();
2016 no_ahp.g_ahp = 0.0;
2017 let mut spikes_with = 0;
2018 let mut spikes_no = 0;
2019 for _ in 0..5_000 {
2020 spikes_with += with_ahp.step(8.0);
2021 spikes_no += no_ahp.step(8.0);
2022 }
2023 assert!(
2024 spikes_no >= spikes_with,
2025 "AHP removal should increase firing: with={spikes_with}, without={spikes_no}"
2026 );
2027 }
2028
2029 #[test]
2030 fn dcn_ca_rises_during_spiking() {
2031 let mut n = DCNNeuron::new();
2032 let ca_init = n.ca;
2033 for _ in 0..5_000 {
2034 n.step(10.0);
2035 }
2036 assert!(
2037 n.ca > ca_init,
2038 "Ca²⁺ must rise during spiking: init={ca_init}, now={}",
2039 n.ca
2040 );
2041 }
2042
2043 #[test]
2044 fn dcn_has_seven_currents() {
2045 let n = DCNNeuron::new();
2047 assert!(n.g_na > 0.0, "Na_t missing");
2048 assert!(n.g_nap > 0.0, "Na_p missing");
2049 assert!(n.g_k > 0.0, "K_dr missing");
2050 assert!(n.g_t > 0.0, "Ca_T missing");
2051 assert!(n.g_ahp > 0.0, "AHP missing");
2052 assert!(n.g_h > 0.0, "Ih missing");
2053 assert!(n.g_l > 0.0, "Leak missing");
2054 }
2055
2056 #[test]
2057 fn dcn_performance_1k_steps() {
2058 let start = std::time::Instant::now();
2059 let mut n = DCNNeuron::new();
2060 for _ in 0..1_000 {
2061 std::hint::black_box(n.step(5.0));
2062 }
2063 let elapsed = start.elapsed();
2064 assert!(
2065 elapsed.as_millis() < 200,
2066 "1k steps must complete in <200ms"
2067 );
2068 }
2069}