1#[derive(Clone, Debug)]
13pub struct ChialvoMapNeuron {
14 pub x: f64,
15 pub y: f64,
16 pub a: f64,
17 pub b: f64,
18 pub c: f64,
19 pub k: f64,
20 pub x_threshold: f64,
21}
22
23impl ChialvoMapNeuron {
24 pub fn new() -> Self {
25 Self {
26 x: 0.0,
27 y: 0.0,
28 a: 0.89,
29 b: 0.6,
30 c: 0.28,
31 k: 0.04,
32 x_threshold: 1.0,
33 }
34 }
35 pub fn step(&mut self, current: f64) -> i32 {
36 let x_prev = self.x;
37 let x_new = self.x * self.x * (self.y - self.x).exp() + self.k + current;
38 let y_new = self.a * self.y - self.b * self.x + self.c;
39 self.x = x_new;
40 self.y = y_new;
41 if self.x >= self.x_threshold && x_prev < self.x_threshold {
42 1
43 } else {
44 0
45 }
46 }
47 pub fn reset(&mut self) {
48 self.x = 0.0;
49 self.y = 0.0;
50 }
51}
52impl Default for ChialvoMapNeuron {
53 fn default() -> Self {
54 Self::new()
55 }
56}
57
58#[derive(Clone, Debug)]
60pub struct RulkovMapNeuron {
61 pub x: f64,
62 pub y: f64,
63 pub alpha: f64,
64 pub sigma: f64,
65 pub mu: f64,
66 pub x_threshold: f64,
67}
68
69impl RulkovMapNeuron {
70 pub fn new() -> Self {
71 Self {
72 x: -1.0,
73 y: -3.0,
74 alpha: 4.0,
75 sigma: -1.6,
76 mu: 0.001,
77 x_threshold: 0.0,
78 }
79 }
80 pub fn step(&mut self, current: f64) -> i32 {
81 let x_prev = self.x;
82 let x_new = if self.x <= 0.0 {
83 self.alpha / (1.0 - self.x) + self.y + current
84 } else if self.x < self.alpha + self.y + current {
85 self.alpha + self.y + current
86 } else {
87 -1.0
88 };
89 let y_new = self.y - self.mu * (self.x + 1.0) + self.mu * self.sigma;
90 self.x = x_new;
91 self.y = y_new;
92 if self.x >= self.x_threshold && x_prev < self.x_threshold {
93 1
94 } else {
95 0
96 }
97 }
98 pub fn simulate(&mut self, n_steps: usize, current: f64) -> (Vec<f64>, i64) {
103 let mut trace = Vec::with_capacity(n_steps);
104 let mut spikes: i64 = 0;
105 for _ in 0..n_steps {
106 let spiked = self.step(current);
107 trace.push(self.x);
108 spikes += spiked as i64;
109 }
110 (trace, spikes)
111 }
112 pub fn reset(&mut self) {
113 self.x = -1.0;
114 self.y = -3.0;
115 }
116}
117impl Default for RulkovMapNeuron {
118 fn default() -> Self {
119 Self::new()
120 }
121}
122
123#[derive(Clone, Debug)]
125pub struct IbarzTanakaMapNeuron {
126 pub x: f64,
127 pub y: f64,
128 pub alpha: f64,
129 pub beta: f64,
130 pub mu: f64,
131 pub sigma: f64,
132 pub x_threshold: f64,
133 pub x_reset: f64,
134}
135
136impl IbarzTanakaMapNeuron {
137 pub fn new() -> Self {
138 Self {
139 x: -1.0,
140 y: -2.5,
141 alpha: 3.65,
142 beta: 0.25,
143 mu: 0.0005,
144 sigma: -1.6,
145 x_threshold: 3.0,
146 x_reset: -1.0,
147 }
148 }
149 pub fn step(&mut self, current: f64) -> i32 {
150 let f = if self.x <= 0.0 {
151 self.alpha / (1.0 - self.x)
152 } else {
153 self.alpha + self.beta * self.x
154 };
155 let x_new = f + self.y + current;
156 let y_new = self.y - self.mu * (self.x + 1.0) + self.mu * self.sigma;
157 self.x = x_new;
158 self.y = y_new;
159 if self.x >= self.x_threshold {
160 self.x = self.x_reset;
161 1
162 } else {
163 0
164 }
165 }
166 pub fn simulate(&mut self, n_steps: usize, current: f64) -> (Vec<f64>, i64) {
171 let mut trace = Vec::with_capacity(n_steps);
172 let mut spikes: i64 = 0;
173 for _ in 0..n_steps {
174 let spiked = self.step(current);
175 trace.push(self.x);
176 spikes += spiked as i64;
177 }
178 (trace, spikes)
179 }
180 pub fn reset(&mut self) {
181 self.x = -1.0;
182 self.y = -2.5;
183 }
184}
185impl Default for IbarzTanakaMapNeuron {
186 fn default() -> Self {
187 Self::new()
188 }
189}
190
191#[derive(Clone, Debug)]
193pub struct MedvedevMapNeuron {
194 pub x: f64,
195 pub alpha: f64,
196 pub beta: f64,
197 pub x_threshold: f64,
198}
199
200impl MedvedevMapNeuron {
201 pub fn new() -> Self {
202 Self {
203 x: 0.0,
204 alpha: 3.5,
205 beta: 0.5,
206 x_threshold: 0.9,
207 }
208 }
209 pub fn step(&mut self, current: f64) -> i32 {
210 let x_prev = self.x;
211 self.x = if self.x < self.beta {
212 self.alpha * self.x + current
213 } else {
214 self.alpha * (1.0 - self.x) + current
215 };
216 self.x = self.x.rem_euclid(1.0);
217 if self.x >= self.x_threshold && x_prev < self.x_threshold {
218 1
219 } else {
220 0
221 }
222 }
223 pub fn simulate(&mut self, n_steps: usize, current: f64) -> (Vec<f64>, i64) {
229 let mut trace = Vec::with_capacity(n_steps);
230 let mut spikes: i64 = 0;
231 for _ in 0..n_steps {
232 let spiked = self.step(current);
233 trace.push(self.x);
234 spikes += spiked as i64;
235 }
236 (trace, spikes)
237 }
238 pub fn reset(&mut self) {
239 self.x = 0.0;
240 }
241}
242impl Default for MedvedevMapNeuron {
243 fn default() -> Self {
244 Self::new()
245 }
246}
247
248#[derive(Clone, Debug)]
250pub struct CazellesMapNeuron {
251 pub x: f64,
252 pub y: f64,
253 pub a: f64,
254 pub epsilon: f64,
255 pub sigma: f64,
256 pub x_threshold: f64,
257}
258
259impl CazellesMapNeuron {
260 pub fn new() -> Self {
261 Self {
262 x: 0.1,
263 y: 0.0,
264 a: 3.8,
265 epsilon: 0.01,
266 sigma: 0.5,
267 x_threshold: 0.9,
268 }
269 }
270 pub fn step(&mut self, current: f64) -> i32 {
271 let f = self.a * self.x * (1.0 - self.x);
272 let x_new = (f - self.y + current).clamp(-2.0, 2.0);
273 let y_new = self.y + self.epsilon * (self.x - self.sigma);
274 self.x = x_new;
275 self.y = y_new;
276 if self.x >= self.x_threshold {
277 1
278 } else {
279 0
280 }
281 }
282 pub fn simulate(&mut self, n_steps: usize, current: f64) -> (Vec<f64>, i64) {
287 let mut trace = Vec::with_capacity(n_steps);
288 let mut spikes: i64 = 0;
289 for _ in 0..n_steps {
290 let spiked = self.step(current);
291 trace.push(self.x);
292 spikes += spiked as i64;
293 }
294 (trace, spikes)
295 }
296 pub fn reset(&mut self) {
297 self.x = 0.1;
298 self.y = 0.0;
299 }
300}
301impl Default for CazellesMapNeuron {
302 fn default() -> Self {
303 Self::new()
304 }
305}
306
307#[derive(Clone, Debug)]
325pub struct CourageNekorkinMapNeuron {
326 pub x: f64,
327 pub y: f64,
328 pub m0: f64,
329 pub m1: f64,
330 pub a: f64,
331 pub d: f64,
332 pub j: f64,
333 pub beta: f64,
334 pub eps: f64,
335 pub x_threshold: f64,
336}
337
338impl CourageNekorkinMapNeuron {
339 pub fn new() -> Self {
340 Self {
341 x: 0.0,
342 y: 0.0,
343 m0: 0.0864,
344 m1: 0.65,
345 a: 0.2,
346 d: 0.235,
347 j: 0.2,
348 beta: 0.085,
349 eps: 0.02,
350 x_threshold: 0.235,
351 }
352 }
353 pub fn step(&mut self, current: f64) -> i32 {
354 let x_prev = self.x;
355 let am1 = self.a * self.m1;
356 let den = self.m0 + self.m1;
357 let jmin = am1 / den;
358 let jmax = (self.m0 + am1) / den;
359 let fx = if self.x <= jmin {
360 -self.m0 * self.x
361 } else if self.x < jmax {
362 self.m1 * (self.x - self.a)
363 } else {
364 -self.m0 * (self.x - 1.0)
365 };
366 let h = if (self.x - self.d) >= 0.0 { 1.0 } else { 0.0 };
367 let x_new = self.x + fx - self.y - self.beta * h + current;
368 let y_new = self.y + self.eps * (self.x - self.j);
369 self.x = x_new;
370 self.y = y_new;
371 if self.x >= self.x_threshold && x_prev < self.x_threshold {
372 1
373 } else {
374 0
375 }
376 }
377 pub fn simulate(&mut self, n_steps: usize, current: f64) -> (Vec<f64>, i64) {
382 let mut trace = Vec::with_capacity(n_steps);
383 let mut spikes: i64 = 0;
384 for _ in 0..n_steps {
385 let spiked = self.step(current);
386 trace.push(self.x);
387 spikes += spiked as i64;
388 }
389 (trace, spikes)
390 }
391 pub fn reset(&mut self) {
392 self.x = 0.0;
393 self.y = 0.0;
394 }
395}
396impl Default for CourageNekorkinMapNeuron {
397 fn default() -> Self {
398 Self::new()
399 }
400}
401
402#[derive(Clone, Debug)]
413pub struct AiharaMapNeuron {
414 pub x: f64,
415 pub y: f64,
416 pub k_f: f64, pub k_s: f64, pub alpha: f64, pub delta: f64, pub x_threshold: f64,
421}
422
423impl Default for AiharaMapNeuron {
424 fn default() -> Self {
425 Self::new()
426 }
427}
428
429impl AiharaMapNeuron {
430 pub fn new() -> Self {
431 Self {
432 x: 0.0,
433 y: 0.0,
434 k_f: 0.7,
435 k_s: 0.95,
436 alpha: 2.0,
437 delta: 0.05,
438 x_threshold: 0.5,
439 }
440 }
441
442 pub fn step(&mut self, current: f64) -> i32 {
443 let x_prev = self.x;
444 let sigmoid = 1.0 / (1.0 + (-(self.x + self.alpha)).exp());
445 let x_new = self.k_f * self.x * sigmoid - self.y + current;
446 let y_new = self.k_s * self.y + self.delta * self.x;
447
448 self.x = x_new.clamp(-10.0, 10.0);
449 self.y = y_new.clamp(-10.0, 10.0);
450
451 if !self.x.is_finite() {
452 self.x = 0.0;
453 }
454 if !self.y.is_finite() {
455 self.y = 0.0;
456 }
457
458 if self.x >= self.x_threshold && x_prev < self.x_threshold {
459 1
460 } else {
461 0
462 }
463 }
464
465 pub fn reset(&mut self) {
466 *self = Self::new();
467 }
468}
469
470#[derive(Clone, Debug)]
481pub struct KilincBhattMapNeuron {
482 pub x: f64,
483 pub theta: f64, pub k: f64, pub beta: f64, pub gamma: f64, pub theta_spike: f64, pub x_threshold: f64,
489}
490
491impl Default for KilincBhattMapNeuron {
492 fn default() -> Self {
493 Self::new()
494 }
495}
496
497impl KilincBhattMapNeuron {
498 pub fn new() -> Self {
499 Self {
500 x: 0.0,
501 theta: 0.0,
502 k: 1.5,
503 beta: 0.95,
504 gamma: 0.3,
505 theta_spike: 0.8,
506 x_threshold: 0.8,
507 }
508 }
509
510 pub fn step(&mut self, current: f64) -> i32 {
511 let x_prev = self.x;
512 let sig = 1.0 / (1.0 + (-(self.x - self.theta) * 4.0).exp());
513 let x_new = -self.x + self.k * sig + current;
514 let spiked = if self.x >= self.theta_spike { 1.0 } else { 0.0 };
515 let theta_new = self.beta * self.theta + self.gamma * spiked;
516
517 self.x = x_new.clamp(-5.0, 5.0);
518 self.theta = theta_new.clamp(-5.0, 5.0);
519
520 if !self.x.is_finite() {
521 self.x = 0.0;
522 }
523 if !self.theta.is_finite() {
524 self.theta = 0.0;
525 }
526
527 if self.x >= self.x_threshold && x_prev < self.x_threshold {
528 1
529 } else {
530 0
531 }
532 }
533
534 pub fn reset(&mut self) {
535 *self = Self::new();
536 }
537}
538
539#[derive(Clone, Debug)]
547pub struct ErmentroutKopellMapNeuron {
548 pub theta: f64, pub dt: f64,
550 pub gain: f64,
551 pub theta_threshold: f64,
552}
553
554impl Default for ErmentroutKopellMapNeuron {
555 fn default() -> Self {
556 Self::new()
557 }
558}
559
560impl ErmentroutKopellMapNeuron {
561 pub fn new() -> Self {
562 Self {
563 theta: 0.0,
564 dt: 0.1, gain: 1.0,
566 theta_threshold: std::f64::consts::PI,
567 }
568 }
569
570 pub fn step(&mut self, current: f64) -> i32 {
571 let input = self.gain * current;
572 let theta_prev = self.theta;
573
574 let d_theta = (1.0 - self.theta.cos()) + (1.0 + self.theta.cos()) * input;
575 self.theta += self.dt * d_theta;
576
577 let fired = if self.theta >= self.theta_threshold && theta_prev < self.theta_threshold {
579 1
580 } else {
581 0
582 };
583
584 let two_pi = 2.0 * std::f64::consts::PI;
586 if self.theta >= two_pi {
587 self.theta -= two_pi;
588 }
589 if self.theta < 0.0 {
590 self.theta += two_pi;
591 }
592
593 if !self.theta.is_finite() {
594 self.theta = 0.0;
595 }
596
597 fired
598 }
599
600 pub fn simulate(&mut self, n_steps: usize, current: f64) -> (Vec<f64>, i64) {
607 let mut trace = Vec::with_capacity(n_steps);
608 let mut spikes: i64 = 0;
609 for _ in 0..n_steps {
610 let spiked = self.step(current);
611 trace.push(self.theta);
612 spikes += spiked as i64;
613 }
614 (trace, spikes)
615 }
616
617 pub fn reset(&mut self) {
618 *self = Self::new();
619 }
620}
621
622#[cfg(test)]
623mod tests {
624 use super::*;
625
626 #[test]
627 fn chialvo_fires() {
628 let mut n = ChialvoMapNeuron::new();
629 let t: i32 = (0..1000).map(|_| n.step(1.0)).sum();
630 assert!(t > 0);
631 }
632 #[test]
633 fn rulkov_fires() {
634 let mut n = RulkovMapNeuron::new();
635 let t: i32 = (0..2000).map(|_| n.step(0.5)).sum();
636 assert!(t > 0);
637 }
638 #[test]
639 fn ibarz_fires() {
640 let mut n = IbarzTanakaMapNeuron::new();
641 let t: i32 = (0..2000).map(|_| n.step(2.0)).sum();
642 assert!(t > 0);
643 }
644 #[test]
645 fn medvedev_fires() {
646 let mut n = MedvedevMapNeuron {
647 x: 0.5,
648 ..Default::default()
649 };
650 let t: i32 = (0..500).map(|_| n.step(0.1)).sum();
651 assert!(t > 0);
652 }
653 #[test]
654 fn cazelles_fires() {
655 let mut n = CazellesMapNeuron::new();
656 let t: i32 = (0..200).map(|_| n.step(0.0)).sum();
657 assert!(t > 0);
658 }
659 #[test]
660 fn cournekorkin_fires() {
661 let mut n = CourageNekorkinMapNeuron::new();
662 let t: i32 = (0..200).map(|_| n.step(0.5)).sum();
663 assert!(t > 0);
664 }
665
666 #[test]
669 fn aihara_fires_with_input() {
670 let mut n = AiharaMapNeuron::new();
671 let t: i32 = (0..2000).map(|_| n.step(1.0)).sum();
672 assert!(t > 0, "Aihara must fire with input, got {t}");
673 }
674
675 #[test]
676 fn aihara_silent_without_input() {
677 let mut n = AiharaMapNeuron::new();
678 let t: i32 = (0..5000).map(|_| n.step(0.0)).sum();
679 assert_eq!(t, 0, "Aihara must be silent without input, got {t}");
680 }
681
682 #[test]
683 fn aihara_chaotic_dynamics() {
684 let mut n = AiharaMapNeuron::new();
686 let mut values = Vec::new();
687 for _ in 0..1000 {
688 n.step(0.5);
689 values.push(n.x);
690 }
691 let mean = values.iter().sum::<f64>() / values.len() as f64;
692 let var = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / values.len() as f64;
693 assert!(
694 var > 0.001,
695 "Trajectory should show variability (chaos), var={var}"
696 );
697 }
698
699 #[test]
700 fn aihara_negative_input_no_crash() {
701 let mut n = AiharaMapNeuron::new();
702 for _ in 0..10_000 {
703 n.step(-100.0);
704 }
705 assert!(n.x.is_finite());
706 }
707
708 #[test]
709 fn aihara_nan_input_stays_finite() {
710 let mut n = AiharaMapNeuron::new();
711 n.step(f64::NAN);
712 assert!(n.x.is_finite());
713 }
714
715 #[test]
716 fn aihara_extreme_input_bounded() {
717 let mut n = AiharaMapNeuron::new();
718 for _ in 0..1000 {
719 n.step(1e6);
720 }
721 assert!(n.x.is_finite() && n.x <= 1e6);
722 }
723
724 #[test]
725 fn aihara_reset_clears_state() {
726 let mut n = AiharaMapNeuron::new();
727 for _ in 0..100 {
728 n.step(1.0);
729 }
730 n.reset();
731 assert_eq!(n.x, 0.0);
732 assert_eq!(n.y, 0.0);
733 }
734
735 #[test]
736 fn aihara_rate_increases_with_input() {
737 let mut low = AiharaMapNeuron::new();
738 let mut high = AiharaMapNeuron::new();
739 let spikes_low: i32 = (0..5000).map(|_| low.step(0.5)).sum();
740 let spikes_high: i32 = (0..5000).map(|_| high.step(2.0)).sum();
741 assert!(
742 spikes_high >= spikes_low,
743 "Higher input should produce more spikes: high={spikes_high} vs low={spikes_low}"
744 );
745 }
746
747 #[test]
748 fn aihara_performance_100k_steps() {
749 let start = std::time::Instant::now();
750 let mut n = AiharaMapNeuron::new();
751 for _ in 0..100_000 {
752 std::hint::black_box(n.step(0.5));
753 }
754 let elapsed = start.elapsed();
755 assert!(
756 elapsed.as_millis() < 50,
757 "100k steps must complete in <50ms"
758 );
759 }
760
761 #[test]
764 fn kb_fires_with_input() {
765 let mut n = KilincBhattMapNeuron::new();
766 let t: i32 = (0..5000).map(|_| n.step(1.0)).sum();
767 assert!(t > 0, "KB must fire with input, got {t}");
768 }
769
770 #[test]
771 fn kb_silent_without_input() {
772 let mut n = KilincBhattMapNeuron::new();
773 let t: i32 = (0..5000).map(|_| n.step(0.0)).sum();
774 assert_eq!(t, 0, "KB must be silent without input, got {t}");
775 }
776
777 #[test]
778 fn kb_adaptation() {
779 let mut n = KilincBhattMapNeuron::new();
781 let early: i32 = (0..2000).map(|_| n.step(1.0)).sum();
782 let late: i32 = (0..2000).map(|_| n.step(1.0)).sum();
783 assert!(
784 early >= late,
785 "Adaptation should slow firing: early={early}, late={late}"
786 );
787 }
788
789 #[test]
790 fn kb_theta_increases_during_spiking() {
791 let mut n = KilincBhattMapNeuron::new();
792 let theta_before = n.theta;
793 for _ in 0..5000 {
794 n.step(1.5);
795 }
796 assert!(
797 n.theta > theta_before,
798 "Theta must increase during spiking, theta={}",
799 n.theta
800 );
801 }
802
803 #[test]
804 fn kb_negative_input_no_crash() {
805 let mut n = KilincBhattMapNeuron::new();
806 for _ in 0..10_000 {
807 n.step(-100.0);
808 }
809 assert!(n.x.is_finite());
810 }
811
812 #[test]
813 fn kb_nan_input_stays_finite() {
814 let mut n = KilincBhattMapNeuron::new();
815 n.step(f64::NAN);
816 assert!(n.x.is_finite());
817 }
818
819 #[test]
820 fn kb_extreme_input_bounded() {
821 let mut n = KilincBhattMapNeuron::new();
822 for _ in 0..1000 {
823 n.step(1e6);
824 }
825 assert!(n.x.is_finite() && n.x <= 5.0);
826 }
827
828 #[test]
829 fn kb_reset_clears_state() {
830 let mut n = KilincBhattMapNeuron::new();
831 for _ in 0..100 {
832 n.step(1.0);
833 }
834 n.reset();
835 assert_eq!(n.x, 0.0);
836 assert_eq!(n.theta, 0.0);
837 }
838
839 #[test]
840 fn kb_performance_100k_steps() {
841 let start = std::time::Instant::now();
842 let mut n = KilincBhattMapNeuron::new();
843 for _ in 0..100_000 {
844 std::hint::black_box(n.step(0.5));
845 }
846 let elapsed = start.elapsed();
847 assert!(
848 elapsed.as_millis() < 50,
849 "100k steps must complete in <50ms"
850 );
851 }
852
853 #[test]
856 fn ek_fires_with_input() {
857 let mut n = ErmentroutKopellMapNeuron::new();
858 let t: i32 = (0..5000).map(|_| n.step(0.5)).sum();
859 assert!(t > 0, "EK must fire with input, got {t}");
860 }
861
862 #[test]
863 fn ek_silent_without_input() {
864 let mut n = ErmentroutKopellMapNeuron::new();
866 let t: i32 = (0..5000).map(|_| n.step(-0.1)).sum();
867 assert_eq!(t, 0, "EK must be silent with negative input, got {t}");
868 }
869
870 #[test]
871 fn ek_type_i_excitability() {
872 let mut n_low = ErmentroutKopellMapNeuron::new();
874 let mut n_high = ErmentroutKopellMapNeuron::new();
875 let spikes_low: i32 = (0..10_000).map(|_| n_low.step(0.01)).sum();
876 let spikes_high: i32 = (0..10_000).map(|_| n_high.step(1.0)).sum();
877 assert!(
878 spikes_high > spikes_low,
879 "Higher input → higher rate: high={spikes_high} vs low={spikes_low}"
880 );
881 }
882
883 #[test]
884 fn ek_theta_wraps() {
885 let mut n = ErmentroutKopellMapNeuron::new();
887 for _ in 0..10_000 {
888 n.step(0.5);
889 }
890 let two_pi = 2.0 * std::f64::consts::PI;
891 assert!(
892 n.theta >= 0.0 && n.theta < two_pi,
893 "Theta must wrap to [0, 2pi), theta={}",
894 n.theta
895 );
896 }
897
898 #[test]
899 fn ek_negative_input_no_crash() {
900 let mut n = ErmentroutKopellMapNeuron::new();
901 for _ in 0..10_000 {
902 n.step(-100.0);
903 }
904 assert!(n.theta.is_finite());
905 }
906
907 #[test]
908 fn ek_nan_input_stays_finite() {
909 let mut n = ErmentroutKopellMapNeuron::new();
910 n.step(f64::NAN);
911 assert!(n.theta.is_finite());
912 }
913
914 #[test]
915 fn ek_extreme_input_bounded() {
916 let mut n = ErmentroutKopellMapNeuron::new();
917 for _ in 0..1000 {
918 n.step(1e6);
919 }
920 assert!(n.theta.is_finite());
921 }
922
923 #[test]
924 fn ek_reset_clears_state() {
925 let mut n = ErmentroutKopellMapNeuron::new();
926 for _ in 0..100 {
927 n.step(0.5);
928 }
929 n.reset();
930 assert_eq!(n.theta, 0.0);
931 }
932
933 #[test]
934 fn ek_performance_100k_steps() {
935 let start = std::time::Instant::now();
936 let mut n = ErmentroutKopellMapNeuron::new();
937 for _ in 0..100_000 {
938 std::hint::black_box(n.step(0.5));
939 }
940 let elapsed = start.elapsed();
941 assert!(
942 elapsed.as_millis() < 50,
943 "100k steps must complete in <50ms"
944 );
945 }
946
947 #[test]
950 fn cn_default_sustained_bounded_spiking() {
951 let mut n = CourageNekorkinMapNeuron::new();
954 let mut spikes = 0i64;
955 let mut max_abs = 0.0f64;
956 for _ in 0..20_000 {
957 spikes += n.step(0.0) as i64;
958 max_abs = max_abs.max(n.x.abs());
959 }
960 assert!(spikes > 1000, "expected sustained spiking, got {spikes}");
961 assert!(
962 max_abs < 10.0,
963 "trajectory must stay bounded, got {max_abs}"
964 );
965 }
966
967 #[test]
968 fn cn_breakpoints_inside_region() {
969 let n = CourageNekorkinMapNeuron::new();
971 let am1 = n.a * n.m1;
972 let den = n.m0 + n.m1;
973 let jmin = am1 / den;
974 let jmax = (n.m0 + am1) / den;
975 assert!(jmin < n.d && n.d < jmax);
976 assert!(n.j > 0.0 && n.j < n.d);
977 assert!(n.m0 < 1.0);
978 }
979
980 #[test]
981 fn cn_f_piecewise_branches() {
982 let n = CourageNekorkinMapNeuron::new();
984 let am1 = n.a * n.m1;
985 let den = n.m0 + n.m1;
986 let jmin = am1 / den;
987 let jmax = (n.m0 + am1) / den;
988 let f = |x: f64| {
990 if x <= jmin {
991 -n.m0 * x
992 } else if x < jmax {
993 n.m1 * (x - n.a)
994 } else {
995 -n.m0 * (x - 1.0)
996 }
997 };
998 assert_eq!(f(jmin - 0.05), -n.m0 * (jmin - 0.05));
999 let mid = 0.5 * (jmin + jmax);
1000 assert_eq!(f(mid), n.m1 * (mid - n.a));
1001 assert_eq!(f(jmax + 0.05), -n.m0 * (jmax + 0.05 - 1.0));
1002 }
1003
1004 #[test]
1005 fn cn_heaviside_subtracts_beta_above_d() {
1006 let mut with_beta = CourageNekorkinMapNeuron::new();
1008 with_beta.x = 0.30; let mut no_beta = with_beta.clone();
1010 no_beta.beta = 0.0;
1011 with_beta.step(0.0);
1012 no_beta.step(0.0);
1013 assert!((with_beta.x - no_beta.x - (-0.085)).abs() < 1e-15);
1014 }
1015
1016 #[test]
1017 fn cn_simulate_matches_repeated_step() {
1018 let (trace, spikes) = CourageNekorkinMapNeuron::new().simulate(500, 0.0);
1019 let mut stepper = CourageNekorkinMapNeuron::new();
1020 let mut manual = Vec::with_capacity(500);
1021 let mut sp = 0i64;
1022 for _ in 0..500 {
1023 sp += stepper.step(0.0) as i64;
1024 manual.push(stepper.x);
1025 }
1026 assert_eq!(trace, manual);
1027 assert_eq!(spikes, sp);
1028 assert_eq!(
1029 stepper.x,
1030 CourageNekorkinMapNeuron::new().simulate(500, 0.0).0[499]
1031 );
1032 }
1033
1034 #[test]
1035 fn cn_reset_clears_state() {
1036 let mut n = CourageNekorkinMapNeuron::new();
1037 for _ in 0..100 {
1038 n.step(0.0);
1039 }
1040 n.reset();
1041 assert_eq!(n.x, 0.0);
1042 assert_eq!(n.y, 0.0);
1043 }
1044
1045 #[test]
1046 fn cn_deterministic() {
1047 let (a, sa) = CourageNekorkinMapNeuron::new().simulate(1000, 0.05);
1048 let (b, sb) = CourageNekorkinMapNeuron::new().simulate(1000, 0.05);
1049 assert_eq!(a, b);
1050 assert_eq!(sa, sb);
1051 }
1052
1053 #[test]
1054 fn cn_performance_100k_steps() {
1055 let start = std::time::Instant::now();
1056 let mut n = CourageNekorkinMapNeuron::new();
1057 for _ in 0..100_000 {
1058 std::hint::black_box(n.step(0.0));
1059 }
1060 let elapsed = start.elapsed();
1061 assert!(
1062 elapsed.as_millis() < 50,
1063 "100k steps must complete in <50ms"
1064 );
1065 }
1066}