1use super::biophysical::safe_rate;
15
16#[derive(Clone, Debug)]
37pub struct AlphaMotorNeuron {
38 pub v: f64,
39 pub h: f64,
40 pub n: f64,
41 pub m_pic: f64, pub h_pic: f64, pub ca: f64, pub ca_buf: f64, pub g_na: f64,
47 pub g_k: f64,
48 pub g_pic: f64, pub g_ahp: f64, pub g_l: f64,
51 pub e_na: f64,
53 pub e_k: f64,
54 pub e_ca: f64,
55 pub e_l: f64,
56 pub c_m: f64,
57 pub phi: f64,
58 pub tau_ca: f64, pub buf_ratio: f64, pub dt: f64,
61 pub v_threshold: f64,
62}
63
64impl AlphaMotorNeuron {
65 pub fn new() -> Self {
66 Self {
67 v: -65.0,
68 h: 0.8,
69 n: 0.1,
70 m_pic: 0.0,
71 h_pic: 1.0, ca: 0.0,
73 ca_buf: 0.0,
74 g_na: 35.0,
75 g_k: 9.0,
76 g_pic: 0.15, g_ahp: 3.0, g_l: 0.3, e_na: 55.0,
80 e_k: -90.0,
81 e_ca: 120.0,
82 e_l: -65.0,
83 c_m: 1.5, phi: 4.0,
85 tau_ca: 150.0, buf_ratio: 0.003, dt: 0.01,
88 v_threshold: -20.0,
89 }
90 }
91
92 pub fn step(&mut self, current: f64) -> i32 {
93 let v_prev = self.v;
94 let n_sub = (0.5 / self.dt.max(0.001)) as usize;
95 for _ in 0..n_sub {
96 let am = safe_rate(0.1, 35.0, self.v, 10.0, 1.0);
98 let bm = 4.0 * (-(self.v + 60.0) / 18.0).exp();
99 let m_inf = am / (am + bm);
100 let ah = 0.07 * (-(self.v + 58.0) / 20.0).exp();
101 let bh = 1.0 / (1.0 + (-(self.v + 28.0) / 10.0).exp());
102 let an = safe_rate(0.01, 34.0, self.v, 10.0, 0.1);
103 let bn = 0.125 * (-(self.v + 44.0) / 80.0).exp();
104
105 self.h += self.phi * (ah * (1.0 - self.h) - bh * self.h) * self.dt;
106 self.n += self.phi * (an * (1.0 - self.n) - bn * self.n) * self.dt;
107
108 let m_pic_inf = 1.0 / (1.0 + (-(self.v + 40.0) / 5.0).exp());
111 self.m_pic += (m_pic_inf - self.m_pic) / 50.0 * self.dt;
112 let h_pic_inf = 1.0 / (1.0 + ((self.v + 40.0) / 8.0).exp());
115 let tau_h_pic = 200.0 + 100.0 / (1.0 + ((self.v + 40.0) / 10.0).powi(2)).max(0.01);
116 self.h_pic += (h_pic_inf - self.h_pic) / tau_h_pic * self.dt;
117 self.h_pic = self.h_pic.clamp(0.0, 1.0);
118
119 let i_ca_entry = self.g_pic * self.m_pic * self.h_pic * (self.v - self.e_ca);
122 let ca_influx = if i_ca_entry < 0.0 {
123 -i_ca_entry * 0.001
124 } else {
125 0.0
126 };
127 let ca_spike = if self.v > -10.0 { 0.02 } else { 0.0 };
128 let free_ca_change = (ca_influx + ca_spike) * self.buf_ratio;
130 self.ca += (-self.ca / self.tau_ca + free_ca_change) * self.dt;
131 if self.ca < 0.0 {
132 self.ca = 0.0;
133 }
134 self.ca_buf += ((ca_influx + ca_spike) * (1.0 - self.buf_ratio)
136 - self.ca_buf / (self.tau_ca * 5.0))
137 * self.dt;
138 if self.ca_buf < 0.0 {
139 self.ca_buf = 0.0;
140 }
141
142 let ca_total = self.ca + self.ca_buf * 0.01; let ahp_inf = ca_total * ca_total / (ca_total * ca_total + 0.25);
145
146 let i_na = self.g_na * m_inf.powi(3) * self.h * (self.v - self.e_na);
147 let i_k = self.g_k * self.n.powi(4) * (self.v - self.e_k);
148 let i_pic = self.g_pic * self.m_pic * self.h_pic * (self.v - self.e_ca);
149 let i_ahp = self.g_ahp * ahp_inf * (self.v - self.e_k);
150 let i_l = self.g_l * (self.v - self.e_l);
151
152 self.v += (-i_na - i_k - i_pic - i_ahp - i_l + current) / self.c_m * self.dt;
153 }
154 if self.v >= self.v_threshold && v_prev < self.v_threshold {
155 1
156 } else {
157 0
158 }
159 }
160
161 pub fn reset(&mut self) {
162 *self = Self::new();
163 }
164}
165
166impl Default for AlphaMotorNeuron {
167 fn default() -> Self {
168 Self::new()
169 }
170}
171
172#[derive(Clone, Debug)]
187pub struct GammaMotorNeuron {
188 pub v: f64,
189 pub v_rest: f64,
190 pub v_reset: f64,
191 pub v_threshold: f64,
192 pub tau: f64,
193 pub adapt: f64, pub tau_adapt: f64, pub a_adapt: f64, pub gain: f64, pub dynamic: bool, pub dt: f64,
199}
200
201impl GammaMotorNeuron {
202 pub fn new() -> Self {
203 Self::dynamic()
204 }
205
206 pub fn dynamic() -> Self {
208 Self {
209 v: -65.0,
210 v_rest: -65.0,
211 v_reset: -70.0,
212 v_threshold: -50.0,
213 tau: 8.0,
214 adapt: 0.0,
215 tau_adapt: 100.0,
216 a_adapt: 0.3,
217 gain: 1.0,
218 dynamic: true,
219 dt: 0.5,
220 }
221 }
222
223 pub fn static_type() -> Self {
225 Self {
226 tau: 12.0, tau_adapt: 200.0, a_adapt: 0.5,
229 dynamic: false,
230 ..Self::dynamic()
231 }
232 }
233
234 pub fn step(&mut self, drive: f64) -> i32 {
236 let input = self.gain * drive.max(0.0) - self.adapt;
237 self.v += (-(self.v - self.v_rest) + input) / self.tau * self.dt;
238 self.adapt +=
239 (self.a_adapt * (self.v - self.v_rest) - self.adapt) / self.tau_adapt * self.dt;
240
241 if self.v >= self.v_threshold {
242 self.v = self.v_reset;
243 1
244 } else {
245 0
246 }
247 }
248
249 pub fn reset(&mut self) {
250 self.v = self.v_rest;
251 self.adapt = 0.0;
252 }
253}
254
255impl Default for GammaMotorNeuron {
256 fn default() -> Self {
257 Self::new()
258 }
259}
260
261#[derive(Clone, Debug)]
275pub struct UpperMotorNeuron {
276 pub v: f64,
277 pub m: f64,
278 pub h: f64,
279 pub n: f64,
280 pub p: f64, pub s: f64, pub g_na: f64,
284 pub g_k: f64,
285 pub g_m: f64,
286 pub g_ca: f64,
287 pub g_l: f64,
288 pub e_na: f64,
290 pub e_k: f64,
291 pub e_ca: f64,
292 pub e_l: f64,
293 pub c_m: f64,
294 pub dt: f64,
295 pub v_threshold: f64,
296}
297
298impl UpperMotorNeuron {
299 pub fn new() -> Self {
300 Self {
301 v: -70.0,
302 m: 0.05,
303 h: 0.6,
304 n: 0.3,
305 p: 0.0,
306 s: 0.0,
307 g_na: 50.0,
308 g_k: 5.0,
309 g_m: 0.07, g_ca: 0.3, g_l: 0.1,
312 e_na: 50.0,
313 e_k: -90.0,
314 e_ca: 120.0,
315 e_l: -70.0,
316 c_m: 1.0,
317 dt: 0.025,
318 v_threshold: -20.0,
319 }
320 }
321
322 pub fn step(&mut self, current: f64) -> i32 {
323 let v_prev = self.v;
324 let vt = -56.2;
325 for _ in 0..4 {
326 let dv = self.v - vt;
328 let x_m = dv - 13.0;
329 let alpha_m = if x_m.abs() < 1e-6 {
330 0.32 * 4.0
331 } else {
332 -0.32 * x_m / ((-x_m / 4.0).exp() - 1.0)
333 };
334 let x_h = dv - 17.0;
335 let beta_m = if x_h.abs() < 1e-6 {
336 0.28 * 5.0
337 } else {
338 0.28 * x_h / ((x_h / 5.0).exp() - 1.0)
339 };
340 let alpha_h = 0.128 * (-(dv - 17.0) / 18.0).exp();
341 let beta_h = 4.0 / (1.0 + (-(dv - 40.0) / 5.0).exp());
342 let x_n = dv - 15.0;
344 let alpha_n = if x_n.abs() < 1e-6 {
345 0.032 * 5.0
346 } else {
347 -0.032 * x_n / ((-x_n / 5.0).exp() - 1.0)
348 };
349 let beta_n = 0.5 * (-(dv - 10.0) / 40.0).exp();
350
351 self.m += (alpha_m * (1.0 - self.m) - beta_m * self.m) * self.dt;
352 self.h += (alpha_h * (1.0 - self.h) - beta_h * self.h) * self.dt;
353 self.n += (alpha_n * (1.0 - self.n) - beta_n * self.n) * self.dt;
354
355 let p_inf = 1.0 / (1.0 + (-(self.v + 35.0) / 10.0).exp());
357 let tau_p =
358 400.0 / (3.3 * ((self.v + 35.0) / 20.0).exp() + (-(self.v + 35.0) / 20.0).exp());
359 self.p += (p_inf - self.p) / tau_p * self.dt;
360
361 let s_inf = 1.0 / (1.0 + (-(self.v + 20.0) / 5.0).exp());
363 self.s += (s_inf - self.s) / 10.0 * self.dt;
364
365 let i_na = self.g_na * self.m.powi(3) * self.h * (self.v - self.e_na);
366 let i_k = self.g_k * self.n.powi(4) * (self.v - self.e_k);
367 let i_m = self.g_m * self.p * (self.v - self.e_k);
368 let i_ca = self.g_ca * self.s.powi(2) * (self.v - self.e_ca);
369 let i_l = self.g_l * (self.v - self.e_l);
370
371 self.v += (-i_na - i_k - i_m - i_ca - i_l + current) / self.c_m * self.dt;
372 }
373 if self.v >= self.v_threshold && v_prev < self.v_threshold {
374 1
375 } else {
376 0
377 }
378 }
379
380 pub fn reset(&mut self) {
381 self.v = -70.0;
382 self.m = 0.05;
383 self.h = 0.6;
384 self.n = 0.3;
385 self.p = 0.0;
386 self.s = 0.0;
387 }
388}
389
390impl Default for UpperMotorNeuron {
391 fn default() -> Self {
392 Self::new()
393 }
394}
395
396#[derive(Clone, Debug)]
412pub struct RenshawCell {
413 pub v: f64,
414 pub h: f64,
415 pub n: f64,
416 pub adapt: f64,
417 pub g_na: f64,
418 pub g_k: f64,
419 pub g_adapt: f64,
420 pub g_l: f64,
421 pub e_na: f64,
422 pub e_k: f64,
423 pub e_l: f64,
424 pub c_m: f64,
425 pub phi: f64,
426 pub tau_adapt: f64,
427 pub dt: f64,
428 pub v_threshold: f64,
429}
430
431impl RenshawCell {
432 pub fn new() -> Self {
433 Self {
434 v: -65.0,
435 h: 0.8,
436 n: 0.1,
437 adapt: 0.0,
438 g_na: 35.0,
439 g_k: 9.0,
440 g_adapt: 5.0,
441 g_l: 0.12,
442 e_na: 55.0,
443 e_k: -90.0,
444 e_l: -65.0,
445 c_m: 1.0,
446 phi: 5.0,
447 tau_adapt: 50.0,
448 dt: 0.01,
449 v_threshold: -20.0,
450 }
451 }
452
453 pub fn step(&mut self, current: f64) -> i32 {
454 let v_prev = self.v;
455 let n_sub = (0.5 / self.dt.max(0.001)) as usize;
456 for _ in 0..n_sub {
457 let am = safe_rate(0.1, 35.0, self.v, 10.0, 1.0);
458 let bm = 4.0 * (-(self.v + 60.0) / 18.0).exp();
459 let m_inf = am / (am + bm);
460 let ah = 0.07 * (-(self.v + 58.0) / 20.0).exp();
461 let bh = 1.0 / (1.0 + (-(self.v + 28.0) / 10.0).exp());
462 let an = safe_rate(0.01, 34.0, self.v, 10.0, 0.1);
463 let bn = 0.125 * (-(self.v + 44.0) / 80.0).exp();
464
465 self.h += self.phi * (ah * (1.0 - self.h) - bh * self.h) * self.dt;
466 self.n += self.phi * (an * (1.0 - self.n) - bn * self.n) * self.dt;
467
468 let adapt_inf = 1.0 / (1.0 + (-(self.v + 30.0) / 5.0).exp());
469 self.adapt += (adapt_inf - self.adapt) / self.tau_adapt * self.dt;
470
471 let i_na = self.g_na * m_inf.powi(3) * self.h * (self.v - self.e_na);
472 let i_k = self.g_k * self.n.powi(4) * (self.v - self.e_k);
473 let i_adapt = self.g_adapt * self.adapt * (self.v - self.e_k);
474 let i_l = self.g_l * (self.v - self.e_l);
475
476 self.v += (-i_na - i_k - i_adapt - i_l + current) / self.c_m * self.dt;
477 }
478 if self.v >= self.v_threshold && v_prev < self.v_threshold {
479 1
480 } else {
481 0
482 }
483 }
484
485 pub fn reset(&mut self) {
486 self.v = -65.0;
487 self.h = 0.8;
488 self.n = 0.1;
489 self.adapt = 0.0;
490 }
491}
492
493impl Default for RenshawCell {
494 fn default() -> Self {
495 Self::new()
496 }
497}
498
499#[derive(Clone, Debug)]
515pub struct MotorUnit {
516 pub v: f64,
517 pub v_rest: f64,
518 pub v_reset: f64,
519 pub v_threshold: f64,
520 pub tau_m: f64, pub adapt: f64,
522 pub tau_adapt: f64,
523 pub a_adapt: f64,
524 pub gain: f64,
525 pub force: f64, pub twitch_amp: f64, pub tau_twitch: f64, pub force_decay: f64, pub dt: f64,
531}
532
533impl MotorUnit {
534 pub fn new() -> Self {
535 Self::slow()
536 }
537
538 pub fn slow() -> Self {
540 Self {
541 v: -65.0,
542 v_rest: -65.0,
543 v_reset: -70.0,
544 v_threshold: -50.0,
545 tau_m: 10.0,
546 adapt: 0.0,
547 tau_adapt: 100.0,
548 a_adapt: 0.2,
549 gain: 1.0,
550 force: 0.0,
551 twitch_amp: 0.05,
552 tau_twitch: 90.0,
553 force_decay: 0.0,
554 dt: 0.5,
555 }
556 }
557
558 pub fn fast() -> Self {
560 Self {
561 tau_m: 6.0,
562 tau_adapt: 50.0,
563 a_adapt: 0.1,
564 twitch_amp: 0.3,
565 tau_twitch: 30.0,
566 ..Self::slow()
567 }
568 }
569
570 pub fn step(&mut self, drive: f64) -> i32 {
572 let input = self.gain * drive.max(0.0) - self.adapt;
573 self.v += (-(self.v - self.v_rest) + input) / self.tau_m * self.dt;
574 self.adapt +=
575 (self.a_adapt * (self.v - self.v_rest) - self.adapt) / self.tau_adapt * self.dt;
576
577 self.force *= (-self.dt / self.tau_twitch).exp();
579
580 if self.v >= self.v_threshold {
581 self.v = self.v_reset;
582 self.force += self.twitch_amp;
584 if self.force > 1.0 {
585 self.force = 1.0;
586 }
587 1
588 } else {
589 0
590 }
591 }
592
593 pub fn reset(&mut self) {
594 self.v = self.v_rest;
595 self.adapt = 0.0;
596 self.force = 0.0;
597 }
598}
599
600impl Default for MotorUnit {
601 fn default() -> Self {
602 Self::new()
603 }
604}
605
606#[cfg(test)]
611mod tests {
612 use super::*;
613
614 #[test]
617 fn alpha_motor_fires_with_input() {
618 let mut n = AlphaMotorNeuron::new();
619 let spikes: i32 = (0..5000).map(|_| n.step(3.0)).sum();
620 assert!(
621 spikes > 0,
622 "alpha motor must fire with sustained input: got {spikes}"
623 );
624 }
625
626 #[test]
627 fn alpha_motor_no_fire_without_input() {
628 let mut n = AlphaMotorNeuron::new();
629 let spikes: i32 = (0..3000).map(|_| n.step(0.0)).sum();
630 assert_eq!(spikes, 0, "alpha motor should not fire at rest");
631 }
632
633 #[test]
634 fn alpha_motor_negative_current_no_fire() {
635 let mut n = AlphaMotorNeuron::new();
636 let spikes: i32 = (0..2000).map(|_| n.step(-2.0)).sum();
637 assert_eq!(spikes, 0);
638 }
639
640 #[test]
641 fn alpha_motor_ahp_limits_rate() {
642 let mut with_ahp = AlphaMotorNeuron::new();
645 let mut no_ahp = AlphaMotorNeuron::new();
646 no_ahp.g_ahp = 0.0;
647 let s_ahp: i32 = (0..5000).map(|_| with_ahp.step(5.0)).sum();
648 let s_none: i32 = (0..5000).map(|_| no_ahp.step(5.0)).sum();
649 assert!(
650 s_ahp <= s_none + 5,
651 "AHP should limit rate: with={s_ahp}, without={s_none}"
652 );
653 }
654
655 #[test]
656 fn alpha_motor_pic_responds_to_depolarisation() {
657 let mut n = AlphaMotorNeuron::new();
659 let baseline = n.m_pic;
660 for _ in 0..2000 {
661 n.step(4.0);
662 }
663 assert!(
664 n.m_pic > baseline + 0.001,
665 "PIC should respond to depolarisation: baseline={baseline}, after={}",
666 n.m_pic
667 );
668 }
669
670 #[test]
671 fn alpha_motor_ca_increases_during_spiking() {
672 let mut n = AlphaMotorNeuron::new();
673 for _ in 0..5000 {
674 n.step(5.0);
675 }
676 assert!(
677 n.ca > 0.0,
678 "Ca2+ should accumulate during spiking: ca={}",
679 n.ca
680 );
681 }
682
683 #[test]
684 fn alpha_motor_reset_roundtrip() {
685 let mut n = AlphaMotorNeuron::new();
686 for _ in 0..2000 {
687 n.step(4.0);
688 }
689 n.reset();
690 let mut fresh = AlphaMotorNeuron::new();
691 let r1: i32 = (0..1000).map(|_| n.step(4.0)).sum();
692 let r2: i32 = (0..1000).map(|_| fresh.step(4.0)).sum();
693 assert_eq!(r1, r2, "reset neuron must match fresh");
694 }
695
696 #[test]
697 fn alpha_motor_voltage_bounded() {
698 let mut n = AlphaMotorNeuron::new();
699 for _ in 0..10000 {
700 n.step(10.0);
701 }
702 assert!(n.v.is_finite(), "voltage must stay finite");
703 assert!(n.ca.is_finite(), "Ca2+ must stay finite");
704 assert!(n.ca >= 0.0, "Ca2+ must be non-negative");
705 }
706
707 #[test]
708 fn alpha_motor_nan_recovery() {
709 let mut n = AlphaMotorNeuron::new();
710 for _ in 0..100 {
711 n.step(3.0);
712 }
713 for _ in 0..10 {
714 let _ = n.step(f64::NAN);
715 }
716 n.reset();
717 assert!(n.v.is_finite());
718 assert!(n.ca >= 0.0);
719 }
720
721 #[test]
722 fn alpha_motor_extreme_input() {
723 let mut n = AlphaMotorNeuron::new();
724 for _ in 0..50 {
725 n.step(1e6);
726 }
727 n.reset();
728 assert!(n.v.is_finite());
729 for _ in 0..50 {
730 n.step(-1e6);
731 }
732 n.reset();
733 assert!(n.v.is_finite());
734 }
735
736 #[test]
737 fn alpha_motor_performance() {
738 let mut n = AlphaMotorNeuron::new();
739 let start = std::time::Instant::now();
740 for _ in 0..5_000 {
741 n.step(4.0);
742 }
743 assert!(
744 start.elapsed().as_millis() < 500,
745 "5k steps took {:?}",
746 start.elapsed()
747 );
748 }
749
750 #[test]
753 fn gamma_dynamic_fires_with_drive() {
754 let mut n = GammaMotorNeuron::dynamic();
755 let spikes: i32 = (0..2000).map(|_| n.step(20.0)).sum();
756 assert!(spikes > 0, "gamma dynamic must fire: got {spikes}");
757 }
758
759 #[test]
760 fn gamma_static_fires_with_drive() {
761 let mut n = GammaMotorNeuron::static_type();
762 let spikes: i32 = (0..2000).map(|_| n.step(20.0)).sum();
763 assert!(spikes > 0, "gamma static must fire: got {spikes}");
764 }
765
766 #[test]
767 fn gamma_no_fire_without_drive() {
768 let mut n = GammaMotorNeuron::new();
769 let spikes: i32 = (0..1000).map(|_| n.step(0.0)).sum();
770 assert_eq!(spikes, 0);
771 }
772
773 #[test]
774 fn gamma_negative_drive_no_fire() {
775 let mut n = GammaMotorNeuron::new();
776 let spikes: i32 = (0..1000).map(|_| n.step(-10.0)).sum();
778 assert_eq!(spikes, 0);
779 }
780
781 #[test]
782 fn gamma_adaptation_reduces_rate() {
783 let mut n = GammaMotorNeuron::new();
784 let first: i32 = (0..1000).map(|_| n.step(20.0)).sum();
785 let second: i32 = (0..1000).map(|_| n.step(20.0)).sum();
786 assert!(
787 second <= first + 3,
788 "gamma should adapt: first={first}, second={second}"
789 );
790 }
791
792 #[test]
793 fn gamma_static_adapts_more_than_dynamic() {
794 let mut dyn_ = GammaMotorNeuron::dynamic();
795 let mut stat = GammaMotorNeuron::static_type();
796 let dyn_spikes: i32 = (0..2000).map(|_| dyn_.step(20.0)).sum();
797 let stat_spikes: i32 = (0..2000).map(|_| stat.step(20.0)).sum();
798 assert!(
800 stat_spikes <= dyn_spikes + 5,
801 "static ({stat_spikes}) should fire <= dynamic ({dyn_spikes})"
802 );
803 }
804
805 #[test]
806 fn gamma_reset_roundtrip() {
807 let mut n = GammaMotorNeuron::new();
808 for _ in 0..1000 {
809 n.step(20.0);
810 }
811 n.reset();
812 let mut fresh = GammaMotorNeuron::new();
813 let r1: i32 = (0..500).map(|_| n.step(20.0)).sum();
814 let r2: i32 = (0..500).map(|_| fresh.step(20.0)).sum();
815 assert_eq!(r1, r2);
816 }
817
818 #[test]
819 fn gamma_voltage_bounded() {
820 let mut n = GammaMotorNeuron::new();
821 for _ in 0..10000 {
822 n.step(50.0);
823 }
824 assert!(n.v.is_finite());
825 assert!(n.adapt.is_finite());
826 }
827
828 #[test]
829 fn gamma_nan_recovery() {
830 let mut n = GammaMotorNeuron::new();
831 for _ in 0..50 {
832 n.step(20.0);
833 }
834 for _ in 0..10 {
835 let _ = n.step(f64::NAN);
836 }
837 n.reset();
838 assert!(n.v.is_finite());
839 assert_eq!(n.adapt, 0.0);
840 }
841
842 #[test]
843 fn gamma_extreme_input() {
844 let mut n = GammaMotorNeuron::new();
845 for _ in 0..50 {
846 n.step(1e6);
847 }
848 n.reset();
849 assert!(n.v.is_finite());
850 }
851
852 #[test]
853 fn gamma_performance() {
854 let mut n = GammaMotorNeuron::new();
855 let start = std::time::Instant::now();
856 for _ in 0..100_000 {
857 n.step(20.0);
858 }
859 assert!(
860 start.elapsed().as_millis() < 50,
861 "100k steps took {:?}",
862 start.elapsed()
863 );
864 }
865
866 #[test]
869 fn upper_motor_fires_with_input() {
870 let mut n = UpperMotorNeuron::new();
871 let spikes: i32 = (0..10000).map(|_| n.step(5.0)).sum();
872 assert!(spikes > 0, "upper motor must fire: got {spikes}");
873 }
874
875 #[test]
876 fn upper_motor_no_fire_without_input() {
877 let mut n = UpperMotorNeuron::new();
878 let spikes: i32 = (0..5000).map(|_| n.step(0.0)).sum();
879 assert_eq!(spikes, 0);
880 }
881
882 #[test]
883 fn upper_motor_negative_current_no_fire() {
884 let mut n = UpperMotorNeuron::new();
885 let spikes: i32 = (0..2000).map(|_| n.step(-5.0)).sum();
886 assert_eq!(spikes, 0);
887 }
888
889 #[test]
890 fn upper_motor_adaptation_via_m_current() {
891 let mut n = UpperMotorNeuron::new();
892 let first: i32 = (0..5000).map(|_| n.step(5.0)).sum();
893 let second: i32 = (0..5000).map(|_| n.step(5.0)).sum();
894 assert!(
895 second <= first + 3,
896 "M-current should cause adaptation: first={first}, second={second}"
897 );
898 }
899
900 #[test]
901 fn upper_motor_ca_activates_during_depolarisation() {
902 let mut n = UpperMotorNeuron::new();
903 let baseline = n.s;
904 for _ in 0..5000 {
905 n.step(5.0);
906 }
907 assert!(
908 n.s > baseline + 0.001,
909 "Ca2+ gate should activate: s={}",
910 n.s
911 );
912 }
913
914 #[test]
915 fn upper_motor_reset_roundtrip() {
916 let mut n = UpperMotorNeuron::new();
917 for _ in 0..3000 {
918 n.step(5.0);
919 }
920 n.reset();
921 let mut fresh = UpperMotorNeuron::new();
922 let r1: i32 = (0..2000).map(|_| n.step(5.0)).sum();
923 let r2: i32 = (0..2000).map(|_| fresh.step(5.0)).sum();
924 assert_eq!(r1, r2);
925 }
926
927 #[test]
928 fn upper_motor_voltage_bounded() {
929 let mut n = UpperMotorNeuron::new();
930 for _ in 0..20000 {
931 n.step(10.0);
932 }
933 assert!(n.v.is_finite());
934 assert!(n.p.is_finite());
935 assert!(n.s.is_finite());
936 }
937
938 #[test]
939 fn upper_motor_nan_recovery() {
940 let mut n = UpperMotorNeuron::new();
941 for _ in 0..100 {
942 n.step(5.0);
943 }
944 for _ in 0..10 {
945 let _ = n.step(f64::NAN);
946 }
947 n.reset();
948 assert!(n.v.is_finite());
949 }
950
951 #[test]
952 fn upper_motor_extreme_input() {
953 let mut n = UpperMotorNeuron::new();
954 for _ in 0..50 {
955 n.step(1e6);
956 }
957 n.reset();
958 assert!(n.v.is_finite());
959 }
960
961 #[test]
962 fn upper_motor_performance() {
963 let mut n = UpperMotorNeuron::new();
964 let start = std::time::Instant::now();
965 for _ in 0..10_000 {
966 n.step(5.0);
967 }
968 assert!(
969 start.elapsed().as_millis() < 100,
970 "10k steps took {:?}",
971 start.elapsed()
972 );
973 }
974
975 #[test]
978 fn renshaw_fires_with_input() {
979 let mut n = RenshawCell::new();
980 let spikes: i32 = (0..5000).map(|_| n.step(3.0)).sum();
981 assert!(spikes > 0, "Renshaw must fire: got {spikes}");
982 }
983
984 #[test]
985 fn renshaw_no_fire_without_input() {
986 let mut n = RenshawCell::new();
987 let spikes: i32 = (0..3000).map(|_| n.step(0.0)).sum();
988 assert_eq!(spikes, 0);
989 }
990
991 #[test]
992 fn renshaw_negative_current_no_fire() {
993 let mut n = RenshawCell::new();
994 let spikes: i32 = (0..2000).map(|_| n.step(-2.0)).sum();
995 assert_eq!(spikes, 0);
996 }
997
998 #[test]
999 fn renshaw_burst_then_adapt() {
1000 let mut n = RenshawCell::new();
1002 let first: i32 = (0..2000).map(|_| n.step(4.0)).sum();
1003 let second: i32 = (0..2000).map(|_| n.step(4.0)).sum();
1004 assert!(
1005 second <= first + 5,
1006 "Renshaw should adapt: first={first}, second={second}"
1007 );
1008 }
1009
1010 #[test]
1011 fn renshaw_adapt_increases_during_firing() {
1012 let mut n = RenshawCell::new();
1013 let baseline = n.adapt;
1014 for _ in 0..3000 {
1015 n.step(4.0);
1016 }
1017 assert!(
1018 n.adapt > baseline + 0.01,
1019 "adaptation variable should increase: adapt={}",
1020 n.adapt
1021 );
1022 }
1023
1024 #[test]
1025 fn renshaw_reset_roundtrip() {
1026 let mut n = RenshawCell::new();
1027 for _ in 0..2000 {
1028 n.step(4.0);
1029 }
1030 n.reset();
1031 let mut fresh = RenshawCell::new();
1032 let r1: i32 = (0..1000).map(|_| n.step(4.0)).sum();
1033 let r2: i32 = (0..1000).map(|_| fresh.step(4.0)).sum();
1034 assert_eq!(r1, r2);
1035 }
1036
1037 #[test]
1038 fn renshaw_voltage_bounded() {
1039 let mut n = RenshawCell::new();
1040 for _ in 0..10000 {
1041 n.step(10.0);
1042 }
1043 assert!(n.v.is_finite());
1044 assert!(n.adapt.is_finite());
1045 }
1046
1047 #[test]
1048 fn renshaw_nan_recovery() {
1049 let mut n = RenshawCell::new();
1050 for _ in 0..100 {
1051 n.step(3.0);
1052 }
1053 for _ in 0..10 {
1054 let _ = n.step(f64::NAN);
1055 }
1056 n.reset();
1057 assert!(n.v.is_finite());
1058 assert_eq!(n.adapt, 0.0);
1059 }
1060
1061 #[test]
1062 fn renshaw_extreme_input() {
1063 let mut n = RenshawCell::new();
1064 for _ in 0..50 {
1065 n.step(1e6);
1066 }
1067 n.reset();
1068 assert!(n.v.is_finite());
1069 }
1070
1071 #[test]
1072 fn renshaw_performance() {
1073 let mut n = RenshawCell::new();
1074 let start = std::time::Instant::now();
1075 for _ in 0..5_000 {
1076 n.step(4.0);
1077 }
1078 assert!(
1079 start.elapsed().as_millis() < 500,
1080 "5k steps took {:?}",
1081 start.elapsed()
1082 );
1083 }
1084
1085 #[test]
1088 fn motor_unit_fires_with_drive() {
1089 let mut mu = MotorUnit::new();
1090 let spikes: i32 = (0..2000).map(|_| mu.step(20.0)).sum();
1091 assert!(spikes > 0, "motor unit must fire: got {spikes}");
1092 }
1093
1094 #[test]
1095 fn motor_unit_no_fire_without_drive() {
1096 let mut mu = MotorUnit::new();
1097 let spikes: i32 = (0..1000).map(|_| mu.step(0.0)).sum();
1098 assert_eq!(spikes, 0);
1099 }
1100
1101 #[test]
1102 fn motor_unit_negative_drive_no_fire() {
1103 let mut mu = MotorUnit::new();
1104 let spikes: i32 = (0..1000).map(|_| mu.step(-10.0)).sum();
1105 assert_eq!(spikes, 0);
1106 }
1107
1108 #[test]
1109 fn motor_unit_force_increases_with_spikes() {
1110 let mut mu = MotorUnit::new();
1111 assert_eq!(mu.force, 0.0);
1112 for _ in 0..2000 {
1113 mu.step(20.0);
1114 }
1115 assert!(
1116 mu.force > 0.0,
1117 "force should increase during spiking: f={}",
1118 mu.force
1119 );
1120 }
1121
1122 #[test]
1123 fn motor_unit_force_decays_without_input() {
1124 let mut mu = MotorUnit::new();
1125 for _ in 0..1000 {
1127 mu.step(20.0);
1128 }
1129 let peak = mu.force;
1130 assert!(peak > 0.0);
1131 for _ in 0..5000 {
1133 mu.step(0.0);
1134 }
1135 assert!(
1136 mu.force < peak,
1137 "force should decay: peak={peak}, now={}",
1138 mu.force
1139 );
1140 }
1141
1142 #[test]
1143 fn motor_unit_fast_produces_more_force() {
1144 let mut slow = MotorUnit::slow();
1145 let mut fast = MotorUnit::fast();
1146 for _ in 0..2000 {
1147 slow.step(20.0);
1148 fast.step(20.0);
1149 }
1150 assert!(
1151 fast.force >= slow.force,
1152 "fast MU ({}) should produce >= force than slow ({})",
1153 fast.force,
1154 slow.force
1155 );
1156 }
1157
1158 #[test]
1159 fn motor_unit_force_capped_at_one() {
1160 let mut mu = MotorUnit::fast();
1161 for _ in 0..10000 {
1162 mu.step(50.0);
1163 }
1164 assert!(mu.force <= 1.0, "force must not exceed 1.0: f={}", mu.force);
1165 }
1166
1167 #[test]
1168 fn motor_unit_reset_roundtrip() {
1169 let mut mu = MotorUnit::new();
1170 for _ in 0..1000 {
1171 mu.step(20.0);
1172 }
1173 mu.reset();
1174 assert_eq!(mu.force, 0.0);
1175 assert_eq!(mu.adapt, 0.0);
1176 let mut fresh = MotorUnit::new();
1177 let r1: i32 = (0..500).map(|_| mu.step(20.0)).sum();
1178 let r2: i32 = (0..500).map(|_| fresh.step(20.0)).sum();
1179 assert_eq!(r1, r2);
1180 }
1181
1182 #[test]
1183 fn motor_unit_voltage_bounded() {
1184 let mut mu = MotorUnit::new();
1185 for _ in 0..10000 {
1186 mu.step(50.0);
1187 }
1188 assert!(mu.v.is_finite());
1189 assert!(mu.force.is_finite());
1190 }
1191
1192 #[test]
1193 fn motor_unit_nan_recovery() {
1194 let mut mu = MotorUnit::new();
1195 for _ in 0..50 {
1196 mu.step(20.0);
1197 }
1198 for _ in 0..10 {
1199 let _ = mu.step(f64::NAN);
1200 }
1201 mu.reset();
1202 assert!(mu.v.is_finite());
1203 assert_eq!(mu.force, 0.0);
1204 }
1205
1206 #[test]
1207 fn motor_unit_performance() {
1208 let mut mu = MotorUnit::new();
1209 let start = std::time::Instant::now();
1210 for _ in 0..100_000 {
1211 mu.step(20.0);
1212 }
1213 assert!(
1214 start.elapsed().as_millis() < 50,
1215 "100k steps took {:?}",
1216 start.elapsed()
1217 );
1218 }
1219}