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 let z = -(v - vh) / k;
121 if z > 60.0 {
122 0.0
123 } else if z < -60.0 {
124 1.0
125 } else {
126 1.0 / (1.0 + z.exp())
127 }
128 }
129
130 fn is_valid(&self) -> bool {
131 [
132 self.v,
133 self.m,
134 self.h,
135 self.n,
136 self.a,
137 self.b,
138 self.m_t,
139 self.s,
140 self.ca,
141 self.r,
142 self.c_m,
143 self.g_na,
144 self.g_kdr,
145 self.g_ka,
146 self.g_t,
147 self.g_kca,
148 self.g_h,
149 self.g_l,
150 self.g_tonic,
151 self.e_na,
152 self.e_k,
153 self.e_ca,
154 self.e_h,
155 self.e_l,
156 self.e_gaba,
157 self.tau_ca,
158 self.kd_kca,
159 self.dt,
160 self.gain,
161 ]
162 .iter()
163 .all(|value| value.is_finite())
164 && [
165 self.m, self.h, self.n, self.a, self.b, self.m_t, self.s, self.r,
166 ]
167 .iter()
168 .all(|gate| (0.0..=1.0).contains(gate))
169 && (-100.0..=60.0).contains(&self.v)
170 && self.ca >= 0.0
171 && [
172 self.g_na,
173 self.g_kdr,
174 self.g_ka,
175 self.g_t,
176 self.g_kca,
177 self.g_h,
178 self.g_l,
179 self.g_tonic,
180 ]
181 .iter()
182 .all(|conductance| *conductance >= 0.0)
183 && self.c_m > 0.0
184 && self.tau_ca > 0.0
185 && self.kd_kca > 0.0
186 && self.dt > 0.0
187 && self.sub_steps > 0
188 && self.gain >= 0.0
189 }
190
191 pub fn step(&mut self, current: f64) -> i32 {
192 if !self.is_valid() || !current.is_finite() {
193 return 0;
194 }
195
196 let input = self.gain * current;
197 let dt_sub = self.dt / self.sub_steps as f64;
198 let v_prev = self.v;
199 let mut v = self.v;
200 let mut m = self.m;
201 let mut h = self.h;
202 let mut n = self.n;
203 let mut a = self.a;
204 let mut b = self.b;
205 let mut m_t = self.m_t;
206 let mut s_gate = self.s;
207 let mut ca = self.ca;
208 let mut r = self.r;
209
210 for _ in 0..self.sub_steps {
211 let m_inf = Self::boltz(v, -30.0, 7.0);
213 let tau_m = 0.1 + 0.3 / (1.0 + ((v + 30.0) / 10.0).powi(2)).max(0.01);
214 m = granule_exact_relax(m, m_inf, tau_m, dt_sub).clamp(0.0, 1.0);
215
216 let h_inf = Self::boltz(v, -52.0, -6.0);
218 let tau_h = 0.5 + 5.0 / (1.0 + ((v + 50.0) / 15.0).powi(2)).max(0.01);
219 h = granule_exact_relax(h, h_inf, tau_h, dt_sub).clamp(0.0, 1.0);
220
221 let n_inf = Self::boltz(v, -35.0, 8.0);
223 let tau_n = 1.0 + 5.0 / (1.0 + ((v + 35.0) / 15.0).powi(2)).max(0.01);
224 n = granule_exact_relax(n, n_inf, tau_n, dt_sub).clamp(0.0, 1.0);
225
226 let a_inf = Self::boltz(v, -50.0, 20.0);
228 let tau_a = 2.0;
229 a = granule_exact_relax(a, a_inf, tau_a, dt_sub).clamp(0.0, 1.0);
230
231 let b_inf = Self::boltz(v, -70.0, -6.0);
233 let tau_b = 50.0;
234 b = granule_exact_relax(b, b_inf, tau_b, dt_sub).clamp(0.0, 1.0);
235
236 let mt_inf = Self::boltz(v, -52.0, 5.0);
238 let tau_mt = 1.0;
239 m_t = granule_exact_relax(m_t, mt_inf, tau_mt, dt_sub).clamp(0.0, 1.0);
240
241 let s_inf = Self::boltz(v, -60.0, -6.5);
243 let tau_s = 20.0 + 50.0 / (1.0 + ((v + 65.0) / 10.0).powi(2)).max(0.01);
244 s_gate = granule_exact_relax(s_gate, s_inf, tau_s, dt_sub).clamp(0.0, 1.0);
245
246 let r_inf = Self::boltz(v, -80.0, -10.0);
248 let tau_r = 50.0 + 200.0 / (1.0 + ((v + 80.0) / 20.0).powi(2)).max(0.01);
249 r = granule_exact_relax(r, r_inf, tau_r, dt_sub).clamp(0.0, 1.0);
250
251 let i_ca_t = self.g_t * m_t * m_t * s_gate * (v - self.e_ca);
253 let ca_entry = if i_ca_t < 0.0 { -i_ca_t * 0.001 } else { 0.0 }; ca = granule_exact_relax(ca, ca_entry * self.tau_ca, self.tau_ca, dt_sub).max(0.0);
255
256 let kca_inf = ca * ca / (ca * ca + self.kd_kca * self.kd_kca);
258
259 let g_na_eff = self.g_na * m.powi(3) * h;
261 let g_kdr_eff = self.g_kdr * n.powi(4);
262 let g_ka_eff = self.g_ka * a.powi(3) * b;
263 let g_t_eff = self.g_t * m_t * m_t * s_gate;
264 let g_kca_eff = self.g_kca * kca_inf;
265 let g_h_eff = self.g_h * r;
266 v = granule_exact_voltage_step(
267 v,
268 input,
269 self.c_m,
270 dt_sub,
271 &[
272 (g_na_eff, self.e_na),
273 (g_kdr_eff, self.e_k),
274 (g_ka_eff, self.e_k),
275 (g_t_eff, self.e_ca),
276 (g_kca_eff, self.e_k),
277 (g_h_eff, self.e_h),
278 (self.g_l, self.e_l),
279 (self.g_tonic, self.e_gaba),
280 ],
281 )
282 .clamp(-100.0, 60.0);
283
284 if ![v, m, h, n, a, b, m_t, s_gate, ca, r]
285 .iter()
286 .all(|value| value.is_finite())
287 {
288 return 0;
289 }
290 }
291
292 self.v = v;
293 self.m = m;
294 self.h = h;
295 self.n = n;
296 self.a = a;
297 self.b = b;
298 self.m_t = m_t;
299 self.s = s_gate;
300 self.ca = ca;
301 self.r = r;
302
303 if self.v >= 0.0 && v_prev < 0.0 {
305 1
306 } else {
307 0
308 }
309 }
310
311 pub fn reset(&mut self) {
312 *self = Self::new();
313 }
314}
315
316fn granule_exact_relax(value: f64, target: f64, tau: f64, dt: f64) -> f64 {
317 target + (value - target) * (-dt / tau).exp()
318}
319
320fn granule_exact_voltage_step(
321 v: f64,
322 input_current: f64,
323 c_m: f64,
324 dt: f64,
325 conductances: &[(f64, f64)],
326) -> f64 {
327 let g_total: f64 = conductances.iter().map(|(g, _)| *g).sum();
328 if g_total <= 0.0 {
329 return v + dt * input_current / c_m;
330 }
331 let reversal_drive: f64 = conductances.iter().map(|(g, e_rev)| g * e_rev).sum();
332 let v_inf = (input_current + reversal_drive) / g_total;
333 v_inf + (v - v_inf) * (-dt * g_total / c_m).exp()
334}
335
336#[derive(Clone, Debug)]
363pub struct GolgiCell {
364 pub v: f64,
365 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,
379 pub g_na_p: f64,
380 pub g_kdr: f64,
381 pub g_ka: f64,
382 pub g_km: f64,
383 pub g_cat: f64,
384 pub g_can: f64,
385 pub g_bk: f64,
386 pub g_sk: f64,
387 pub g_h: f64,
388 pub g_l: f64,
389 pub e_na: f64,
391 pub e_k: f64,
392 pub e_ca: f64,
393 pub e_h: f64,
394 pub e_l: f64,
395 pub c_m: f64,
396 pub tau_ca: f64,
397 pub kd_bk: f64,
398 pub kd_sk: f64,
399 pub dt: f64,
400 pub sub_steps: usize,
401 pub gain: f64,
402}
403
404impl Default for GolgiCell {
405 fn default() -> Self {
406 Self::new()
407 }
408}
409
410impl GolgiCell {
411 pub fn new() -> Self {
412 Self {
413 v: -60.0,
414 m: 0.02,
415 h: 0.85,
416 p_na: 0.01,
417 n: 0.05,
418 a: 0.1,
419 b: 0.8,
420 w: 0.01,
421 m_t: 0.01,
422 s: 0.9,
423 c_n: 0.01,
424 r: 0.1,
425 ca: 0.05,
426 g_na_t: 48.0, g_na_p: 0.2, g_kdr: 16.0,
429 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,
437 e_na: 55.0,
438 e_k: -90.0,
439 e_ca: 120.0,
440 e_h: -40.0,
441 e_l: -55.0, c_m: 1.0,
443 tau_ca: 200.0,
444 kd_bk: 1.0,
445 kd_sk: 0.5,
446 dt: 0.5,
447 sub_steps: 10,
448 gain: 1.0,
449 }
450 }
451
452 #[inline]
453 fn boltz(v: f64, vh: f64, k: f64) -> f64 {
454 let x = (v - vh) / k;
455 if x >= 0.0 {
456 1.0 / (1.0 + (-x).exp())
457 } else {
458 let ex = x.exp();
459 ex / (1.0 + ex)
460 }
461 }
462
463 #[inline]
464 fn voltage_valid(value: f64) -> bool {
465 value.is_finite() && (-100.0..=60.0).contains(&value)
466 }
467
468 #[inline]
469 fn probability(value: f64) -> bool {
470 value.is_finite() && (0.0..=1.0).contains(&value)
471 }
472
473 #[inline]
474 fn gate_alpha_beta(previous: f64, alpha: f64, beta: f64, phi: f64, dt: f64) -> Option<f64> {
475 let total = phi * (alpha + beta);
476 if !previous.is_finite()
477 || !alpha.is_finite()
478 || !beta.is_finite()
479 || !total.is_finite()
480 || !dt.is_finite()
481 || total <= 0.0
482 {
483 return None;
484 }
485 let steady = alpha / (alpha + beta);
486 Some((steady + (previous - steady) * (-total * dt).exp()).clamp(0.0, 1.0))
487 }
488
489 #[inline]
490 fn gate_inf(previous: f64, steady: f64, tau: f64, dt: f64) -> Option<f64> {
491 if !previous.is_finite()
492 || !steady.is_finite()
493 || !tau.is_finite()
494 || !dt.is_finite()
495 || tau <= 0.0
496 {
497 return None;
498 }
499 Some((steady + (previous - steady) * (-dt / tau).exp()).clamp(0.0, 1.0))
500 }
501
502 #[inline]
503 fn calcium_exact(previous: f64, entry: f64, tau: f64, dt: f64) -> Option<f64> {
504 if !previous.is_finite()
505 || !entry.is_finite()
506 || !tau.is_finite()
507 || !dt.is_finite()
508 || tau <= 0.0
509 || previous < 0.0
510 {
511 return None;
512 }
513 let steady = entry * tau;
514 let value = steady + (previous - steady) * (-dt / tau).exp();
515 value.is_finite().then_some(value.max(0.0))
516 }
517
518 fn valid_state(&self) -> bool {
519 Self::voltage_valid(self.v)
520 && [
521 self.m, self.h, self.p_na, self.n, self.a, self.b, self.w, self.m_t, self.s,
522 self.c_n, self.r,
523 ]
524 .into_iter()
525 .all(Self::probability)
526 && [
527 self.g_na_t,
528 self.g_na_p,
529 self.g_kdr,
530 self.g_ka,
531 self.g_km,
532 self.g_cat,
533 self.g_can,
534 self.g_bk,
535 self.g_sk,
536 self.g_h,
537 self.g_l,
538 ]
539 .into_iter()
540 .all(|g| g.is_finite() && g >= 0.0)
541 && self.ca.is_finite()
542 && self.ca >= 0.0
543 && self.e_na.is_finite()
544 && self.e_k.is_finite()
545 && self.e_ca.is_finite()
546 && self.e_h.is_finite()
547 && self.e_l.is_finite()
548 && self.c_m.is_finite()
549 && self.tau_ca.is_finite()
550 && self.kd_bk.is_finite()
551 && self.kd_sk.is_finite()
552 && self.dt.is_finite()
553 && self.gain.is_finite()
554 && self.c_m > 0.0
555 && self.tau_ca > 0.0
556 && self.kd_bk > 0.0
557 && self.kd_sk > 0.0
558 && self.dt > 0.0
559 && self.sub_steps > 0
560 && self.gain >= 0.0
561 }
562
563 pub fn step(&mut self, current: f64) -> i32 {
564 if !current.is_finite() || !self.valid_state() {
565 return 0;
566 }
567
568 let input = self.gain * current;
569 let dt_sub = self.dt / self.sub_steps as f64;
570 let v_prev = self.v;
571 let mut next = self.clone();
572
573 for _ in 0..self.sub_steps {
574 let v = next.v;
575
576 let alpha_m = safe_rate(0.1, 35.0, v, 10.0, 1.0);
578 let beta_m = 4.0 * (-(v + 60.0) / 18.0).exp();
579 let alpha_h = 0.07 * (-(v + 58.0) / 20.0).exp();
580 let beta_h = 1.0 / (1.0 + (-(v + 28.0) / 10.0).exp());
581 let Some(m) = Self::gate_alpha_beta(next.m, alpha_m, beta_m, 5.0, dt_sub) else {
582 return 0;
583 };
584 let Some(h) = Self::gate_alpha_beta(next.h, alpha_h, beta_h, 5.0, dt_sub) else {
585 return 0;
586 };
587
588 let pna_inf = Self::boltz(v, -48.0, 5.0);
590 let tau_pna = 5.0 + 20.0 / (1.0 + ((v + 48.0) / 10.0).powi(2)).max(0.01);
591 let Some(p_na) = Self::gate_inf(next.p_na, pna_inf, tau_pna, dt_sub) else {
592 return 0;
593 };
594
595 let alpha_n = safe_rate(0.01, 34.0, v, 10.0, 0.1);
597 let beta_n = 0.125 * (-(v + 44.0) / 80.0).exp();
598 let Some(n) = Self::gate_alpha_beta(next.n, alpha_n, beta_n, 5.0, dt_sub) else {
599 return 0;
600 };
601
602 let a_inf = Self::boltz(v, -27.0, 16.0);
604 let b_inf = Self::boltz(v, -80.0, -6.0);
605 let Some(a) = Self::gate_inf(next.a, a_inf, 2.0, dt_sub) else {
606 return 0;
607 };
608 let Some(b) = Self::gate_inf(next.b, b_inf, 15.0, dt_sub) else {
609 return 0;
610 };
611
612 let w_inf = Self::boltz(v, -35.0, 10.0);
614 let tau_w = 100.0 / (3.3 * ((v + 35.0) / 20.0).exp() + (-(v + 35.0) / 20.0).exp());
615 let Some(w) = Self::gate_inf(next.w, w_inf, tau_w, dt_sub) else {
616 return 0;
617 };
618
619 let mt_inf = Self::boltz(v, -52.0, 5.0);
621 let s_inf = Self::boltz(v, -60.0, -6.5);
622 let tau_s = 20.0 + 50.0 / (1.0 + ((v + 65.0) / 10.0).powi(2)).max(0.01);
623 let Some(m_t) = Self::gate_inf(next.m_t, mt_inf, 1.0, dt_sub) else {
624 return 0;
625 };
626 let Some(s) = Self::gate_inf(next.s, s_inf, tau_s, dt_sub) else {
627 return 0;
628 };
629
630 let cn_inf = Self::boltz(v, -20.0, 5.0);
632 let tau_cn = 2.0 + 10.0 / (1.0 + ((v + 20.0) / 10.0).powi(2)).max(0.01);
633 let Some(c_n) = Self::gate_inf(next.c_n, cn_inf, tau_cn, dt_sub) else {
634 return 0;
635 };
636
637 let r_inf = Self::boltz(v, -80.0, -10.0);
639 let tau_r = 50.0 + 200.0 / (1.0 + ((v + 80.0) / 20.0).powi(2)).max(0.01);
640 let Some(r) = Self::gate_inf(next.r, r_inf, tau_r, dt_sub) else {
641 return 0;
642 };
643
644 let g_cat = self.g_cat * m_t.powi(2) * s;
646 let g_can = self.g_can * c_n.powi(2);
647 let i_cat = g_cat * (v - self.e_ca);
648 let i_can = g_can * (v - self.e_ca);
649 let ca_entry = if i_cat + i_can < 0.0 {
650 -(i_cat + i_can) * 0.001
651 } else {
652 0.0
653 };
654 let Some(ca) = Self::calcium_exact(next.ca, ca_entry, self.tau_ca, dt_sub) else {
655 return 0;
656 };
657
658 let ca2 = ca * ca;
661 let kd2 = self.kd_bk * self.kd_bk;
662 let bk_v = Self::boltz(v, 100.0 - 120.0 * ca2 / (ca2 + kd2), 15.0);
663 let sk_inf = ca2 / (ca2 + self.kd_sk.powi(2));
665
666 let g_na = self.g_na_t * m.powi(3) * h + self.g_na_p * p_na;
668 let g_k = self.g_kdr * n.powi(4)
669 + self.g_ka * a.powi(3) * b
670 + self.g_km * w
671 + self.g_bk * bk_v
672 + self.g_sk * sk_inf;
673 let g_ca = g_cat + g_can;
674 let g_h = self.g_h * r;
675 let g_total = g_na + g_k + g_ca + g_h + self.g_l;
676 if !g_total.is_finite() || g_total <= 0.0 {
677 return 0;
678 }
679 let steady_v = (input
680 + g_na * self.e_na
681 + g_k * self.e_k
682 + g_ca * self.e_ca
683 + g_h * self.e_h
684 + self.g_l * self.e_l)
685 / g_total;
686 let v_next = steady_v + (v - steady_v) * (-(g_total / self.c_m) * dt_sub).exp();
687 if !Self::voltage_valid(v_next) || !ca.is_finite() || ca < 0.0 {
688 return 0;
689 }
690
691 next.v = v_next;
692 next.m = m;
693 next.h = h;
694 next.p_na = p_na;
695 next.n = n;
696 next.a = a;
697 next.b = b;
698 next.w = w;
699 next.m_t = m_t;
700 next.s = s;
701 next.c_n = c_n;
702 next.r = r;
703 next.ca = ca;
704 }
705
706 *self = next;
707
708 if self.v >= 0.0 && v_prev < 0.0 {
710 1
711 } else {
712 0
713 }
714 }
715
716 pub fn reset(&mut self) {
717 *self = Self::new();
718 }
719}
720
721#[derive(Clone, Debug)]
738pub struct StellateCell {
739 pub v: f64,
740 pub h: f64, pub n: f64, pub p: f64, pub g_na: f64,
745 pub g_k: f64,
746 pub g_kv3: f64,
747 pub g_l: f64,
748 pub e_na: f64,
750 pub e_k: f64,
751 pub e_l: f64,
752 pub c_m: f64,
753 pub phi: f64,
754 pub dt: f64,
755 pub v_threshold: f64,
756 pub gain: f64,
757}
758
759impl Default for StellateCell {
760 fn default() -> Self {
761 Self::new()
762 }
763}
764
765impl StellateCell {
766 pub fn new() -> Self {
767 Self {
768 v: -65.0,
769 h: 0.6,
770 n: 0.32,
771 p: 0.0,
772 g_na: 35.0,
773 g_k: 9.0,
774 g_kv3: 3.0, g_l: 0.1,
776 e_na: 55.0,
777 e_k: -90.0,
778 e_l: -65.0,
779 c_m: 0.5, phi: 5.0,
781 dt: 0.5,
782 v_threshold: -20.0,
783 gain: 1.0,
784 }
785 }
786
787 #[inline]
788 fn safe_exp(value: f64) -> f64 {
789 value.clamp(-60.0, 60.0).exp()
790 }
791
792 #[inline]
793 fn boltz(v: f64, vh: f64, k: f64) -> f64 {
794 let z = -(v - vh) / k;
795 if z > 60.0 {
796 0.0
797 } else if z < -60.0 {
798 1.0
799 } else {
800 1.0 / (1.0 + z.exp())
801 }
802 }
803
804 #[inline]
805 fn exact_relax(value: f64, target: f64, tau: f64, dt: f64) -> f64 {
806 if tau <= f64::EPSILON {
807 target.clamp(0.0, 1.0)
808 } else {
809 (target + (value - target) * (-dt / tau).exp()).clamp(0.0, 1.0)
810 }
811 }
812
813 #[inline]
814 fn exact_hh_gate(value: f64, alpha: f64, beta: f64, phi: f64, dt: f64) -> f64 {
815 let total = alpha + beta;
816 if total <= f64::EPSILON {
817 value.clamp(0.0, 1.0)
818 } else {
819 let steady = alpha / total;
820 (steady + (value - steady) * (-phi * total * dt).exp()).clamp(0.0, 1.0)
821 }
822 }
823
824 #[inline]
825 fn exact_voltage_step(
826 v: f64,
827 c_m: f64,
828 input: f64,
829 conductances: [(f64, f64); 4],
830 dt: f64,
831 ) -> f64 {
832 let g_total = conductances.iter().map(|(g, _)| *g).sum::<f64>();
833 let drive = input
834 + conductances
835 .iter()
836 .map(|(g, reversal)| g * reversal)
837 .sum::<f64>();
838 if g_total <= f64::EPSILON {
839 v + dt * drive / c_m
840 } else {
841 let v_inf = drive / g_total;
842 let tau = c_m / g_total;
843 v_inf + (v - v_inf) * (-dt / tau).exp()
844 }
845 }
846
847 fn is_valid(&self) -> bool {
848 [
849 self.v,
850 self.h,
851 self.n,
852 self.p,
853 self.g_na,
854 self.g_k,
855 self.g_kv3,
856 self.g_l,
857 self.e_na,
858 self.e_k,
859 self.e_l,
860 self.c_m,
861 self.phi,
862 self.dt,
863 self.v_threshold,
864 self.gain,
865 ]
866 .iter()
867 .all(|value| value.is_finite())
868 && (-100.0..=60.0).contains(&self.v)
869 && [self.h, self.n, self.p]
870 .iter()
871 .all(|gate| (0.0..=1.0).contains(gate))
872 && [self.g_na, self.g_k, self.g_kv3, self.g_l]
873 .iter()
874 .all(|conductance| *conductance >= 0.0)
875 && self.c_m > 0.0
876 && self.phi > 0.0
877 && self.dt > 0.0
878 && self.gain >= 0.0
879 }
880
881 pub fn step(&mut self, current: f64) -> i32 {
882 if !self.is_valid() || !current.is_finite() {
883 return 0;
884 }
885
886 let input = self.gain * current;
887 let sub_steps = 50;
888 let sub_dt = self.dt / sub_steps as f64;
889 let mut fired = 0i32;
890 let mut v = self.v;
891 let mut h = self.h;
892 let mut n = self.n;
893 let mut p = self.p;
894
895 for _ in 0..sub_steps {
896 let alpha_m = safe_rate(0.1, 35.0, v, 10.0, 1.0);
898 let beta_m = 4.0 * Self::safe_exp(-(v + 60.0) / 18.0);
899 let m_inf = alpha_m / (alpha_m + beta_m);
900
901 let alpha_h = 0.07 * Self::safe_exp(-(v + 58.0) / 20.0);
902 let beta_h = Self::boltz(v, -28.0, 10.0);
903
904 let alpha_n = safe_rate(0.01, 34.0, v, 10.0, 0.1);
905 let beta_n = 0.125 * Self::safe_exp(-(v + 44.0) / 80.0);
906
907 let p_inf = Self::boltz(v, -10.0, 10.0);
909 let tau_p = 1.0 + 4.0 / (1.0 + Self::safe_exp((v + 20.0) / 15.0));
910
911 h = Self::exact_hh_gate(h, alpha_h, beta_h, self.phi, sub_dt);
913 n = Self::exact_hh_gate(n, alpha_n, beta_n, self.phi, sub_dt);
914 p = Self::exact_relax(p, p_inf, tau_p, sub_dt);
915
916 let g_na = self.g_na * m_inf.powi(3) * h;
918 let g_k = self.g_k * n.powi(4);
919 let g_kv3 = self.g_kv3 * p.powi(2);
920 let g_l = self.g_l;
921
922 v = Self::exact_voltage_step(
923 v,
924 self.c_m,
925 input,
926 [
927 (g_na, self.e_na),
928 (g_k, self.e_k),
929 (g_kv3, self.e_k),
930 (g_l, self.e_l),
931 ],
932 sub_dt,
933 )
934 .clamp(-100.0, 60.0);
935 if ![v, h, n, p].iter().all(|value| value.is_finite()) {
936 return 0;
937 }
938
939 if v >= self.v_threshold {
940 fired = 1;
941 v = -65.0;
942 }
943 }
944
945 self.v = v;
946 self.h = h;
947 self.n = n;
948 self.p = p;
949
950 fired
951 }
952
953 pub fn reset(&mut self) {
954 *self = Self::new();
955 }
956}
957
958#[derive(Clone, Debug)]
974pub struct LugaroCell {
975 pub v: f64,
976 pub adapt: f64, pub v_rest: f64,
978 pub v_reset: f64,
979 pub v_threshold: f64,
980 pub tau_m: f64,
981 pub tau_adapt: f64,
982 pub a_adapt: f64, pub gain: f64,
984 pub serotonin: f64, pub dt: f64,
986}
987
988impl Default for LugaroCell {
989 fn default() -> Self {
990 Self::new()
991 }
992}
993
994impl LugaroCell {
995 pub fn new() -> Self {
996 Self {
997 v: -55.0,
998 adapt: 0.0,
999 v_rest: -55.0, v_reset: -65.0,
1001 v_threshold: -48.0,
1002 tau_m: 10.0,
1003 tau_adapt: 150.0,
1004 a_adapt: 0.05,
1005 gain: 2.0,
1006 serotonin: 0.0, dt: 0.5,
1008 }
1009 }
1010
1011 pub fn with_serotonin(serotonin_level: f64) -> Self {
1013 let mut n = Self::new();
1014 n.serotonin = serotonin_level.clamp(0.0, 1.0);
1015 n
1016 }
1017
1018 fn is_valid(&self) -> bool {
1019 [
1020 self.v,
1021 self.adapt,
1022 self.v_rest,
1023 self.v_reset,
1024 self.v_threshold,
1025 self.tau_m,
1026 self.tau_adapt,
1027 self.a_adapt,
1028 self.gain,
1029 self.serotonin,
1030 self.dt,
1031 ]
1032 .iter()
1033 .all(|value| value.is_finite())
1034 && self.tau_m > 0.0
1035 && self.tau_adapt > 0.0
1036 && self.dt > 0.0
1037 && self.a_adapt >= 0.0
1038 && self.gain >= 0.0
1039 && (-100.0..=60.0).contains(&self.v)
1040 && (0.0..=1.0).contains(&self.serotonin)
1041 && self.adapt >= 0.0
1042 && self.v_threshold > self.v_reset
1043 && self.v_threshold > self.v_rest
1044 }
1045
1046 pub fn step(&mut self, current: f64) -> i32 {
1047 if !self.is_valid() || !current.is_finite() {
1048 return 0;
1049 }
1050
1051 let effective_gain = self.gain * (1.0 + 0.5 * self.serotonin);
1053 let input = effective_gain * current;
1054
1055 let v_inf = self.v_rest + input - self.adapt;
1057 let v_next = v_inf + (self.v - v_inf) * (-self.dt / self.tau_m).exp();
1058
1059 let adapt_inf = (self.a_adapt * (v_next - self.v_rest).max(0.0)).max(0.0);
1061 let adapt_next =
1062 (adapt_inf + (self.adapt - adapt_inf) * (-self.dt / self.tau_adapt).exp()).max(0.0);
1063 if !v_next.is_finite() || !adapt_next.is_finite() {
1064 return 0;
1065 }
1066
1067 if v_next >= self.v_threshold {
1069 self.v = self.v_reset;
1070 self.adapt = adapt_next + 1.0; return 1;
1072 }
1073
1074 self.v = v_next.clamp(-100.0, 60.0);
1076 self.adapt = adapt_next;
1077
1078 0
1079 }
1080
1081 pub fn reset(&mut self) {
1082 *self = Self::new();
1083 }
1084}
1085
1086#[derive(Clone, Debug)]
1103pub struct UnipolarBrushCell {
1104 pub v: f64,
1105 pub persistent: f64, pub v_rest: f64,
1107 pub v_reset: f64,
1108 pub v_threshold: f64,
1109 pub tau_m: f64,
1110 pub tau_persistent: f64, pub persistent_gain: f64, pub gain: f64,
1113 pub dt: f64,
1114}
1115
1116impl Default for UnipolarBrushCell {
1117 fn default() -> Self {
1118 Self::new()
1119 }
1120}
1121
1122impl UnipolarBrushCell {
1123 pub fn new() -> Self {
1124 Self {
1125 v: -65.0,
1126 persistent: 0.0,
1127 v_rest: -65.0,
1128 v_reset: -70.0,
1129 v_threshold: -50.0,
1130 tau_m: 8.0,
1131 tau_persistent: 200.0,
1132 persistent_gain: 0.5,
1133 gain: 2.5,
1134 dt: 0.5,
1135 }
1136 }
1137
1138 fn finite(values: &[f64]) -> bool {
1139 values.iter().all(|value| value.is_finite())
1140 }
1141
1142 fn valid_configuration(&self) -> bool {
1143 Self::finite(&[
1144 self.v_rest,
1145 self.v_reset,
1146 self.v_threshold,
1147 self.tau_m,
1148 self.tau_persistent,
1149 self.persistent_gain,
1150 self.gain,
1151 self.dt,
1152 ]) && self.tau_m > 0.0
1153 && self.tau_persistent > 0.0
1154 && self.persistent_gain >= 0.0
1155 && self.gain >= 0.0
1156 && self.dt > 0.0
1157 && self.v_reset < self.v_threshold
1158 }
1159
1160 fn valid_state(&self) -> bool {
1161 Self::finite(&[self.v, self.persistent])
1162 && (-100.0..=60.0).contains(&self.v)
1163 && self.persistent >= 0.0
1164 }
1165
1166 fn first_order_relaxation(previous: f64, steady_state: f64, dt: f64, tau: f64) -> f64 {
1167 previous + (steady_state - previous) * (-(-dt / tau).exp_m1())
1168 }
1169
1170 pub fn step(&mut self, current: f64) -> i32 {
1171 if !self.valid_configuration() || !self.valid_state() || !current.is_finite() {
1172 return 0;
1173 }
1174 let input = self.gain * current.max(0.0);
1175 if !input.is_finite() {
1176 return 0;
1177 }
1178 let next_persistent = Self::first_order_relaxation(
1179 self.persistent,
1180 self.persistent_gain * input,
1181 self.dt,
1182 self.tau_persistent,
1183 )
1184 .max(0.0);
1185 let next_v = Self::first_order_relaxation(
1186 self.v,
1187 self.v_rest + input + next_persistent,
1188 self.dt,
1189 self.tau_m,
1190 );
1191 if !Self::finite(&[next_persistent, next_v]) {
1192 return 0;
1193 }
1194 self.persistent = next_persistent;
1195 if next_v >= self.v_threshold {
1196 self.v = self.v_reset;
1197 return 1;
1198 }
1199 self.v = next_v.clamp(-100.0, 60.0);
1200 0
1201 }
1202
1203 pub fn reset(&mut self) {
1204 self.v = self.v_rest;
1205 self.persistent = 0.0;
1206 }
1207}
1208
1209#[derive(Clone, Debug)]
1228pub struct DCNNeuron {
1229 pub v: f64,
1230 pub h: f64, pub n: f64, pub p: f64, pub s: f64, pub r: f64, pub ca: f64, pub g_na: f64,
1238 pub g_nap: f64, pub g_k: f64,
1240 pub g_t: f64, pub g_ahp: f64, pub g_h: f64, pub g_l: f64,
1244 pub e_na: f64,
1246 pub e_k: f64,
1247 pub e_ca: f64,
1248 pub e_h: f64,
1249 pub e_l: f64,
1250 pub c_m: f64,
1251 pub phi: f64,
1252 pub tau_ca: f64,
1253 pub kd_ahp: f64,
1254 pub dt: f64,
1255 pub v_threshold: f64,
1256 pub gain: f64,
1257}
1258
1259impl Default for DCNNeuron {
1260 fn default() -> Self {
1261 Self::new()
1262 }
1263}
1264
1265impl DCNNeuron {
1266 pub fn new() -> Self {
1267 Self {
1268 v: -60.0,
1269 h: 0.6,
1270 n: 0.32,
1271 p: 0.01, s: 0.8, r: 0.1, ca: 0.05, g_na: 35.0,
1276 g_nap: 0.5, g_k: 9.0,
1278 g_t: 0.1, g_ahp: 2.0, g_h: 0.02, g_l: 0.2, e_na: 55.0,
1283 e_k: -90.0,
1284 e_ca: 120.0,
1285 e_h: -40.0,
1286 e_l: -65.0,
1287 c_m: 1.0,
1288 phi: 5.0,
1289 tau_ca: 150.0,
1290 kd_ahp: 0.5,
1291 dt: 0.5,
1292 v_threshold: -20.0,
1293 gain: 1.0,
1294 }
1295 }
1296
1297 pub fn step(&mut self, current: f64) -> i32 {
1298 if !self.is_valid() || !current.is_finite() {
1299 return 0;
1300 }
1301 let input = self.gain * current;
1302 let sub_steps = 20;
1303 let sub_dt = self.dt / sub_steps as f64;
1304 let mut fired = 0i32;
1305 let (mut v, mut h, mut n, mut p, mut s, mut r, mut ca) =
1306 (self.v, self.h, self.n, self.p, self.s, self.r, self.ca);
1307
1308 for _ in 0..sub_steps {
1309 let alpha_m = safe_rate(0.1, 35.0, v, 10.0, 1.0);
1311 let beta_m = 4.0 * (-(v + 60.0) / 18.0).exp();
1312 let m_inf = alpha_m / (alpha_m + beta_m);
1313
1314 let alpha_h = 0.07 * (-(v + 58.0) / 20.0).exp();
1315 let beta_h = 1.0 / (1.0 + (-(v + 28.0) / 10.0).exp());
1316
1317 let alpha_n = safe_rate(0.01, 34.0, v, 10.0, 0.1);
1319 let beta_n = 0.125 * (-(v + 44.0) / 80.0).exp();
1320
1321 let p_inf = 1.0 / (1.0 + (-(v + 48.0) / 5.0).exp());
1323 let tau_p = 5.0 + 15.0 / (1.0 + ((v + 48.0) / 10.0).powi(2)).max(0.01);
1324
1325 let m_t_inf = 1.0 / (1.0 + (-(v + 52.0) / 5.0).exp());
1327 let s_inf = 1.0 / (1.0 + ((v + 60.0) / 6.5).exp());
1328 let tau_s = 20.0 + 50.0 / (1.0 + ((v + 65.0) / 10.0).exp());
1329
1330 let r_inf = 1.0 / (1.0 + ((v + 80.0) / 10.0).exp());
1332 let tau_r = 100.0 + 200.0 / (1.0 + ((v + 70.0) / 10.0).exp());
1333
1334 h = dcn_exact_hh_gate(h, alpha_h, beta_h, self.phi, sub_dt);
1338 n = dcn_exact_hh_gate(n, alpha_n, beta_n, self.phi, sub_dt);
1339 p = dcn_exact_relax(p, p_inf, tau_p, sub_dt);
1340 s = dcn_exact_relax(s, s_inf, tau_s, sub_dt);
1341 r = dcn_exact_relax(r, r_inf, tau_r, sub_dt);
1342
1343 let i_t = self.g_t * m_t_inf.powi(2) * s * (v - self.e_ca);
1345 let ca_entry = if i_t < 0.0 { -i_t * 0.001 } else { 0.0 };
1346 ca = dcn_exact_relax(ca, ca_entry * self.tau_ca, self.tau_ca, sub_dt).max(0.0);
1347
1348 let ahp_inf = ca.powi(2) / (ca.powi(2) + self.kd_ahp.powi(2));
1350
1351 let g_na_eff = self.g_na * m_inf.powi(3) * h;
1354 let g_nap_eff = self.g_nap * p;
1355 let g_k_eff = self.g_k * n.powi(4);
1356 let g_t_eff = self.g_t * m_t_inf.powi(2) * s;
1357 let g_ahp_eff = self.g_ahp * ahp_inf;
1358 let g_h_eff = self.g_h * r;
1359 v = dcn_exact_voltage_step(
1360 v,
1361 input,
1362 self.c_m,
1363 sub_dt,
1364 &[
1365 (g_na_eff, self.e_na),
1366 (g_nap_eff, self.e_na),
1367 (g_k_eff, self.e_k),
1368 (g_t_eff, self.e_ca),
1369 (g_ahp_eff, self.e_k),
1370 (g_h_eff, self.e_h),
1371 (self.g_l, self.e_l),
1372 ],
1373 );
1374
1375 if v >= self.v_threshold {
1376 fired = 1;
1377 v = -60.0;
1378 s *= 0.5; ca += 0.5; }
1381 }
1382
1383 if ![v, h, n, p, s, r, ca].iter().all(|value| value.is_finite()) {
1384 return 0;
1385 }
1386 self.v = v.clamp(-100.0, 60.0);
1387 self.h = h.clamp(0.0, 1.0);
1388 self.n = n.clamp(0.0, 1.0);
1389 self.p = p.clamp(0.0, 1.0);
1390 self.s = s.clamp(0.0, 1.0);
1391 self.r = r.clamp(0.0, 1.0);
1392 self.ca = ca.max(0.0);
1393
1394 fired
1395 }
1396
1397 pub fn reset(&mut self) {
1398 *self = Self::new();
1399 }
1400
1401 fn is_valid(&self) -> bool {
1402 [
1403 self.v,
1404 self.h,
1405 self.n,
1406 self.p,
1407 self.s,
1408 self.r,
1409 self.ca,
1410 self.g_na,
1411 self.g_nap,
1412 self.g_k,
1413 self.g_t,
1414 self.g_ahp,
1415 self.g_h,
1416 self.g_l,
1417 self.e_na,
1418 self.e_k,
1419 self.e_ca,
1420 self.e_h,
1421 self.e_l,
1422 self.c_m,
1423 self.phi,
1424 self.tau_ca,
1425 self.kd_ahp,
1426 self.dt,
1427 self.v_threshold,
1428 self.gain,
1429 ]
1430 .iter()
1431 .all(|value| value.is_finite())
1432 && [self.h, self.n, self.p, self.s, self.r]
1433 .iter()
1434 .all(|gate| (0.0..=1.0).contains(gate))
1435 && self.ca >= 0.0
1436 && (-100.0..=60.0).contains(&self.v)
1437 && [
1438 self.g_na, self.g_nap, self.g_k, self.g_t, self.g_ahp, self.g_h, self.g_l,
1439 ]
1440 .iter()
1441 .all(|g| *g >= 0.0)
1442 && self.c_m > 0.0
1443 && self.phi > 0.0
1444 && self.tau_ca > 0.0
1445 && self.kd_ahp > 0.0
1446 && self.dt > 0.0
1447 && self.gain >= 0.0
1448 }
1449}
1450
1451fn dcn_exact_relax(value: f64, target: f64, tau: f64, dt: f64) -> f64 {
1452 target + (value - target) * (-dt / tau).exp()
1453}
1454
1455fn dcn_exact_hh_gate(value: f64, alpha: f64, beta: f64, phi: f64, dt: f64) -> f64 {
1456 let rate = phi * (alpha + beta);
1457 let target = alpha / (alpha + beta);
1458 target + (value - target) * (-rate * dt).exp()
1459}
1460
1461fn dcn_exact_voltage_step(
1462 v: f64,
1463 input_current: f64,
1464 c_m: f64,
1465 dt: f64,
1466 conductances: &[(f64, f64)],
1467) -> f64 {
1468 let g_total: f64 = conductances.iter().map(|(g, _)| *g).sum();
1469 if g_total <= 0.0 {
1470 return v + dt * input_current / c_m;
1471 }
1472 let reversal_drive: f64 = conductances.iter().map(|(g, e_rev)| g * e_rev).sum();
1473 let v_inf = (input_current + reversal_drive) / g_total;
1474 v_inf + (v - v_inf) * (-dt * g_total / c_m).exp()
1475}
1476
1477#[cfg(test)]
1482mod tests {
1483 use super::*;
1484
1485 #[test]
1488 fn granule_fires_with_strong_input() {
1489 let mut n = GranuleCell::new();
1490 let mut spikes = 0;
1491 for _ in 0..10_000 {
1492 spikes += n.step(15.0);
1493 }
1494 assert!(
1495 spikes > 10,
1496 "Granule cell must fire with strong excitatory input, got {spikes}"
1497 );
1498 }
1499
1500 #[test]
1501 fn granule_silent_at_rest() {
1502 let mut n = GranuleCell::new();
1503 let mut spikes = 0;
1504 for _ in 0..10_000 {
1505 spikes += n.step(0.0);
1506 }
1507 assert_eq!(
1508 spikes, 0,
1509 "Granule cell must be silent without input (tonic GABA inhibition)"
1510 );
1511 }
1512
1513 #[test]
1514 fn granule_no_fire_weak_input() {
1515 let mut n = GranuleCell::new();
1517 let mut spikes = 0;
1518 for _ in 0..10_000 {
1519 spikes += n.step(1.0);
1520 }
1521 assert!(
1522 spikes == 0,
1523 "Weak input should not overcome tonic GABA, got {spikes}"
1524 );
1525 }
1526
1527 #[test]
1528 fn granule_tonic_gaba_raises_threshold() {
1529 let mut with_gaba = GranuleCell::new();
1531 let mut no_gaba = GranuleCell::new();
1532 no_gaba.g_tonic = 0.0;
1533
1534 let input = 8.0;
1535 let mut spikes_gaba = 0;
1536 let mut spikes_no_gaba = 0;
1537 for _ in 0..10_000 {
1538 spikes_gaba += with_gaba.step(input);
1539 spikes_no_gaba += no_gaba.step(input);
1540 }
1541 assert!(
1542 spikes_no_gaba > spikes_gaba,
1543 "Removing tonic GABA must increase firing: no_gaba={spikes_no_gaba} vs gaba={spikes_gaba}"
1544 );
1545 }
1546
1547 #[test]
1548 fn granule_has_seven_currents() {
1549 let n = GranuleCell::new();
1551 assert!(n.g_na > 0.0, "Must have INa");
1552 assert!(n.g_kdr > 0.0, "Must have IK_dr");
1553 assert!(n.g_ka > 0.0, "Must have IK_A");
1554 assert!(n.g_t > 0.0, "Must have ICa_T");
1555 assert!(n.g_kca > 0.0, "Must have IK_Ca");
1556 assert!(n.g_h > 0.0, "Must have Ih");
1557 assert!(n.g_l > 0.0, "Must have IL");
1558 }
1559
1560 #[test]
1561 fn granule_t_type_deinactivates_at_rest() {
1562 let mut n = GranuleCell::new();
1564 for _ in 0..5000 {
1565 n.step(0.0);
1566 }
1567 assert!(
1568 n.s > 0.5,
1569 "T-type must be partially de-inactivated at rest, s={}",
1570 n.s
1571 );
1572 }
1573
1574 #[test]
1575 fn granule_gate_and_calcium_kinetics_use_closed_form_relaxation() {
1576 let mut n = GranuleCell::new();
1577 n.g_na = 0.0;
1578 n.g_kdr = 0.0;
1579 n.g_ka = 0.0;
1580 n.g_t = 0.0;
1581 n.g_kca = 0.0;
1582 n.g_h = 0.0;
1583 n.g_l = 0.0;
1584 n.g_tonic = 0.0;
1585 n.gain = 0.0;
1586 n.sub_steps = 1;
1587 let (v0, m0, h0, n0, a0, b0, mt0, s0, ca0, r0) =
1588 (n.v, n.m, n.h, n.n, n.a, n.b, n.m_t, n.s, n.ca, n.r);
1589 let m_inf = GranuleCell::boltz(v0, -30.0, 7.0);
1590 let tau_m = 0.1 + 0.3 / (1.0 + ((v0 + 30.0) / 10.0).powi(2)).max(0.01);
1591 let h_inf = GranuleCell::boltz(v0, -52.0, -6.0);
1592 let tau_h = 0.5 + 5.0 / (1.0 + ((v0 + 50.0) / 15.0).powi(2)).max(0.01);
1593 let n_inf = GranuleCell::boltz(v0, -35.0, 8.0);
1594 let tau_n = 1.0 + 5.0 / (1.0 + ((v0 + 35.0) / 15.0).powi(2)).max(0.01);
1595 let a_inf = GranuleCell::boltz(v0, -50.0, 20.0);
1596 let b_inf = GranuleCell::boltz(v0, -70.0, -6.0);
1597 let mt_inf = GranuleCell::boltz(v0, -52.0, 5.0);
1598 let s_inf = GranuleCell::boltz(v0, -60.0, -6.5);
1599 let tau_s = 20.0 + 50.0 / (1.0 + ((v0 + 65.0) / 10.0).powi(2)).max(0.01);
1600 let r_inf = GranuleCell::boltz(v0, -80.0, -10.0);
1601 let tau_r = 50.0 + 200.0 / (1.0 + ((v0 + 80.0) / 20.0).powi(2)).max(0.01);
1602
1603 n.step(0.0);
1604
1605 assert_close_granule(n.v, v0);
1606 assert_close_granule(n.m, granule_exact_relax(m0, m_inf, tau_m, n.dt));
1607 assert_close_granule(n.h, granule_exact_relax(h0, h_inf, tau_h, n.dt));
1608 assert_close_granule(n.n, granule_exact_relax(n0, n_inf, tau_n, n.dt));
1609 assert_close_granule(n.a, granule_exact_relax(a0, a_inf, 2.0, n.dt));
1610 assert_close_granule(n.b, granule_exact_relax(b0, b_inf, 50.0, n.dt));
1611 assert_close_granule(n.m_t, granule_exact_relax(mt0, mt_inf, 1.0, n.dt));
1612 assert_close_granule(n.s, granule_exact_relax(s0, s_inf, tau_s, n.dt));
1613 assert_close_granule(n.ca, granule_exact_relax(ca0, 0.0, n.tau_ca, n.dt));
1614 assert_close_granule(n.r, granule_exact_relax(r0, r_inf, tau_r, n.dt));
1615 }
1616
1617 #[test]
1618 fn granule_ca_rises_with_spiking() {
1619 let mut n = GranuleCell::new();
1621 let ca0 = n.ca;
1622 for _ in 0..5000 {
1623 n.step(8.0);
1624 }
1625 assert!(
1626 n.ca > ca0,
1627 "Ca²⁺ should rise in the T-current firing regime: ca0={ca0}, ca_now={}",
1628 n.ca
1629 );
1630 }
1631
1632 #[test]
1633 fn granule_negative_input_no_crash() {
1634 let mut n = GranuleCell::new();
1635 for _ in 0..10_000 {
1636 n.step(-100.0);
1637 }
1638 assert!(n.v.is_finite(), "Must stay finite with negative input");
1639 assert!(n.v >= -100.0, "Must be bounded");
1640 }
1641
1642 #[test]
1643 fn granule_nan_input_stays_finite() {
1644 let mut n = GranuleCell::new();
1645 let before = n.clone();
1646 n.step(f64::NAN);
1647 assert!(n.v.is_finite(), "NaN input must not corrupt state");
1648 assert_eq!(n.v, before.v);
1649 assert_eq!(n.ca, before.ca);
1650 assert_eq!(n.s, before.s);
1651 }
1652
1653 #[test]
1654 fn granule_corrupted_state_preserved_on_step() {
1655 let mut n = GranuleCell::new();
1656 n.m = -0.1;
1657 let before = n.clone();
1658 assert_eq!(n.step(10.0), 0);
1659 assert_eq!(n.v, before.v);
1660 assert_eq!(n.m, before.m);
1661 assert_eq!(n.ca, before.ca);
1662 }
1663
1664 #[test]
1665 fn granule_extreme_input_bounded() {
1666 let mut n = GranuleCell::new();
1667 for _ in 0..1000 {
1668 n.step(1e6);
1669 }
1670 assert!(
1671 n.v.is_finite() && n.v <= 60.0,
1672 "Extreme input must stay bounded"
1673 );
1674 }
1675
1676 #[test]
1677 fn granule_reset_clears_state() {
1678 let mut n = GranuleCell::new();
1679 for _ in 0..1000 {
1680 n.step(20.0);
1681 }
1682 n.reset();
1683 assert_eq!(n.v, -70.0);
1684 assert_eq!(n.s, 0.95);
1685 assert_eq!(n.m, 0.02);
1686 }
1687
1688 #[test]
1689 fn granule_high_input_resistance() {
1690 let mut n = GranuleCell::new();
1692 let v_before = n.v;
1693 n.step(5.0);
1695 let dv = n.v - v_before;
1696 assert!(
1697 dv > 0.5,
1698 "High Rin should give large voltage change, got dv={dv}"
1699 );
1700 }
1701
1702 #[test]
1703 fn granule_performance_10k_steps() {
1704 let start = std::time::Instant::now();
1705 let mut n = GranuleCell::new();
1706 for _ in 0..10_000 {
1707 std::hint::black_box(n.step(10.0));
1708 }
1709 let elapsed = start.elapsed();
1710 assert!(
1711 elapsed.as_millis() < 100,
1712 "10k exact-integrator steps must complete in <100ms, took {}ms",
1713 elapsed.as_millis()
1714 );
1715 }
1716
1717 fn assert_close_granule(observed: f64, expected: f64) {
1718 assert!(
1719 (observed - expected).abs() <= 1.0e-12,
1720 "observed {:.17e}, expected {:.17e}",
1721 observed,
1722 expected,
1723 );
1724 }
1725
1726 #[test]
1729 fn golgi_fires_with_input() {
1730 let mut n = GolgiCell::new();
1731 let mut spikes = 0;
1732 for _ in 0..10_000 {
1733 spikes += n.step(15.0);
1734 }
1735 assert!(
1736 spikes > 10,
1737 "Golgi cell must fire with excitatory input, got {spikes}"
1738 );
1739 }
1740
1741 #[test]
1742 fn golgi_spontaneous_firing() {
1743 let mut n = GolgiCell::new();
1745 let _spikes: i32 = (0..20_000).map(|_| n.step(0.0)).sum();
1746 let mut n2 = GolgiCell::new();
1749 let mut spikes_small = 0;
1750 for _ in 0..20_000 {
1751 spikes_small += n2.step(0.5);
1752 }
1753 assert!(
1754 spikes_small > 0,
1755 "Golgi cell should fire with minimal input (near-threshold), got {spikes_small}"
1756 );
1757 }
1758
1759 #[test]
1760 fn golgi_ahp_reduces_rate_at_high_drive() {
1761 let mut with_ahp = GolgiCell::new();
1763 let mut no_ahp = GolgiCell::new();
1764 no_ahp.g_bk = 0.0;
1765 no_ahp.g_sk = 0.0;
1766 let mut spikes_with = 0;
1767 let mut spikes_no = 0;
1768 for _ in 0..10_000 {
1769 spikes_with += with_ahp.step(10.0);
1770 spikes_no += no_ahp.step(10.0);
1771 }
1772 assert!(
1773 spikes_no >= spikes_with,
1774 "AHP removal should increase firing: with={spikes_with}, without={spikes_no}"
1775 );
1776 }
1777
1778 #[test]
1779 fn golgi_ka_is_transient() {
1780 let mut with_a = GolgiCell::new();
1783 let mut no_a = GolgiCell::new();
1784 no_a.g_ka = 0.0;
1785 let mut spikes_with = 0;
1786 let mut spikes_no = 0;
1787 for _ in 0..10_000 {
1788 spikes_with += with_a.step(5.0);
1789 spikes_no += no_a.step(5.0);
1790 }
1791 assert!(spikes_with > 0, "Must fire with K_A");
1793 assert!(
1795 spikes_with != spikes_no,
1796 "K_A should affect firing rate: with={spikes_with}, without={spikes_no}"
1797 );
1798 }
1799
1800 #[test]
1801 fn golgi_ca_accumulates_during_spiking() {
1802 let mut n = GolgiCell::new();
1803 let ca_init = n.ca;
1804 for _ in 0..5000 {
1805 n.step(10.0);
1806 }
1807 assert!(
1808 n.ca > ca_init,
1809 "Ca²⁺ must rise during spiking: init={ca_init}, now={}",
1810 n.ca
1811 );
1812 }
1813
1814 #[test]
1815 fn golgi_negative_input_no_crash() {
1816 let mut n = GolgiCell::new();
1817 for _ in 0..10_000 {
1818 n.step(-100.0);
1819 }
1820 assert!(n.v.is_finite(), "Must stay finite with negative input");
1821 assert!(n.v >= -100.0);
1822 }
1823
1824 #[test]
1825 fn golgi_nan_input_stays_finite() {
1826 let mut n = GolgiCell::new();
1827 n.step(f64::NAN);
1828 assert!(n.v.is_finite(), "NaN input must not corrupt state");
1829 }
1830
1831 #[test]
1832 fn golgi_extreme_input_bounded() {
1833 let mut n = GolgiCell::new();
1834 for _ in 0..1000 {
1835 n.step(1e6);
1836 }
1837 assert!(
1838 n.v.is_finite() && n.v <= 60.0,
1839 "Extreme input must stay bounded"
1840 );
1841 }
1842
1843 #[test]
1844 fn golgi_reset_clears_state() {
1845 let mut n = GolgiCell::new();
1846 for _ in 0..5000 {
1847 n.step(10.0);
1848 }
1849 n.reset();
1850 let fresh = GolgiCell::new();
1851 assert_eq!(n.v, fresh.v);
1852 assert_eq!(n.ca, fresh.ca);
1853 assert_eq!(n.m, fresh.m);
1854 assert_eq!(n.h, fresh.h);
1855 assert_eq!(n.p_na, fresh.p_na);
1856 assert_eq!(n.w, fresh.w);
1857 assert_eq!(n.r, fresh.r);
1858 }
1859
1860 #[test]
1861 fn golgi_gates_bounded() {
1862 let mut n = GolgiCell::new();
1863 for _ in 0..10_000 {
1864 n.step(15.0);
1865 }
1866 for (name, val) in [
1868 ("m", n.m),
1869 ("h", n.h),
1870 ("p_na", n.p_na),
1871 ("n", n.n),
1872 ("a", n.a),
1873 ("b", n.b),
1874 ("w", n.w),
1875 ("m_t", n.m_t),
1876 ("s", n.s),
1877 ("c_n", n.c_n),
1878 ("r", n.r),
1879 ] {
1880 assert!((0.0..=1.0).contains(&val), "{name} out of bounds: {val}");
1881 }
1882 assert!(n.ca >= 0.0, "Ca²⁺ must be non-negative: {}", n.ca);
1883 }
1884
1885 #[test]
1886 fn golgi_has_eleven_currents() {
1887 let n = GolgiCell::new();
1889 assert!(n.g_na_t > 0.0, "Na_t missing");
1890 assert!(n.g_na_p > 0.0, "Na_p missing");
1891 assert!(n.g_kdr > 0.0, "K_dr missing");
1892 assert!(n.g_ka > 0.0, "K_A missing");
1893 assert!(n.g_km > 0.0, "K_M missing");
1894 assert!(n.g_cat > 0.0, "Ca_T missing");
1895 assert!(n.g_can > 0.0, "Ca_N missing");
1896 assert!(n.g_bk > 0.0, "BK missing");
1897 assert!(n.g_sk > 0.0, "SK missing");
1898 assert!(n.g_h > 0.0, "Ih missing");
1899 assert!(n.g_l > 0.0, "Leak missing");
1900 }
1901
1902 #[test]
1903 fn golgi_persistent_na_depolarises() {
1904 let mut with_nap = GolgiCell::new();
1906 let mut no_nap = GolgiCell::new();
1907 no_nap.g_na_p = 0.0;
1908 let mut spikes_with = 0;
1909 let mut spikes_no = 0;
1910 for _ in 0..10_000 {
1911 spikes_with += with_nap.step(2.0);
1912 spikes_no += no_nap.step(2.0);
1913 }
1914 assert!(
1915 spikes_with >= spikes_no,
1916 "Na_p should increase excitability: with={spikes_with} vs without={spikes_no}"
1917 );
1918 }
1919
1920 #[test]
1921 fn golgi_km_modulates_firing_pattern() {
1922 let mut with_km = GolgiCell::new();
1927 let mut no_km = GolgiCell::new();
1928 no_km.g_km = 0.0;
1929 let mut spikes_with = 0;
1930 let mut spikes_no = 0;
1931 for _ in 0..10_000 {
1932 spikes_with += with_km.step(10.0);
1933 spikes_no += no_km.step(10.0);
1934 }
1935 assert!(spikes_with > 0, "Golgi cell with K_M should fire");
1936 assert!(spikes_no > 0, "Golgi cell without K_M should fire");
1937 assert!(
1938 spikes_with != spikes_no,
1939 "K_M should measurably modulate firing: with_km={spikes_with}, without={spikes_no}"
1940 );
1941 }
1942
1943 #[test]
1944 fn golgi_ih_sag() {
1945 let mut with_h = GolgiCell::new();
1947 let mut no_h = GolgiCell::new();
1948 no_h.g_h = 0.0;
1949 for _ in 0..10_000 {
1951 with_h.step(-1.0);
1952 no_h.step(-1.0);
1953 }
1954 assert!(
1956 with_h.v > no_h.v,
1957 "Ih should cause sag (less hyperpolarised): with_h={:.1} vs no_h={:.1}",
1958 with_h.v,
1959 no_h.v
1960 );
1961 }
1962
1963 #[test]
1964 fn golgi_bk_fast_ahp() {
1965 let mut with_bk = GolgiCell::new();
1967 let mut no_bk = GolgiCell::new();
1968 no_bk.g_bk = 0.0;
1969 let mut spikes_with = 0;
1971 let mut spikes_no = 0;
1972 for _ in 0..10_000 {
1973 spikes_with += with_bk.step(10.0);
1974 spikes_no += no_bk.step(10.0);
1975 }
1976 assert!(
1978 spikes_with > 0 && spikes_no > 0,
1979 "Both should fire: with_bk={spikes_with}, no_bk={spikes_no}"
1980 );
1981 }
1982
1983 #[test]
1984 fn golgi_sk_slow_adaptation() {
1985 let mut with_sk = GolgiCell::new();
1987 let mut no_sk = GolgiCell::new();
1988 no_sk.g_sk = 0.0;
1989 let mut spikes_with = 0;
1990 let mut spikes_no = 0;
1991 for _ in 0..20_000 {
1992 spikes_with += with_sk.step(8.0);
1993 spikes_no += no_sk.step(8.0);
1994 }
1995 assert!(
1996 spikes_no >= spikes_with,
1997 "SK removal should increase firing: with_sk={spikes_with}, no_sk={spikes_no}"
1998 );
1999 }
2000
2001 #[test]
2002 fn golgi_performance_1k_steps() {
2003 let start = std::time::Instant::now();
2004 let mut n = GolgiCell::new();
2005 for _ in 0..1_000 {
2006 std::hint::black_box(n.step(5.0));
2007 }
2008 let elapsed = start.elapsed();
2009 assert!(
2010 elapsed.as_millis() < 50,
2011 "1k steps must complete in <50ms, took {}ms",
2012 elapsed.as_millis()
2013 );
2014 }
2015
2016 #[test]
2019 fn stellate_fires_with_input() {
2020 let mut n = StellateCell::new();
2021 let mut spikes = 0;
2022 for _ in 0..2_000 {
2023 spikes += n.step(2.0);
2024 }
2025 assert!(
2026 spikes > 5,
2027 "Stellate cell must fire with input, got {spikes}"
2028 );
2029 }
2030
2031 #[test]
2032 fn stellate_silent_without_input() {
2033 let mut n = StellateCell::new();
2034 let mut spikes = 0;
2035 for _ in 0..10_000 {
2036 spikes += n.step(0.0);
2037 }
2038 assert_eq!(
2039 spikes, 0,
2040 "Stellate cell must be silent without input, got {spikes}"
2041 );
2042 }
2043
2044 #[test]
2045 fn stellate_high_frequency() {
2046 let mut n = StellateCell::new();
2048 let mut spikes = 0;
2049 for _ in 0..2_000 {
2050 spikes += n.step(20.0);
2051 }
2052 assert!(
2054 spikes > 50,
2055 "FS stellate should fire at high rate, got {spikes}"
2056 );
2057 }
2058
2059 #[test]
2060 fn stellate_minimal_adaptation() {
2061 let mut n = StellateCell::new();
2063 let input = 10.0;
2064 let mut spikes_early = 0;
2065 for _ in 0..2000 {
2066 spikes_early += n.step(input);
2067 }
2068 let mut spikes_late = 0;
2069 for _ in 0..2000 {
2070 spikes_late += n.step(input);
2071 }
2072 let diff = (spikes_early - spikes_late).abs();
2074 assert!(
2075 diff < 20,
2076 "FS should have minimal adaptation: early={spikes_early}, late={spikes_late}"
2077 );
2078 }
2079
2080 #[test]
2081 fn stellate_kv3_narrows_spikes() {
2082 let mut with_kv3 = StellateCell::new();
2084 let mut no_kv3 = StellateCell::new();
2085 no_kv3.g_kv3 = 0.0;
2086
2087 let mut spikes_kv3 = 0;
2088 let mut spikes_no = 0;
2089 for _ in 0..2000 {
2090 spikes_kv3 += with_kv3.step(15.0);
2091 spikes_no += no_kv3.step(15.0);
2092 }
2093 assert!(spikes_kv3 > 0, "With Kv3.1 must fire, got {spikes_kv3}");
2095 assert!(
2096 spikes_no >= 0,
2097 "No-Kv3.1 baseline must not panic, got {spikes_no}"
2098 );
2099 }
2100
2101 #[test]
2102 fn stellate_negative_input_no_crash() {
2103 let mut n = StellateCell::new();
2104 for _ in 0..10_000 {
2105 n.step(-100.0);
2106 }
2107 assert!(n.v.is_finite());
2108 assert!(n.v >= -100.0);
2109 }
2110
2111 #[test]
2112 fn stellate_nan_input_stays_finite() {
2113 let mut n = StellateCell::new();
2114 let before = n.clone();
2115 n.step(f64::NAN);
2116 assert!(n.v.is_finite());
2117 assert_eq!(n.v, before.v);
2118 assert_eq!(n.h, before.h);
2119 assert_eq!(n.n, before.n);
2120 assert_eq!(n.p, before.p);
2121 }
2122
2123 #[test]
2124 fn stellate_corrupted_state_preserved_on_step() {
2125 let mut n = StellateCell::new();
2126 n.h = -0.1;
2127 let before = n.clone();
2128 assert_eq!(n.step(8.0), 0);
2129 assert_eq!(n.v, before.v);
2130 assert_eq!(n.h, before.h);
2131 assert_eq!(n.n, before.n);
2132 assert_eq!(n.p, before.p);
2133 }
2134
2135 #[test]
2136 fn stellate_invalid_voltage_preserved_on_step() {
2137 let mut n = StellateCell::new();
2138 n.v = 60.1;
2139 let before = n.clone();
2140 assert_eq!(n.step(8.0), 0);
2141 assert_eq!(n.v, before.v);
2142 assert_eq!(n.h, before.h);
2143 assert_eq!(n.n, before.n);
2144 assert_eq!(n.p, before.p);
2145 }
2146
2147 #[test]
2148 fn stellate_closed_form_gate_kinetics() {
2149 let mut n = StellateCell::new();
2150 n.g_na = 0.0;
2151 n.g_k = 0.0;
2152 n.g_kv3 = 0.0;
2153 n.g_l = 0.0;
2154 n.gain = 0.0;
2155
2156 let alpha_h = 0.07 * StellateCell::safe_exp(-(n.v + 58.0) / 20.0);
2157 let beta_h = StellateCell::boltz(n.v, -28.0, 10.0);
2158 let alpha_n = safe_rate(0.01, 34.0, n.v, 10.0, 0.1);
2159 let beta_n = 0.125 * StellateCell::safe_exp(-(n.v + 44.0) / 80.0);
2160 let p_inf = StellateCell::boltz(n.v, -10.0, 10.0);
2161 let tau_p = 1.0 + 4.0 / (1.0 + StellateCell::safe_exp((n.v + 20.0) / 15.0));
2162
2163 let expected_h = exact_hh_gate_stellate(n.h, alpha_h, beta_h, n.phi, n.dt);
2164 let expected_n = exact_hh_gate_stellate(n.n, alpha_n, beta_n, n.phi, n.dt);
2165 let expected_p = exact_relax_stellate(n.p, p_inf, tau_p, n.dt);
2166 let expected_v = n.v;
2167
2168 assert_eq!(n.step(0.0), 0);
2169 assert_close_stellate(n.v, expected_v, 1e-12);
2170 assert_close_stellate(n.h, expected_h, 1e-12);
2171 assert_close_stellate(n.n, expected_n, 1e-12);
2172 assert_close_stellate(n.p, expected_p, 1e-12);
2173 }
2174
2175 fn exact_relax_stellate(value: f64, target: f64, tau: f64, dt: f64) -> f64 {
2176 if tau <= f64::EPSILON {
2177 target.clamp(0.0, 1.0)
2178 } else {
2179 (target + (value - target) * (-dt / tau).exp()).clamp(0.0, 1.0)
2180 }
2181 }
2182
2183 fn exact_hh_gate_stellate(value: f64, alpha: f64, beta: f64, phi: f64, dt: f64) -> f64 {
2184 let total = alpha + beta;
2185 if total <= f64::EPSILON {
2186 value.clamp(0.0, 1.0)
2187 } else {
2188 let steady = alpha / total;
2189 (steady + (value - steady) * (-phi * total * dt).exp()).clamp(0.0, 1.0)
2190 }
2191 }
2192
2193 fn assert_close_stellate(actual: f64, expected: f64, tolerance: f64) {
2194 assert!(
2195 (actual - expected).abs() <= tolerance,
2196 "actual={actual:.16e} expected={expected:.16e} tolerance={tolerance:.3e}"
2197 );
2198 }
2199
2200 #[test]
2201 fn stellate_extreme_input_bounded() {
2202 let mut n = StellateCell::new();
2203 for _ in 0..1000 {
2204 n.step(1e6);
2205 }
2206 assert!(n.v.is_finite() && n.v <= 60.0);
2207 }
2208
2209 #[test]
2210 fn stellate_reset_clears_state() {
2211 let mut n = StellateCell::new();
2212 for _ in 0..1000 {
2213 n.step(20.0);
2214 }
2215 n.reset();
2216 assert_eq!(n.v, -65.0);
2217 assert_eq!(n.p, 0.0);
2218 }
2219
2220 #[test]
2221 fn stellate_gates_bounded() {
2222 let mut n = StellateCell::new();
2223 for _ in 0..10_000 {
2224 n.step(15.0);
2225 }
2226 assert!(n.h >= 0.0 && n.h <= 1.0);
2227 assert!(n.n >= 0.0 && n.n <= 1.0);
2228 assert!(n.p >= 0.0 && n.p <= 1.0);
2229 }
2230
2231 #[test]
2232 fn stellate_performance_1k_steps() {
2233 let start = std::time::Instant::now();
2234 let mut n = StellateCell::new();
2235 for _ in 0..1_000 {
2236 std::hint::black_box(n.step(10.0));
2237 }
2238 let elapsed = start.elapsed();
2239 assert!(
2240 elapsed.as_millis() < 200,
2241 "1k steps must complete in <200ms, took {}ms",
2242 elapsed.as_millis()
2243 );
2244 }
2245
2246 #[test]
2249 fn lugaro_fires_with_input() {
2250 let mut n = LugaroCell::new();
2251 let mut spikes = 0;
2252 for _ in 0..10_000 {
2253 spikes += n.step(5.0);
2254 }
2255 assert!(
2256 spikes > 10,
2257 "Lugaro must fire with excitatory input, got {spikes}"
2258 );
2259 }
2260
2261 #[test]
2262 fn lugaro_low_threshold() {
2263 let mut n = LugaroCell::new();
2265 let mut spikes = 0;
2266 for _ in 0..10_000 {
2267 spikes += n.step(4.0);
2268 }
2269 assert!(
2270 spikes > 10,
2271 "Lugaro should fire easily with moderate input, got {spikes}"
2272 );
2273 }
2274
2275 #[test]
2276 fn lugaro_adaptation() {
2277 let mut n = LugaroCell::new();
2278 let input = 10.0;
2279 let mut spikes_early = 0;
2280 for _ in 0..2000 {
2281 spikes_early += n.step(input);
2282 }
2283 let mut spikes_late = 0;
2284 for _ in 0..2000 {
2285 spikes_late += n.step(input);
2286 }
2287 assert!(
2288 spikes_early >= spikes_late,
2289 "Adaptation should slow firing: early={spikes_early}, late={spikes_late}"
2290 );
2291 }
2292
2293 #[test]
2294 fn lugaro_serotonin_increases_firing() {
2295 let mut no_5ht = LugaroCell::new();
2296 let mut with_5ht = LugaroCell::with_serotonin(1.0);
2297
2298 let input = 3.0;
2299 let mut spikes_no = 0;
2300 let mut spikes_5ht = 0;
2301 for _ in 0..10_000 {
2302 spikes_no += no_5ht.step(input);
2303 spikes_5ht += with_5ht.step(input);
2304 }
2305 assert!(
2306 spikes_5ht >= spikes_no,
2307 "5-HT must increase firing: 5HT={spikes_5ht} vs none={spikes_no}"
2308 );
2309 }
2310
2311 #[test]
2312 fn lugaro_negative_input_no_crash() {
2313 let mut n = LugaroCell::new();
2314 for _ in 0..10_000 {
2315 n.step(-100.0);
2316 }
2317 assert!(n.v.is_finite());
2318 assert!(n.v >= -100.0);
2319 }
2320
2321 #[test]
2322 fn lugaro_nan_input_stays_finite() {
2323 let mut n = LugaroCell::new();
2324 let before = n.clone();
2325 n.step(f64::NAN);
2326 assert!(n.v.is_finite());
2327 assert_eq!(n.v, before.v);
2328 assert_eq!(n.adapt, before.adapt);
2329 }
2330
2331 #[test]
2332 fn lugaro_corrupted_state_preserved_on_step() {
2333 let mut n = LugaroCell::new();
2334 n.adapt = f64::NAN;
2335 let before = n.clone();
2336 assert_eq!(n.step(5.0), 0);
2337 assert_eq!(n.v, before.v);
2338 assert!(n.adapt.is_nan());
2339 }
2340
2341 #[test]
2342 fn lugaro_invalid_voltage_preserved_on_step() {
2343 let mut n = LugaroCell::new();
2344 n.v = 60.1;
2345 let before = n.clone();
2346 assert_eq!(n.step(5.0), 0);
2347 assert_eq!(n.v, before.v);
2348 assert_eq!(n.adapt, before.adapt);
2349 }
2350
2351 #[test]
2352 fn lugaro_closed_form_membrane_and_adaptation_relaxation() {
2353 let mut n = LugaroCell::new();
2354 n.v = -56.0;
2355 n.adapt = 0.2;
2356 n.gain = 0.0;
2357
2358 let v_inf = n.v_rest - n.adapt;
2359 let expected_v = exact_relax_lugaro(n.v, v_inf, n.tau_m, n.dt);
2360 let adapt_inf = (n.a_adapt * (expected_v - n.v_rest).max(0.0)).max(0.0);
2361 let expected_adapt = exact_relax_lugaro(n.adapt, adapt_inf, n.tau_adapt, n.dt).max(0.0);
2362
2363 assert_eq!(n.step(0.0), 0);
2364 assert_close_lugaro(n.v, expected_v, 1e-12);
2365 assert_close_lugaro(n.adapt, expected_adapt, 1e-12);
2366 }
2367
2368 fn exact_relax_lugaro(value: f64, target: f64, tau: f64, dt: f64) -> f64 {
2369 target + (value - target) * (-dt / tau).exp()
2370 }
2371
2372 fn assert_close_lugaro(actual: f64, expected: f64, tolerance: f64) {
2373 assert!(
2374 (actual - expected).abs() <= tolerance,
2375 "actual={actual:.16e} expected={expected:.16e} tolerance={tolerance:.3e}"
2376 );
2377 }
2378
2379 #[test]
2380 fn lugaro_extreme_input_bounded() {
2381 let mut n = LugaroCell::new();
2382 for _ in 0..1000 {
2383 n.step(1e6);
2384 }
2385 assert!(n.v.is_finite() && n.v <= 60.0);
2386 }
2387
2388 #[test]
2389 fn lugaro_reset_clears_state() {
2390 let mut n = LugaroCell::new();
2391 for _ in 0..1000 {
2392 n.step(10.0);
2393 }
2394 n.reset();
2395 assert_eq!(n.v, -55.0);
2396 assert_eq!(n.adapt, 0.0);
2397 assert_eq!(n.serotonin, 0.0);
2398 }
2399
2400 #[test]
2401 fn lugaro_adapt_increases_during_spiking() {
2402 let mut n = LugaroCell::new();
2403 let initial = n.adapt;
2404 for _ in 0..5000 {
2405 n.step(10.0);
2406 }
2407 assert!(
2408 n.adapt > initial,
2409 "Adaptation must increase during spiking, adapt={}",
2410 n.adapt
2411 );
2412 }
2413
2414 #[test]
2415 fn lugaro_performance_10k_steps() {
2416 let start = std::time::Instant::now();
2417 let mut n = LugaroCell::new();
2418 for _ in 0..10_000 {
2419 std::hint::black_box(n.step(5.0));
2420 }
2421 let elapsed = start.elapsed();
2422 assert!(
2423 elapsed.as_millis() < 50,
2424 "10k steps must complete in <50ms, took {}ms",
2425 elapsed.as_millis()
2426 );
2427 }
2428
2429 #[test]
2432 fn ubc_fires_with_input() {
2433 let mut n = UnipolarBrushCell::new();
2434 let mut spikes = 0;
2435 for _ in 0..10_000 {
2436 spikes += n.step(5.0);
2437 }
2438 assert!(
2439 spikes > 10,
2440 "UBC must fire with excitatory input, got {spikes}"
2441 );
2442 }
2443
2444 #[test]
2445 fn ubc_silent_without_input() {
2446 let mut n = UnipolarBrushCell::new();
2447 let mut spikes = 0;
2448 for _ in 0..10_000 {
2449 spikes += n.step(0.0);
2450 }
2451 assert_eq!(spikes, 0, "UBC must be silent without input");
2452 }
2453
2454 fn ubc_exact_relaxation(previous: f64, steady_state: f64, dt: f64, tau: f64) -> f64 {
2455 previous + (steady_state - previous) * (-(-dt / tau).exp_m1())
2456 }
2457
2458 #[test]
2459 fn ubc_uses_closed_form_persistent_and_membrane_relaxation() {
2460 let mut n = UnipolarBrushCell::new();
2461
2462 let spike = n.step(1.0);
2463
2464 let input_drive = n.gain;
2465 let expected_persistent =
2466 ubc_exact_relaxation(0.0, n.persistent_gain * input_drive, n.dt, n.tau_persistent);
2467 let expected_v = ubc_exact_relaxation(
2468 n.v_rest,
2469 n.v_rest + input_drive + expected_persistent,
2470 n.dt,
2471 n.tau_m,
2472 );
2473 assert_eq!(spike, 0);
2474 assert!(
2475 (n.persistent - expected_persistent).abs() <= 1e-12,
2476 "persistent={} expected={}",
2477 n.persistent,
2478 expected_persistent
2479 );
2480 assert!(
2481 (n.v - expected_v).abs() <= 1e-12,
2482 "v={} expected={}",
2483 n.v,
2484 expected_v
2485 );
2486 }
2487
2488 #[test]
2489 fn ubc_corrupted_state_is_preserved_on_step() {
2490 let mut n = UnipolarBrushCell::new();
2491 n.v = f64::NAN;
2492 n.persistent = 2.0;
2493
2494 assert_eq!(n.step(10.0), 0);
2495
2496 assert!(n.v.is_nan());
2497 assert_eq!(n.persistent, 2.0);
2498 }
2499
2500 #[test]
2501 fn ubc_persistent_activity() {
2502 let mut n = UnipolarBrushCell::new();
2504 for _ in 0..2000 {
2506 n.step(10.0);
2507 }
2508 assert!(
2509 n.persistent > 0.0,
2510 "Persistent current must build during input"
2511 );
2512
2513 let persistent_before = n.persistent;
2515 for _ in 0..100 {
2516 n.step(0.0);
2517 }
2518 assert!(
2519 n.persistent > 0.0,
2520 "Persistent current must persist after input removal"
2521 );
2522 assert!(
2523 n.persistent < persistent_before,
2524 "Persistent current must decay"
2525 );
2526 }
2527
2528 #[test]
2529 fn ubc_persistent_spikes_after_input() {
2530 let mut n = UnipolarBrushCell::new();
2532 for _ in 0..5000 {
2534 n.step(10.0);
2535 }
2536 let post_spikes: i32 = (0..500).map(|_| n.step(0.0)).sum();
2538 assert!(post_spikes >= 0, "post_spikes must be non-negative");
2540 assert!(n.v.is_finite());
2541 }
2542
2543 #[test]
2544 fn ubc_negative_input_no_crash() {
2545 let mut n = UnipolarBrushCell::new();
2546 for _ in 0..10_000 {
2547 n.step(-100.0);
2548 }
2549 assert!(n.v.is_finite());
2550 }
2551
2552 #[test]
2553 fn ubc_nan_input_stays_finite() {
2554 let mut n = UnipolarBrushCell::new();
2555 n.step(f64::NAN);
2556 assert!(n.v.is_finite());
2557 }
2558
2559 #[test]
2560 fn ubc_extreme_input_bounded() {
2561 let mut n = UnipolarBrushCell::new();
2562 for _ in 0..1000 {
2563 n.step(1e6);
2564 }
2565 assert!(n.v.is_finite() && n.v <= 60.0);
2566 }
2567
2568 #[test]
2569 fn ubc_reset_clears_state() {
2570 let mut n = UnipolarBrushCell::new();
2571 for _ in 0..1000 {
2572 n.step(10.0);
2573 }
2574 n.reset();
2575 assert_eq!(n.v, -65.0);
2576 assert_eq!(n.persistent, 0.0);
2577 }
2578
2579 #[test]
2580 fn ubc_performance_10k_steps() {
2581 let start = std::time::Instant::now();
2582 let mut n = UnipolarBrushCell::new();
2583 for _ in 0..10_000 {
2584 std::hint::black_box(n.step(5.0));
2585 }
2586 let elapsed = start.elapsed();
2587 assert!(elapsed.as_millis() < 50, "10k steps must complete in <50ms");
2588 }
2589
2590 #[test]
2593 fn dcn_fires_with_input() {
2594 let mut n = DCNNeuron::new();
2595 let mut spikes = 0;
2596 for _ in 0..2_000 {
2597 spikes += n.step(5.0);
2598 }
2599 assert!(
2600 spikes > 3,
2601 "DCN must fire with excitatory input, got {spikes}"
2602 );
2603 }
2604
2605 #[test]
2606 fn dcn_spontaneous_activity() {
2607 let mut n = DCNNeuron::new();
2610 let mut spikes = 0;
2611 for _ in 0..20_000 {
2612 spikes += n.step(0.0);
2613 }
2614 let mut no_nap = DCNNeuron::new();
2617 no_nap.g_nap = 0.0;
2618 let mut spikes_no = 0;
2619 for _ in 0..20_000 {
2620 spikes_no += no_nap.step(0.0);
2621 }
2622 assert!(
2623 spikes >= spikes_no,
2624 "INaP should contribute to spontaneous firing: with={spikes}, without={spikes_no}"
2625 );
2626 }
2627
2628 #[test]
2629 fn dcn_rebound_burst() {
2630 let mut n = DCNNeuron::new();
2632 for _ in 0..2000 {
2634 n.step(-5.0);
2635 }
2636 assert!(
2637 n.s > 0.5,
2638 "T-type must de-inactivate during hyperpolarisation, s={}",
2639 n.s
2640 );
2641
2642 let mut spikes = 0;
2644 for _ in 0..200 {
2645 spikes += n.step(3.0);
2646 }
2647 let mut n2 = DCNNeuron::new();
2649 n2.s = 0.05; let mut spikes2 = 0;
2651 for _ in 0..200 {
2652 spikes2 += n2.step(3.0);
2653 }
2654 assert!(
2655 spikes >= spikes2,
2656 "De-inactivated T-type should facilitate rebound: rebound={spikes} vs inact={spikes2}"
2657 );
2658 }
2659
2660 #[test]
2661 fn dcn_ih_depolarises() {
2662 let mut with_ih = DCNNeuron::new();
2664 with_ih.v = -80.0;
2665 let mut no_ih = DCNNeuron::new();
2666 no_ih.v = -80.0;
2667 no_ih.g_h = 0.0;
2668
2669 for _ in 0..1000 {
2670 with_ih.step(0.0);
2671 no_ih.step(0.0);
2672 }
2673 assert!(
2674 with_ih.v > no_ih.v,
2675 "Ih should depolarise from hyperpolarised state: Ih={:.1} vs no_Ih={:.1}",
2676 with_ih.v,
2677 no_ih.v
2678 );
2679 }
2680
2681 #[test]
2682 fn dcn_gate_and_calcium_kinetics_use_closed_form_relaxation() {
2683 let mut n = DCNNeuron::new();
2684 n.g_na = 0.0;
2685 n.g_nap = 0.0;
2686 n.g_k = 0.0;
2687 n.g_t = 0.0;
2688 n.g_ahp = 0.0;
2689 n.g_h = 0.0;
2690 n.g_l = 0.0;
2691 n.gain = 0.0;
2692 let (v0, h0, n0, p0, s0, r0, ca0) = (n.v, n.h, n.n, n.p, n.s, n.r, n.ca);
2693 let alpha_h = 0.07 * (-(v0 + 58.0) / 20.0).exp();
2694 let beta_h = 1.0 / (1.0 + (-(v0 + 28.0) / 10.0).exp());
2695 let alpha_n = safe_rate(0.01, 34.0, v0, 10.0, 0.1);
2696 let beta_n = 0.125 * (-(v0 + 44.0) / 80.0).exp();
2697 let p_inf = 1.0 / (1.0 + (-(v0 + 48.0) / 5.0).exp());
2698 let tau_p = 5.0 + 15.0 / (1.0 + ((v0 + 48.0) / 10.0).powi(2)).max(0.01);
2699 let s_inf = 1.0 / (1.0 + ((v0 + 60.0) / 6.5).exp());
2700 let tau_s = 20.0 + 50.0 / (1.0 + ((v0 + 65.0) / 10.0).exp());
2701 let r_inf = 1.0 / (1.0 + ((v0 + 80.0) / 10.0).exp());
2702 let tau_r = 100.0 + 200.0 / (1.0 + ((v0 + 70.0) / 10.0).exp());
2703
2704 n.step(0.0);
2705
2706 assert_close(n.v, v0);
2707 assert_close(n.h, dcn_exact_hh_gate(h0, alpha_h, beta_h, n.phi, n.dt));
2708 assert_close(n.n, dcn_exact_hh_gate(n0, alpha_n, beta_n, n.phi, n.dt));
2709 assert_close(n.p, dcn_exact_relax(p0, p_inf, tau_p, n.dt));
2710 assert_close(n.s, dcn_exact_relax(s0, s_inf, tau_s, n.dt));
2711 assert_close(n.r, dcn_exact_relax(r0, r_inf, tau_r, n.dt));
2712 assert_close(n.ca, dcn_exact_relax(ca0, 0.0, n.tau_ca, n.dt));
2713 }
2714
2715 #[test]
2716 fn dcn_negative_input_no_crash() {
2717 let mut n = DCNNeuron::new();
2718 for _ in 0..10_000 {
2719 n.step(-100.0);
2720 }
2721 assert!(n.v.is_finite());
2722 assert!(n.v >= -100.0);
2723 }
2724
2725 #[test]
2726 fn dcn_nan_input_stays_finite() {
2727 let mut n = DCNNeuron::new();
2728 let before = n.clone();
2729 n.step(f64::NAN);
2730 assert!(n.v.is_finite());
2731 assert_eq!(n.v, before.v);
2732 assert_eq!(n.ca, before.ca);
2733 }
2734
2735 #[test]
2736 fn dcn_extreme_input_bounded() {
2737 let mut n = DCNNeuron::new();
2738 for _ in 0..1000 {
2739 n.step(1e6);
2740 }
2741 assert!(n.v.is_finite() && n.v <= 60.0);
2742 }
2743
2744 #[test]
2745 fn dcn_corrupted_state_preserved_on_step() {
2746 let mut n = DCNNeuron::new();
2747 n.h = -0.1;
2748 let before_v = n.v;
2749 let before_ca = n.ca;
2750 assert_eq!(n.step(10.0), 0);
2751 assert_eq!(n.v, before_v);
2752 assert_eq!(n.ca, before_ca);
2753 }
2754
2755 #[test]
2756 fn dcn_reset_clears_state() {
2757 let mut n = DCNNeuron::new();
2758 for _ in 0..1000 {
2759 n.step(10.0);
2760 }
2761 n.reset();
2762 assert_eq!(n.v, -60.0);
2763 assert_eq!(n.s, 0.8);
2764 assert_eq!(n.r, 0.1);
2765 }
2766
2767 #[test]
2768 fn dcn_gates_bounded() {
2769 let mut n = DCNNeuron::new();
2770 for _ in 0..10_000 {
2771 n.step(10.0);
2772 }
2773 for (name, val) in [("h", n.h), ("n", n.n), ("p", n.p), ("s", n.s), ("r", n.r)] {
2774 assert!((0.0..=1.0).contains(&val), "{name} out of bounds: {val}");
2775 }
2776 assert!(n.ca >= 0.0, "Ca²⁺ must be non-negative: {}", n.ca);
2777 }
2778
2779 #[test]
2780 fn dcn_nap_increases_excitability() {
2781 let mut with_nap = DCNNeuron::new();
2783 let mut no_nap = DCNNeuron::new();
2784 no_nap.g_nap = 0.0;
2785 let mut spikes_with = 0;
2786 let mut spikes_no = 0;
2787 for _ in 0..5_000 {
2788 spikes_with += with_nap.step(3.0);
2789 spikes_no += no_nap.step(3.0);
2790 }
2791 assert!(
2792 spikes_with >= spikes_no,
2793 "INaP should increase excitability: with={spikes_with}, without={spikes_no}"
2794 );
2795 }
2796
2797 #[test]
2798 fn dcn_ahp_limits_rate() {
2799 let mut with_ahp = DCNNeuron::new();
2801 let mut no_ahp = DCNNeuron::new();
2802 no_ahp.g_ahp = 0.0;
2803 let mut spikes_with = 0;
2804 let mut spikes_no = 0;
2805 for _ in 0..5_000 {
2806 spikes_with += with_ahp.step(8.0);
2807 spikes_no += no_ahp.step(8.0);
2808 }
2809 assert!(
2810 spikes_no >= spikes_with,
2811 "AHP removal should increase firing: with={spikes_with}, without={spikes_no}"
2812 );
2813 }
2814
2815 #[test]
2816 fn dcn_ca_rises_during_spiking() {
2817 let mut n = DCNNeuron::new();
2818 let ca_init = n.ca;
2819 for _ in 0..5_000 {
2820 n.step(10.0);
2821 }
2822 assert!(
2823 n.ca > ca_init,
2824 "Ca²⁺ must rise during spiking: init={ca_init}, now={}",
2825 n.ca
2826 );
2827 }
2828
2829 #[test]
2830 fn dcn_has_seven_currents() {
2831 let n = DCNNeuron::new();
2833 assert!(n.g_na > 0.0, "Na_t missing");
2834 assert!(n.g_nap > 0.0, "Na_p missing");
2835 assert!(n.g_k > 0.0, "K_dr missing");
2836 assert!(n.g_t > 0.0, "Ca_T missing");
2837 assert!(n.g_ahp > 0.0, "AHP missing");
2838 assert!(n.g_h > 0.0, "Ih missing");
2839 assert!(n.g_l > 0.0, "Leak missing");
2840 }
2841
2842 #[test]
2843 fn dcn_performance_1k_steps() {
2844 let start = std::time::Instant::now();
2845 let mut n = DCNNeuron::new();
2846 for _ in 0..1_000 {
2847 std::hint::black_box(n.step(5.0));
2848 }
2849 let elapsed = start.elapsed();
2850 assert!(
2851 elapsed.as_millis() < 200,
2852 "1k steps must complete in <200ms"
2853 );
2854 }
2855
2856 fn assert_close(observed: f64, expected: f64) {
2857 assert!(
2858 (observed - expected).abs() <= 1.0e-12,
2859 "observed {:.17e}, expected {:.17e}",
2860 observed,
2861 expected,
2862 );
2863 }
2864}