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 if !self.is_valid() || !drive.is_finite() {
237 return 0;
238 }
239 let v_old = self.v;
240 let adapt_old = self.adapt;
241 let input = self.gain * drive.max(0.0) - adapt_old;
242 let v_target = self.v_rest + input;
243 let v_candidate = v_target + (v_old - v_target) * (-self.dt / self.tau).exp();
244 let adapt_target = self.a_adapt * (v_candidate - self.v_rest);
245 let adapt_candidate =
246 adapt_target + (adapt_old - adapt_target) * (-self.dt / self.tau_adapt).exp();
247 if !v_candidate.is_finite() || !adapt_candidate.is_finite() {
248 return 0;
249 }
250 self.v = v_candidate;
251 self.adapt = adapt_candidate;
252
253 if v_candidate >= self.v_threshold {
254 self.v = self.v_reset;
255 1
256 } else {
257 0
258 }
259 }
260
261 pub fn reset(&mut self) {
262 self.v = self.v_rest;
263 self.adapt = 0.0;
264 }
265
266 fn is_valid(&self) -> bool {
267 [
268 self.v,
269 self.v_rest,
270 self.v_reset,
271 self.v_threshold,
272 self.tau,
273 self.adapt,
274 self.tau_adapt,
275 self.a_adapt,
276 self.gain,
277 self.dt,
278 ]
279 .iter()
280 .all(|value| value.is_finite())
281 && self.tau > 0.0
282 && self.tau_adapt > 0.0
283 && self.dt > 0.0
284 && self.gain >= 0.0
285 && self.v_reset < self.v_threshold
286 }
287}
288
289impl Default for GammaMotorNeuron {
290 fn default() -> Self {
291 Self::new()
292 }
293}
294
295#[derive(Clone, Debug)]
309pub struct UpperMotorNeuron {
310 pub v: f64,
311 pub m: f64,
312 pub h: f64,
313 pub n: f64,
314 pub p: f64, pub s: f64, pub g_na: f64,
318 pub g_k: f64,
319 pub g_m: f64,
320 pub g_ca: f64,
321 pub g_l: f64,
322 pub e_na: f64,
324 pub e_k: f64,
325 pub e_ca: f64,
326 pub e_l: f64,
327 pub c_m: f64,
328 pub dt: f64,
329 pub v_threshold: f64,
330}
331
332impl UpperMotorNeuron {
333 pub fn new() -> Self {
334 Self {
335 v: -70.0,
336 m: 0.05,
337 h: 0.6,
338 n: 0.3,
339 p: 0.0,
340 s: 0.0,
341 g_na: 50.0,
342 g_k: 5.0,
343 g_m: 0.07, g_ca: 0.3, g_l: 0.1,
346 e_na: 50.0,
347 e_k: -90.0,
348 e_ca: 120.0,
349 e_l: -70.0,
350 c_m: 1.0,
351 dt: 0.025,
352 v_threshold: -20.0,
353 }
354 }
355
356 fn finite(values: &[f64]) -> bool {
357 values.iter().all(|value| value.is_finite())
358 }
359
360 fn rate_exp(value: f64) -> f64 {
361 value.clamp(-60.0, 60.0).exp()
362 }
363
364 fn gate(previous: f64, alpha: f64, beta: f64, dt: f64) -> Option<f64> {
365 let total = alpha + beta;
366 if total <= 0.0 || !total.is_finite() {
367 return None;
368 }
369 let steady = alpha / total;
370 let next = steady + (previous - steady) * Self::rate_exp(-total * dt);
371 next.is_finite().then_some(next.clamp(0.0, 1.0))
372 }
373
374 fn gate_inf(previous: f64, steady: f64, tau: f64, dt: f64) -> Option<f64> {
375 if tau <= 0.0 || !tau.is_finite() {
376 return None;
377 }
378 let next = steady + (previous - steady) * Self::rate_exp(-dt / tau);
379 next.is_finite().then_some(next.clamp(0.0, 1.0))
380 }
381
382 fn valid_configuration(&self) -> bool {
383 Self::finite(&[
384 self.g_na,
385 self.g_k,
386 self.g_m,
387 self.g_ca,
388 self.g_l,
389 self.e_na,
390 self.e_k,
391 self.e_ca,
392 self.e_l,
393 self.c_m,
394 self.dt,
395 self.v_threshold,
396 ]) && self.g_na >= 0.0
397 && self.g_k >= 0.0
398 && self.g_m >= 0.0
399 && self.g_ca >= 0.0
400 && self.g_l >= 0.0
401 && self.c_m > 0.0
402 && self.dt > 0.0
403 }
404
405 fn valid_state(&self) -> bool {
406 Self::finite(&[self.v, self.m, self.h, self.n, self.p, self.s])
407 && (-150.0..=100.0).contains(&self.v)
408 && (0.0..=1.0).contains(&self.m)
409 && (0.0..=1.0).contains(&self.h)
410 && (0.0..=1.0).contains(&self.n)
411 && (0.0..=1.0).contains(&self.p)
412 && (0.0..=1.0).contains(&self.s)
413 }
414
415 fn step_candidate(
416 &self,
417 v: f64,
418 mut m: f64,
419 mut h: f64,
420 mut n: f64,
421 mut p: f64,
422 mut s: f64,
423 current: f64,
424 ) -> Option<(f64, f64, f64, f64, f64, f64)> {
425 let dv = v - (-56.2);
426 let x_m = dv - 13.0;
427 let alpha_m = if x_m.abs() < 1e-6 {
428 0.32 * 4.0
429 } else {
430 -0.32 * x_m / (Self::rate_exp(-x_m / 4.0) - 1.0)
431 };
432 let x_bm = dv - 40.0;
436 let beta_m = if x_bm.abs() < 1e-6 {
437 0.28 * 5.0
438 } else {
439 0.28 * x_bm / (Self::rate_exp(x_bm / 5.0) - 1.0)
440 };
441 let alpha_h = 0.128 * Self::rate_exp(-(dv - 17.0) / 18.0);
442 let beta_h = 4.0 / (1.0 + Self::rate_exp(-(dv - 40.0) / 5.0));
443 let x_n = dv - 15.0;
444 let alpha_n = if x_n.abs() < 1e-6 {
445 0.032 * 5.0
446 } else {
447 -0.032 * x_n / (Self::rate_exp(-x_n / 5.0) - 1.0)
448 };
449 let beta_n = 0.5 * Self::rate_exp(-(dv - 10.0) / 40.0);
450 m = Self::gate(m, alpha_m, beta_m, self.dt)?;
451 h = Self::gate(h, alpha_h, beta_h, self.dt)?;
452 n = Self::gate(n, alpha_n, beta_n, self.dt)?;
453 let p_inf = 1.0 / (1.0 + Self::rate_exp(-(v + 35.0) / 10.0));
454 let tau_p =
455 400.0 / (3.3 * Self::rate_exp((v + 35.0) / 20.0) + Self::rate_exp(-(v + 35.0) / 20.0));
456 p = Self::gate_inf(p, p_inf, tau_p, self.dt)?;
457 let s_inf = 1.0 / (1.0 + Self::rate_exp(-(v + 20.0) / 5.0));
458 s = Self::gate_inf(s, s_inf, 10.0, self.dt)?;
459 let g_na = self.g_na * m.powi(3) * h;
460 let g_k = self.g_k * n.powi(4);
461 let g_m = self.g_m * p;
462 let g_ca = self.g_ca * s.powi(2);
463 let g_total = g_na + g_k + g_m + g_ca + self.g_l;
464 if g_total <= 0.0 || !g_total.is_finite() {
465 return None;
466 }
467 let steady_v = (current
468 + g_na * self.e_na
469 + g_k * self.e_k
470 + g_m * self.e_k
471 + g_ca * self.e_ca
472 + self.g_l * self.e_l)
473 / g_total;
474 let next_v = steady_v + (v - steady_v) * Self::rate_exp(-(g_total / self.c_m) * self.dt);
475 Self::finite(&[next_v, m, h, n, p, s]).then_some((
476 next_v.clamp(-150.0, 100.0),
477 m,
478 h,
479 n,
480 p,
481 s,
482 ))
483 }
484
485 pub fn step(&mut self, current: f64) -> i32 {
486 if !self.valid_configuration() || !self.valid_state() || !current.is_finite() {
487 return 0;
488 }
489 let v_prev = self.v;
490 let (mut v, mut m, mut h, mut n, mut p, mut s) =
491 (self.v, self.m, self.h, self.n, self.p, self.s);
492 for _ in 0..4 {
493 let Some(next) = self.step_candidate(v, m, h, n, p, s, current) else {
494 return 0;
495 };
496 (v, m, h, n, p, s) = next;
497 }
498 (self.v, self.m, self.h, self.n, self.p, self.s) = (v, m, h, n, p, s);
499 if v >= self.v_threshold && v_prev < self.v_threshold {
500 1
501 } else {
502 0
503 }
504 }
505
506 pub fn reset(&mut self) {
507 self.v = -70.0;
508 self.m = 0.05;
509 self.h = 0.6;
510 self.n = 0.3;
511 self.p = 0.0;
512 self.s = 0.0;
513 }
514}
515
516impl Default for UpperMotorNeuron {
517 fn default() -> Self {
518 Self::new()
519 }
520}
521
522#[derive(Clone, Debug)]
538pub struct RenshawCell {
539 pub v: f64,
540 pub h: f64,
541 pub n: f64,
542 pub adapt: f64,
543 pub g_na: f64,
544 pub g_k: f64,
545 pub g_adapt: f64,
546 pub g_l: f64,
547 pub e_na: f64,
548 pub e_k: f64,
549 pub e_l: f64,
550 pub c_m: f64,
551 pub phi: f64,
552 pub tau_adapt: f64,
553 pub dt: f64,
554 pub v_threshold: f64,
555}
556
557impl RenshawCell {
558 pub fn new() -> Self {
559 Self {
560 v: -65.0,
561 h: 0.8,
562 n: 0.1,
563 adapt: 0.0,
564 g_na: 35.0,
565 g_k: 9.0,
566 g_adapt: 5.0,
567 g_l: 0.12,
568 e_na: 55.0,
569 e_k: -90.0,
570 e_l: -65.0,
571 c_m: 1.0,
572 phi: 5.0,
573 tau_adapt: 50.0,
574 dt: 0.01,
575 v_threshold: -20.0,
576 }
577 }
578
579 fn voltage_valid(value: f64) -> bool {
580 value.is_finite() && (-150.0..=100.0).contains(&value)
581 }
582
583 fn probability(value: f64) -> bool {
584 value.is_finite() && (0.0..=1.0).contains(&value)
585 }
586
587 fn exact_gate(previous: f64, alpha: f64, beta: f64, phi: f64, dt: f64) -> Option<f64> {
588 let total = phi * (alpha + beta);
589 if !previous.is_finite()
590 || !alpha.is_finite()
591 || !beta.is_finite()
592 || !total.is_finite()
593 || !dt.is_finite()
594 || total <= 0.0
595 {
596 return None;
597 }
598 let steady = alpha / (alpha + beta);
599 Some((steady + (previous - steady) * (-total * dt).exp()).clamp(0.0, 1.0))
600 }
601
602 fn exact_relax(previous: f64, steady: f64, tau: f64, dt: f64) -> Option<f64> {
603 if !previous.is_finite()
604 || !steady.is_finite()
605 || !tau.is_finite()
606 || !dt.is_finite()
607 || tau <= 0.0
608 {
609 return None;
610 }
611 Some((steady + (previous - steady) * (-dt / tau).exp()).clamp(0.0, 1.0))
612 }
613
614 fn valid_state(&self) -> bool {
615 Self::voltage_valid(self.v)
616 && Self::probability(self.h)
617 && Self::probability(self.n)
618 && Self::probability(self.adapt)
619 && self.g_na.is_finite()
620 && self.g_k.is_finite()
621 && self.g_adapt.is_finite()
622 && self.g_l.is_finite()
623 && self.e_na.is_finite()
624 && self.e_k.is_finite()
625 && self.e_l.is_finite()
626 && self.c_m.is_finite()
627 && self.phi.is_finite()
628 && self.tau_adapt.is_finite()
629 && self.dt.is_finite()
630 && self.v_threshold.is_finite()
631 && self.g_na >= 0.0
632 && self.g_k >= 0.0
633 && self.g_adapt >= 0.0
634 && self.g_l >= 0.0
635 && self.c_m > 0.0
636 && self.phi > 0.0
637 && self.tau_adapt > 0.0
638 && self.dt > 0.0
639 }
640
641 pub fn step(&mut self, current: f64) -> i32 {
642 if !current.is_finite() || !self.valid_state() {
643 return 0;
644 }
645
646 let v_prev = self.v;
647 let mut v = self.v;
648 let mut h = self.h;
649 let mut n = self.n;
650 let mut adapt = self.adapt;
651 let n_sub = ((0.5 / self.dt.max(0.001)).max(1.0)) as usize;
652 for _ in 0..n_sub {
653 let am = safe_rate(0.1, 35.0, v, 10.0, 1.0);
654 let bm = 4.0 * (-(v + 60.0) / 18.0).exp();
655 let m_inf = am / (am + bm);
656 let ah = 0.07 * (-(v + 58.0) / 20.0).exp();
657 let bh = 1.0 / (1.0 + (-(v + 28.0) / 10.0).exp());
658 let an = safe_rate(0.01, 34.0, v, 10.0, 0.1);
659 let bn = 0.125 * (-(v + 44.0) / 80.0).exp();
660
661 let Some(h_next) = Self::exact_gate(h, ah, bh, self.phi, self.dt) else {
662 return 0;
663 };
664 let Some(n_next) = Self::exact_gate(n, an, bn, self.phi, self.dt) else {
665 return 0;
666 };
667
668 let adapt_inf = 1.0 / (1.0 + (-(v + 30.0) / 5.0).exp());
669 let Some(adapt_next) = Self::exact_relax(adapt, adapt_inf, self.tau_adapt, self.dt)
670 else {
671 return 0;
672 };
673
674 let g_na = self.g_na * m_inf.powi(3) * h_next;
675 let g_k = self.g_k * n_next.powi(4);
676 let g_adapt = self.g_adapt * adapt_next;
677 let g_total = g_na + g_k + g_adapt + self.g_l;
678 if !g_total.is_finite() || g_total <= 0.0 {
679 return 0;
680 }
681
682 let steady_v = (current
683 + g_na * self.e_na
684 + g_k * self.e_k
685 + g_adapt * self.e_k
686 + self.g_l * self.e_l)
687 / g_total;
688 let v_next = steady_v + (v - steady_v) * (-(g_total / self.c_m) * self.dt).exp();
689 if !Self::voltage_valid(v_next)
690 || !Self::probability(h_next)
691 || !Self::probability(n_next)
692 || !Self::probability(adapt_next)
693 {
694 return 0;
695 }
696
697 v = v_next;
698 h = h_next;
699 n = n_next;
700 adapt = adapt_next;
701 }
702
703 self.v = v;
704 self.h = h;
705 self.n = n;
706 self.adapt = adapt;
707 if self.v >= self.v_threshold && v_prev < self.v_threshold {
708 1
709 } else {
710 0
711 }
712 }
713
714 pub fn reset(&mut self) {
715 self.v = -65.0;
716 self.h = 0.8;
717 self.n = 0.1;
718 self.adapt = 0.0;
719 }
720}
721
722impl Default for RenshawCell {
723 fn default() -> Self {
724 Self::new()
725 }
726}
727
728#[derive(Clone, Debug)]
744pub struct MotorUnit {
745 pub v: f64,
746 pub v_rest: f64,
747 pub v_reset: f64,
748 pub v_threshold: f64,
749 pub tau_m: f64, pub adapt: f64,
751 pub tau_adapt: f64,
752 pub a_adapt: f64,
753 pub gain: f64,
754 pub force: f64, pub twitch_amp: f64, pub tau_twitch: f64, pub force_decay: f64, pub dt: f64,
760}
761
762impl MotorUnit {
763 pub fn new() -> Self {
764 Self::slow()
765 }
766
767 pub fn slow() -> Self {
769 Self {
770 v: -65.0,
771 v_rest: -65.0,
772 v_reset: -70.0,
773 v_threshold: -50.0,
774 tau_m: 10.0,
775 adapt: 0.0,
776 tau_adapt: 100.0,
777 a_adapt: 0.2,
778 gain: 1.0,
779 force: 0.0,
780 twitch_amp: 0.05,
781 tau_twitch: 90.0,
782 force_decay: 0.0,
783 dt: 0.5,
784 }
785 }
786
787 pub fn fast() -> Self {
789 Self {
790 tau_m: 6.0,
791 tau_adapt: 50.0,
792 a_adapt: 0.1,
793 twitch_amp: 0.3,
794 tau_twitch: 30.0,
795 ..Self::slow()
796 }
797 }
798
799 fn voltage_valid(value: f64) -> bool {
800 value.is_finite() && (-150.0..=100.0).contains(&value)
801 }
802
803 fn force_valid(value: f64) -> bool {
804 value.is_finite() && (0.0..=1.0).contains(&value)
805 }
806
807 fn exact_relax(previous: f64, steady: f64, tau: f64, dt: f64) -> Option<f64> {
808 if !previous.is_finite()
809 || !steady.is_finite()
810 || !tau.is_finite()
811 || !dt.is_finite()
812 || tau <= 0.0
813 || dt <= 0.0
814 {
815 return None;
816 }
817 Some(steady + (previous - steady) * (-dt / tau).exp())
818 }
819
820 fn valid_state(&self) -> bool {
821 Self::voltage_valid(self.v)
822 && Self::voltage_valid(self.v_rest)
823 && Self::voltage_valid(self.v_reset)
824 && Self::voltage_valid(self.v_threshold)
825 && Self::force_valid(self.force)
826 && self.tau_m.is_finite()
827 && self.adapt.is_finite()
828 && self.tau_adapt.is_finite()
829 && self.a_adapt.is_finite()
830 && self.gain.is_finite()
831 && self.twitch_amp.is_finite()
832 && self.tau_twitch.is_finite()
833 && self.force_decay.is_finite()
834 && self.dt.is_finite()
835 && self.tau_m > 0.0
836 && self.tau_adapt > 0.0
837 && self.tau_twitch > 0.0
838 && self.dt > 0.0
839 && self.gain >= 0.0
840 && self.twitch_amp >= 0.0
841 && self.v_reset < self.v_threshold
842 }
843
844 pub fn step(&mut self, drive: f64) -> i32 {
846 if !drive.is_finite() || !self.valid_state() {
847 return 0;
848 }
849
850 let mut force = self.force * (-self.dt / self.tau_twitch).exp();
851 let input = self.gain * drive.max(0.0) - self.adapt;
852 let v_target = self.v_rest + input;
853 let Some(mut v_candidate) = Self::exact_relax(self.v, v_target, self.tau_m, self.dt) else {
854 return 0;
855 };
856 if !Self::voltage_valid(v_candidate) {
857 return 0;
858 }
859
860 let adapt_target = self.a_adapt * (v_candidate - self.v_rest);
861 let Some(adapt_candidate) =
862 Self::exact_relax(self.adapt, adapt_target, self.tau_adapt, self.dt)
863 else {
864 return 0;
865 };
866 if !adapt_candidate.is_finite() {
867 return 0;
868 }
869
870 let mut spike = 0;
871 if v_candidate >= self.v_threshold {
872 v_candidate = self.v_reset;
873 force = (force + self.twitch_amp).min(1.0);
874 spike = 1;
875 }
876 if !Self::voltage_valid(v_candidate) || !Self::force_valid(force) {
877 return 0;
878 }
879
880 self.v = v_candidate;
881 self.adapt = adapt_candidate;
882 self.force = force;
883 spike
884 }
885
886 pub fn reset(&mut self) {
887 self.v = self.v_rest;
888 self.adapt = 0.0;
889 self.force = 0.0;
890 }
891}
892
893impl Default for MotorUnit {
894 fn default() -> Self {
895 Self::new()
896 }
897}
898
899#[cfg(test)]
904mod tests {
905 use super::*;
906
907 #[test]
910 fn alpha_motor_fires_with_input() {
911 let mut n = AlphaMotorNeuron::new();
912 let spikes: i32 = (0..5000).map(|_| n.step(3.0)).sum();
913 assert!(
914 spikes > 0,
915 "alpha motor must fire with sustained input: got {spikes}"
916 );
917 }
918
919 #[test]
920 fn alpha_motor_no_fire_without_input() {
921 let mut n = AlphaMotorNeuron::new();
922 let spikes: i32 = (0..3000).map(|_| n.step(0.0)).sum();
923 assert_eq!(spikes, 0, "alpha motor should not fire at rest");
924 }
925
926 #[test]
927 fn alpha_motor_negative_current_no_fire() {
928 let mut n = AlphaMotorNeuron::new();
929 let spikes: i32 = (0..2000).map(|_| n.step(-2.0)).sum();
930 assert_eq!(spikes, 0);
931 }
932
933 #[test]
934 fn alpha_motor_ahp_limits_rate() {
935 let mut with_ahp = AlphaMotorNeuron::new();
938 let mut no_ahp = AlphaMotorNeuron::new();
939 no_ahp.g_ahp = 0.0;
940 let s_ahp: i32 = (0..5000).map(|_| with_ahp.step(5.0)).sum();
941 let s_none: i32 = (0..5000).map(|_| no_ahp.step(5.0)).sum();
942 assert!(
943 s_ahp <= s_none + 5,
944 "AHP should limit rate: with={s_ahp}, without={s_none}"
945 );
946 }
947
948 #[test]
949 fn alpha_motor_pic_responds_to_depolarisation() {
950 let mut n = AlphaMotorNeuron::new();
952 let baseline = n.m_pic;
953 for _ in 0..2000 {
954 n.step(4.0);
955 }
956 assert!(
957 n.m_pic > baseline + 0.001,
958 "PIC should respond to depolarisation: baseline={baseline}, after={}",
959 n.m_pic
960 );
961 }
962
963 #[test]
964 fn alpha_motor_ca_increases_during_spiking() {
965 let mut n = AlphaMotorNeuron::new();
966 for _ in 0..5000 {
967 n.step(5.0);
968 }
969 assert!(
970 n.ca > 0.0,
971 "Ca2+ should accumulate during spiking: ca={}",
972 n.ca
973 );
974 }
975
976 #[test]
977 fn alpha_motor_reset_roundtrip() {
978 let mut n = AlphaMotorNeuron::new();
979 for _ in 0..2000 {
980 n.step(4.0);
981 }
982 n.reset();
983 let mut fresh = AlphaMotorNeuron::new();
984 let r1: i32 = (0..1000).map(|_| n.step(4.0)).sum();
985 let r2: i32 = (0..1000).map(|_| fresh.step(4.0)).sum();
986 assert_eq!(r1, r2, "reset neuron must match fresh");
987 }
988
989 #[test]
990 fn alpha_motor_voltage_bounded() {
991 let mut n = AlphaMotorNeuron::new();
992 for _ in 0..10000 {
993 n.step(10.0);
994 }
995 assert!(n.v.is_finite(), "voltage must stay finite");
996 assert!(n.ca.is_finite(), "Ca2+ must stay finite");
997 assert!(n.ca >= 0.0, "Ca2+ must be non-negative");
998 }
999
1000 #[test]
1001 fn alpha_motor_nan_recovery() {
1002 let mut n = AlphaMotorNeuron::new();
1003 for _ in 0..100 {
1004 n.step(3.0);
1005 }
1006 for _ in 0..10 {
1007 let _ = n.step(f64::NAN);
1008 }
1009 n.reset();
1010 assert!(n.v.is_finite());
1011 assert!(n.ca >= 0.0);
1012 }
1013
1014 #[test]
1015 fn alpha_motor_extreme_input() {
1016 let mut n = AlphaMotorNeuron::new();
1017 for _ in 0..50 {
1018 n.step(1e6);
1019 }
1020 n.reset();
1021 assert!(n.v.is_finite());
1022 for _ in 0..50 {
1023 n.step(-1e6);
1024 }
1025 n.reset();
1026 assert!(n.v.is_finite());
1027 }
1028
1029 #[test]
1030 fn alpha_motor_performance() {
1031 let mut n = AlphaMotorNeuron::new();
1032 let start = std::time::Instant::now();
1033 for _ in 0..5_000 {
1034 n.step(4.0);
1035 }
1036 assert!(
1037 start.elapsed().as_millis() < 500,
1038 "5k steps took {:?}",
1039 start.elapsed()
1040 );
1041 }
1042
1043 #[test]
1046 fn gamma_dynamic_fires_with_drive() {
1047 let mut n = GammaMotorNeuron::dynamic();
1048 let spikes: i32 = (0..2000).map(|_| n.step(20.0)).sum();
1049 assert!(spikes > 0, "gamma dynamic must fire: got {spikes}");
1050 }
1051
1052 #[test]
1053 fn gamma_static_fires_with_drive() {
1054 let mut n = GammaMotorNeuron::static_type();
1055 let spikes: i32 = (0..2000).map(|_| n.step(20.0)).sum();
1056 assert!(spikes > 0, "gamma static must fire: got {spikes}");
1057 }
1058
1059 #[test]
1060 fn gamma_no_fire_without_drive() {
1061 let mut n = GammaMotorNeuron::new();
1062 let spikes: i32 = (0..1000).map(|_| n.step(0.0)).sum();
1063 assert_eq!(spikes, 0);
1064 }
1065
1066 #[test]
1067 fn gamma_negative_drive_no_fire() {
1068 let mut n = GammaMotorNeuron::new();
1069 let spikes: i32 = (0..1000).map(|_| n.step(-10.0)).sum();
1071 assert_eq!(spikes, 0);
1072 }
1073
1074 #[test]
1075 fn gamma_adaptation_reduces_rate() {
1076 let mut n = GammaMotorNeuron::new();
1077 let first: i32 = (0..1000).map(|_| n.step(20.0)).sum();
1078 let second: i32 = (0..1000).map(|_| n.step(20.0)).sum();
1079 assert!(
1080 second <= first + 3,
1081 "gamma should adapt: first={first}, second={second}"
1082 );
1083 }
1084
1085 #[test]
1086 fn gamma_static_adapts_more_than_dynamic() {
1087 let mut dyn_ = GammaMotorNeuron::dynamic();
1088 let mut stat = GammaMotorNeuron::static_type();
1089 let dyn_spikes: i32 = (0..2000).map(|_| dyn_.step(20.0)).sum();
1090 let stat_spikes: i32 = (0..2000).map(|_| stat.step(20.0)).sum();
1091 assert!(
1093 stat_spikes <= dyn_spikes + 5,
1094 "static ({stat_spikes}) should fire <= dynamic ({dyn_spikes})"
1095 );
1096 }
1097
1098 #[test]
1099 fn gamma_reset_roundtrip() {
1100 let mut n = GammaMotorNeuron::new();
1101 for _ in 0..1000 {
1102 n.step(20.0);
1103 }
1104 n.reset();
1105 let mut fresh = GammaMotorNeuron::new();
1106 let r1: i32 = (0..500).map(|_| n.step(20.0)).sum();
1107 let r2: i32 = (0..500).map(|_| fresh.step(20.0)).sum();
1108 assert_eq!(r1, r2);
1109 }
1110
1111 #[test]
1112 fn gamma_voltage_bounded() {
1113 let mut n = GammaMotorNeuron::new();
1114 for _ in 0..10000 {
1115 n.step(50.0);
1116 }
1117 assert!(n.v.is_finite());
1118 assert!(n.adapt.is_finite());
1119 }
1120
1121 #[test]
1122 fn gamma_nan_recovery() {
1123 let mut n = GammaMotorNeuron::new();
1124 for _ in 0..50 {
1125 n.step(20.0);
1126 }
1127 let before_v = n.v;
1128 let before_adapt = n.adapt;
1129 for _ in 0..10 {
1130 let _ = n.step(f64::NAN);
1131 }
1132 assert_eq!(n.v, before_v);
1133 assert_eq!(n.adapt, before_adapt);
1134 n.reset();
1135 assert!(n.v.is_finite());
1136 assert_eq!(n.adapt, 0.0);
1137 }
1138
1139 #[test]
1140 fn gamma_extreme_input() {
1141 let mut n = GammaMotorNeuron::new();
1142 for _ in 0..50 {
1143 n.step(1e6);
1144 }
1145 n.reset();
1146 assert!(n.v.is_finite());
1147 }
1148
1149 #[test]
1150 fn gamma_corrupted_state_preserved_on_step() {
1151 let mut n = GammaMotorNeuron::new();
1152 n.tau = 0.0;
1153 let before_v = n.v;
1154 let before_adapt = n.adapt;
1155 assert_eq!(n.step(20.0), 0);
1156 assert_eq!(n.v, before_v);
1157 assert_eq!(n.adapt, before_adapt);
1158 }
1159
1160 #[test]
1161 fn gamma_performance() {
1162 let mut n = GammaMotorNeuron::new();
1163 let start = std::time::Instant::now();
1164 for _ in 0..100_000 {
1165 n.step(20.0);
1166 }
1167 assert!(
1168 start.elapsed().as_millis() < 50,
1169 "100k steps took {:?}",
1170 start.elapsed()
1171 );
1172 }
1173
1174 #[test]
1177 fn upper_motor_fires_with_input() {
1178 let mut n = UpperMotorNeuron::new();
1179 let spikes: i32 = (0..10000).map(|_| n.step(5.0)).sum();
1180 assert!(spikes > 0, "upper motor must fire: got {spikes}");
1181 }
1182
1183 #[test]
1184 fn upper_motor_no_fire_without_input() {
1185 let mut n = UpperMotorNeuron::new();
1186 let spikes: i32 = (0..5000).map(|_| n.step(0.0)).sum();
1187 assert_eq!(spikes, 0);
1188 }
1189
1190 fn upper_motor_reference_step(mut n: UpperMotorNeuron, current: f64) -> UpperMotorNeuron {
1191 fn gate(previous: f64, alpha: f64, beta: f64, dt: f64) -> f64 {
1192 let total = alpha + beta;
1193 let steady = alpha / total;
1194 (steady + (previous - steady) * (-total * dt).exp()).clamp(0.0, 1.0)
1195 }
1196 fn gate_inf(previous: f64, steady: f64, tau: f64, dt: f64) -> f64 {
1197 (steady + (previous - steady) * (-dt / tau).exp()).clamp(0.0, 1.0)
1198 }
1199 let vt = -56.2;
1200 for _ in 0..4 {
1201 let dv = n.v - vt;
1202 let x_m = dv - 13.0;
1203 let alpha_m = if x_m.abs() < 1e-6 {
1204 0.32 * 4.0
1205 } else {
1206 -0.32 * x_m / ((-x_m / 4.0).exp() - 1.0)
1207 };
1208 let x_bm = dv - 40.0;
1209 let beta_m = if x_bm.abs() < 1e-6 {
1210 0.28 * 5.0
1211 } else {
1212 0.28 * x_bm / ((x_bm / 5.0).exp() - 1.0)
1213 };
1214 let alpha_h = 0.128 * (-(dv - 17.0) / 18.0).exp();
1215 let beta_h = 4.0 / (1.0 + (-(dv - 40.0) / 5.0).exp());
1216 let x_n = dv - 15.0;
1217 let alpha_n = if x_n.abs() < 1e-6 {
1218 0.032 * 5.0
1219 } else {
1220 -0.032 * x_n / ((-x_n / 5.0).exp() - 1.0)
1221 };
1222 let beta_n = 0.5 * (-(dv - 10.0) / 40.0).exp();
1223
1224 n.m = gate(n.m, alpha_m, beta_m, n.dt);
1225 n.h = gate(n.h, alpha_h, beta_h, n.dt);
1226 n.n = gate(n.n, alpha_n, beta_n, n.dt);
1227
1228 let p_inf = 1.0 / (1.0 + (-(n.v + 35.0) / 10.0).exp());
1229 let tau_p = 400.0 / (3.3 * ((n.v + 35.0) / 20.0).exp() + (-(n.v + 35.0) / 20.0).exp());
1230 n.p = gate_inf(n.p, p_inf, tau_p, n.dt);
1231
1232 let s_inf = 1.0 / (1.0 + (-(n.v + 20.0) / 5.0).exp());
1233 n.s = gate_inf(n.s, s_inf, 10.0, n.dt);
1234
1235 let g_na = n.g_na * n.m.powi(3) * n.h;
1236 let g_k = n.g_k * n.n.powi(4);
1237 let g_m = n.g_m * n.p;
1238 let g_ca = n.g_ca * n.s.powi(2);
1239 let g_total = g_na + g_k + g_m + g_ca + n.g_l;
1240 let steady_v = (current
1241 + g_na * n.e_na
1242 + g_k * n.e_k
1243 + g_m * n.e_k
1244 + g_ca * n.e_ca
1245 + n.g_l * n.e_l)
1246 / g_total;
1247 n.v = steady_v + (n.v - steady_v) * (-(g_total / n.c_m) * n.dt).exp();
1248 }
1249 n
1250 }
1251
1252 #[test]
1253 fn upper_motor_uses_exact_gate_and_conductance_membrane_step() {
1254 let mut n = UpperMotorNeuron::new();
1255 let expected = upper_motor_reference_step(n.clone(), 5.0);
1256
1257 assert_eq!(n.step(5.0), 0);
1258
1259 assert!((n.v - expected.v).abs() <= 1e-12);
1260 assert!((n.m - expected.m).abs() <= 1e-12);
1261 assert!((n.h - expected.h).abs() <= 1e-12);
1262 assert!((n.n - expected.n).abs() <= 1e-12);
1263 assert!((n.p - expected.p).abs() <= 1e-12);
1264 assert!((n.s - expected.s).abs() <= 1e-12);
1265 }
1266
1267 #[test]
1268 fn upper_motor_corrupted_gate_is_preserved_on_step() {
1269 let mut n = UpperMotorNeuron::new();
1270 n.m = 1.5;
1271 let before = n.clone();
1272
1273 assert_eq!(n.step(5.0), 0);
1274
1275 assert_eq!(n.v, before.v);
1276 assert_eq!(n.m, before.m);
1277 assert_eq!(n.h, before.h);
1278 assert_eq!(n.n, before.n);
1279 assert_eq!(n.p, before.p);
1280 assert_eq!(n.s, before.s);
1281 }
1282
1283 #[test]
1284 fn upper_motor_negative_current_no_fire() {
1285 let mut n = UpperMotorNeuron::new();
1286 let spikes: i32 = (0..2000).map(|_| n.step(-5.0)).sum();
1287 assert_eq!(spikes, 0);
1288 }
1289
1290 #[test]
1291 fn upper_motor_adaptation_via_m_current() {
1292 let mut n = UpperMotorNeuron::new();
1293 let first: i32 = (0..5000).map(|_| n.step(5.0)).sum();
1294 let second: i32 = (0..5000).map(|_| n.step(5.0)).sum();
1295 assert!(
1296 second <= first + 3,
1297 "M-current should cause adaptation: first={first}, second={second}"
1298 );
1299 }
1300
1301 #[test]
1302 fn upper_motor_ca_activates_during_depolarisation() {
1303 let mut n = UpperMotorNeuron::new();
1304 let baseline = n.s;
1305 for _ in 0..5000 {
1306 n.step(5.0);
1307 }
1308 assert!(
1309 n.s > baseline + 0.001,
1310 "Ca2+ gate should activate: s={}",
1311 n.s
1312 );
1313 }
1314
1315 #[test]
1316 fn upper_motor_reset_roundtrip() {
1317 let mut n = UpperMotorNeuron::new();
1318 for _ in 0..3000 {
1319 n.step(5.0);
1320 }
1321 n.reset();
1322 let mut fresh = UpperMotorNeuron::new();
1323 let r1: i32 = (0..2000).map(|_| n.step(5.0)).sum();
1324 let r2: i32 = (0..2000).map(|_| fresh.step(5.0)).sum();
1325 assert_eq!(r1, r2);
1326 }
1327
1328 #[test]
1329 fn upper_motor_voltage_bounded() {
1330 let mut n = UpperMotorNeuron::new();
1331 for _ in 0..20000 {
1332 n.step(10.0);
1333 }
1334 assert!(n.v.is_finite());
1335 assert!(n.p.is_finite());
1336 assert!(n.s.is_finite());
1337 }
1338
1339 #[test]
1340 fn upper_motor_nan_recovery() {
1341 let mut n = UpperMotorNeuron::new();
1342 for _ in 0..100 {
1343 n.step(5.0);
1344 }
1345 for _ in 0..10 {
1346 let _ = n.step(f64::NAN);
1347 }
1348 n.reset();
1349 assert!(n.v.is_finite());
1350 }
1351
1352 #[test]
1353 fn upper_motor_extreme_input() {
1354 let mut n = UpperMotorNeuron::new();
1355 for _ in 0..50 {
1356 n.step(1e6);
1357 }
1358 n.reset();
1359 assert!(n.v.is_finite());
1360 }
1361
1362 #[test]
1363 fn upper_motor_performance() {
1364 let mut n = UpperMotorNeuron::new();
1365 let start = std::time::Instant::now();
1366 for _ in 0..10_000 {
1367 n.step(5.0);
1368 }
1369 assert!(
1370 start.elapsed().as_millis() < 100,
1371 "10k steps took {:?}",
1372 start.elapsed()
1373 );
1374 }
1375
1376 #[test]
1379 fn renshaw_fires_with_input() {
1380 let mut n = RenshawCell::new();
1381 let spikes: i32 = (0..5000).map(|_| n.step(3.0)).sum();
1382 assert!(spikes > 0, "Renshaw must fire: got {spikes}");
1383 }
1384
1385 #[test]
1386 fn renshaw_no_fire_without_input() {
1387 let mut n = RenshawCell::new();
1388 let spikes: i32 = (0..3000).map(|_| n.step(0.0)).sum();
1389 assert_eq!(spikes, 0);
1390 }
1391
1392 #[test]
1393 fn renshaw_negative_current_no_fire() {
1394 let mut n = RenshawCell::new();
1395 let spikes: i32 = (0..2000).map(|_| n.step(-2.0)).sum();
1396 assert_eq!(spikes, 0);
1397 }
1398
1399 fn renshaw_test_gate(previous: f64, alpha: f64, beta: f64, phi: f64, dt: f64) -> f64 {
1400 let total = phi * (alpha + beta);
1401 let steady = alpha / (alpha + beta);
1402 (steady + (previous - steady) * (-total * dt).exp()).clamp(0.0, 1.0)
1403 }
1404
1405 fn renshaw_test_adapt(previous: f64, steady: f64, tau: f64, dt: f64) -> f64 {
1406 (steady + (previous - steady) * (-dt / tau).exp()).clamp(0.0, 1.0)
1407 }
1408
1409 fn renshaw_reference_step(mut n: RenshawCell, current: f64) -> RenshawCell {
1410 let n_sub = ((0.5 / n.dt.max(0.001)).max(1.0)) as usize;
1411 for _ in 0..n_sub {
1412 let am = safe_rate(0.1, 35.0, n.v, 10.0, 1.0);
1413 let bm = 4.0 * (-(n.v + 60.0) / 18.0).exp();
1414 let ah = 0.07 * (-(n.v + 58.0) / 20.0).exp();
1415 let bh = 1.0 / (1.0 + (-(n.v + 28.0) / 10.0).exp());
1416 let an = safe_rate(0.01, 34.0, n.v, 10.0, 0.1);
1417 let bn = 0.125 * (-(n.v + 44.0) / 80.0).exp();
1418 let m_inf = am / (am + bm);
1419
1420 n.h = renshaw_test_gate(n.h, ah, bh, n.phi, n.dt);
1421 n.n = renshaw_test_gate(n.n, an, bn, n.phi, n.dt);
1422 let adapt_inf = 1.0 / (1.0 + (-(n.v + 30.0) / 5.0).exp());
1423 n.adapt = renshaw_test_adapt(n.adapt, adapt_inf, n.tau_adapt, n.dt);
1424
1425 let g_na = n.g_na * m_inf.powi(3) * n.h;
1426 let g_k = n.g_k * n.n.powi(4);
1427 let g_adapt = n.g_adapt * n.adapt;
1428 let g_total = g_na + g_k + g_adapt + n.g_l;
1429 let steady_v =
1430 (current + g_na * n.e_na + g_k * n.e_k + g_adapt * n.e_k + n.g_l * n.e_l) / g_total;
1431 n.v = steady_v + (n.v - steady_v) * (-(g_total / n.c_m) * n.dt).exp();
1432 }
1433 n
1434 }
1435
1436 fn renshaw_snapshot(n: &RenshawCell) -> (f64, f64, f64, f64) {
1437 (n.v, n.h, n.n, n.adapt)
1438 }
1439
1440 #[test]
1441 fn renshaw_uses_exact_gate_and_conductance_membrane_step() {
1442 let mut n = RenshawCell::new();
1443 let expected = renshaw_reference_step(RenshawCell::new(), 4.0);
1444
1445 assert_eq!(n.step(4.0), 0);
1446
1447 assert!((n.v - expected.v).abs() <= 1e-12);
1448 assert!((n.h - expected.h).abs() <= 1e-12);
1449 assert!((n.n - expected.n).abs() <= 1e-12);
1450 assert!((n.adapt - expected.adapt).abs() <= 1e-12);
1451 }
1452
1453 #[test]
1454 fn renshaw_rejects_invalid_current_without_state_mutation() {
1455 let mut n = RenshawCell::new();
1456 for _ in 0..20 {
1457 n.step(4.0);
1458 }
1459 let before = renshaw_snapshot(&n);
1460
1461 assert_eq!(n.step(f64::NAN), 0);
1462 assert_eq!(renshaw_snapshot(&n), before);
1463 assert_eq!(n.step(f64::INFINITY), 0);
1464 assert_eq!(renshaw_snapshot(&n), before);
1465 }
1466
1467 #[test]
1468 fn renshaw_rejects_excess_current_without_state_mutation() {
1469 let mut n = RenshawCell::new();
1470 let before = renshaw_snapshot(&n);
1471
1472 assert_eq!(n.step(1.0e8), 0);
1473
1474 assert_eq!(renshaw_snapshot(&n), before);
1475 }
1476
1477 #[test]
1478 fn renshaw_corrupted_gate_is_preserved_on_step() {
1479 let mut n = RenshawCell::new();
1480 n.h = 1.5;
1481 let before = renshaw_snapshot(&n);
1482
1483 assert_eq!(n.step(4.0), 0);
1484
1485 assert_eq!(renshaw_snapshot(&n), before);
1486 }
1487
1488 #[test]
1489 fn renshaw_burst_then_adapt() {
1490 let mut n = RenshawCell::new();
1492 let first: i32 = (0..2000).map(|_| n.step(4.0)).sum();
1493 let second: i32 = (0..2000).map(|_| n.step(4.0)).sum();
1494 assert!(
1495 second <= first + 5,
1496 "Renshaw should adapt: first={first}, second={second}"
1497 );
1498 }
1499
1500 #[test]
1501 fn renshaw_adapt_increases_during_firing() {
1502 let mut n = RenshawCell::new();
1503 let baseline = n.adapt;
1504 for _ in 0..3000 {
1505 n.step(4.0);
1506 }
1507 assert!(
1508 n.adapt > baseline + 0.01,
1509 "adaptation variable should increase: adapt={}",
1510 n.adapt
1511 );
1512 }
1513
1514 #[test]
1515 fn renshaw_reset_roundtrip() {
1516 let mut n = RenshawCell::new();
1517 for _ in 0..2000 {
1518 n.step(4.0);
1519 }
1520 n.reset();
1521 let mut fresh = RenshawCell::new();
1522 let r1: i32 = (0..1000).map(|_| n.step(4.0)).sum();
1523 let r2: i32 = (0..1000).map(|_| fresh.step(4.0)).sum();
1524 assert_eq!(r1, r2);
1525 }
1526
1527 #[test]
1528 fn renshaw_voltage_bounded() {
1529 let mut n = RenshawCell::new();
1530 for _ in 0..10000 {
1531 n.step(10.0);
1532 }
1533 assert!(n.v.is_finite());
1534 assert!(n.adapt.is_finite());
1535 }
1536
1537 #[test]
1538 fn renshaw_nan_recovery() {
1539 let mut n = RenshawCell::new();
1540 for _ in 0..100 {
1541 n.step(3.0);
1542 }
1543 for _ in 0..10 {
1544 let _ = n.step(f64::NAN);
1545 }
1546 n.reset();
1547 assert!(n.v.is_finite());
1548 assert_eq!(n.adapt, 0.0);
1549 }
1550
1551 #[test]
1552 fn renshaw_extreme_input() {
1553 let mut n = RenshawCell::new();
1554 for _ in 0..50 {
1555 n.step(1e6);
1556 }
1557 n.reset();
1558 assert!(n.v.is_finite());
1559 }
1560
1561 #[test]
1562 fn renshaw_performance() {
1563 let mut n = RenshawCell::new();
1564 let start = std::time::Instant::now();
1565 for _ in 0..5_000 {
1566 n.step(4.0);
1567 }
1568 assert!(
1569 start.elapsed().as_millis() < 500,
1570 "5k steps took {:?}",
1571 start.elapsed()
1572 );
1573 }
1574
1575 #[test]
1578 fn motor_unit_fires_with_drive() {
1579 let mut mu = MotorUnit::new();
1580 let spikes: i32 = (0..2000).map(|_| mu.step(20.0)).sum();
1581 assert!(spikes > 0, "motor unit must fire: got {spikes}");
1582 }
1583
1584 #[test]
1585 fn motor_unit_no_fire_without_drive() {
1586 let mut mu = MotorUnit::new();
1587 let spikes: i32 = (0..1000).map(|_| mu.step(0.0)).sum();
1588 assert_eq!(spikes, 0);
1589 }
1590
1591 #[test]
1592 fn motor_unit_negative_drive_no_fire() {
1593 let mut mu = MotorUnit::new();
1594 let spikes: i32 = (0..1000).map(|_| mu.step(-10.0)).sum();
1595 assert_eq!(spikes, 0);
1596 }
1597
1598 #[test]
1599 fn motor_unit_force_increases_with_spikes() {
1600 let mut mu = MotorUnit::new();
1601 assert_eq!(mu.force, 0.0);
1602 for _ in 0..2000 {
1603 mu.step(20.0);
1604 }
1605 assert!(
1606 mu.force > 0.0,
1607 "force should increase during spiking: f={}",
1608 mu.force
1609 );
1610 }
1611
1612 #[test]
1613 fn motor_unit_force_decays_without_input() {
1614 let mut mu = MotorUnit::new();
1615 for _ in 0..1000 {
1617 mu.step(20.0);
1618 }
1619 let peak = mu.force;
1620 assert!(peak > 0.0);
1621 for _ in 0..5000 {
1623 mu.step(0.0);
1624 }
1625 assert!(
1626 mu.force < peak,
1627 "force should decay: peak={peak}, now={}",
1628 mu.force
1629 );
1630 }
1631
1632 #[test]
1633 fn motor_unit_fast_produces_more_force() {
1634 let mut slow = MotorUnit::slow();
1635 let mut fast = MotorUnit::fast();
1636 for _ in 0..2000 {
1637 slow.step(20.0);
1638 fast.step(20.0);
1639 }
1640 assert!(
1641 fast.force >= slow.force,
1642 "fast MU ({}) should produce >= force than slow ({})",
1643 fast.force,
1644 slow.force
1645 );
1646 }
1647
1648 #[test]
1649 fn motor_unit_force_capped_at_one() {
1650 let mut mu = MotorUnit::fast();
1651 for _ in 0..10000 {
1652 mu.step(50.0);
1653 }
1654 assert!(mu.force <= 1.0, "force must not exceed 1.0: f={}", mu.force);
1655 }
1656
1657 #[test]
1658 fn motor_unit_reset_roundtrip() {
1659 let mut mu = MotorUnit::new();
1660 for _ in 0..1000 {
1661 mu.step(20.0);
1662 }
1663 mu.reset();
1664 assert_eq!(mu.force, 0.0);
1665 assert_eq!(mu.adapt, 0.0);
1666 let mut fresh = MotorUnit::new();
1667 let r1: i32 = (0..500).map(|_| mu.step(20.0)).sum();
1668 let r2: i32 = (0..500).map(|_| fresh.step(20.0)).sum();
1669 assert_eq!(r1, r2);
1670 }
1671
1672 #[test]
1673 fn motor_unit_voltage_bounded() {
1674 let mut mu = MotorUnit::new();
1675 for _ in 0..10000 {
1676 mu.step(50.0);
1677 }
1678 assert!(mu.v.is_finite());
1679 assert!(mu.force.is_finite());
1680 }
1681
1682 #[test]
1683 fn motor_unit_nan_recovery() {
1684 let mut mu = MotorUnit::new();
1685 for _ in 0..50 {
1686 mu.step(20.0);
1687 }
1688 for _ in 0..10 {
1689 let _ = mu.step(f64::NAN);
1690 }
1691 mu.reset();
1692 assert!(mu.v.is_finite());
1693 assert_eq!(mu.force, 0.0);
1694 }
1695
1696 #[test]
1697 fn motor_unit_performance() {
1698 let mut mu = MotorUnit::new();
1699 let start = std::time::Instant::now();
1700 for _ in 0..100_000 {
1701 mu.step(20.0);
1702 }
1703 let elapsed = start.elapsed();
1704 assert!(elapsed.as_millis() < 100, "100k steps took {:?}", elapsed);
1705 }
1706}