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 reset(&mut self) {
99 self.x = -1.0;
100 self.y = -3.0;
101 }
102}
103impl Default for RulkovMapNeuron {
104 fn default() -> Self {
105 Self::new()
106 }
107}
108
109#[derive(Clone, Debug)]
111pub struct IbarzTanakaMapNeuron {
112 pub x: f64,
113 pub y: f64,
114 pub alpha: f64,
115 pub beta: f64,
116 pub mu: f64,
117 pub sigma: f64,
118 pub x_threshold: f64,
119 pub x_reset: f64,
120}
121
122impl IbarzTanakaMapNeuron {
123 pub fn new() -> Self {
124 Self {
125 x: -1.0,
126 y: -2.5,
127 alpha: 3.65,
128 beta: 0.25,
129 mu: 0.0005,
130 sigma: -1.6,
131 x_threshold: 3.0,
132 x_reset: -1.0,
133 }
134 }
135 pub fn step(&mut self, current: f64) -> i32 {
136 let f = if self.x <= 0.0 {
137 self.alpha / (1.0 - self.x)
138 } else {
139 self.alpha + self.beta * self.x
140 };
141 let x_new = f + self.y + current;
142 let y_new = self.y - self.mu * (self.x + 1.0) + self.mu * self.sigma;
143 self.x = x_new;
144 self.y = y_new;
145 if self.x >= self.x_threshold {
146 self.x = self.x_reset;
147 1
148 } else {
149 0
150 }
151 }
152 pub fn reset(&mut self) {
153 self.x = -1.0;
154 self.y = -2.5;
155 }
156}
157impl Default for IbarzTanakaMapNeuron {
158 fn default() -> Self {
159 Self::new()
160 }
161}
162
163#[derive(Clone, Debug)]
165pub struct MedvedevMapNeuron {
166 pub x: f64,
167 pub alpha: f64,
168 pub beta: f64,
169 pub x_threshold: f64,
170}
171
172impl MedvedevMapNeuron {
173 pub fn new() -> Self {
174 Self {
175 x: 0.0,
176 alpha: 3.5,
177 beta: 0.5,
178 x_threshold: 0.9,
179 }
180 }
181 pub fn step(&mut self, current: f64) -> i32 {
182 let x_prev = self.x;
183 self.x = if self.x < self.beta {
184 self.alpha * self.x + current
185 } else {
186 self.alpha * (1.0 - self.x) + current
187 };
188 self.x = self.x.rem_euclid(1.0);
189 if self.x >= self.x_threshold && x_prev < self.x_threshold {
190 1
191 } else {
192 0
193 }
194 }
195 pub fn reset(&mut self) {
196 self.x = 0.0;
197 }
198}
199impl Default for MedvedevMapNeuron {
200 fn default() -> Self {
201 Self::new()
202 }
203}
204
205#[derive(Clone, Debug)]
207pub struct CazellesMapNeuron {
208 pub x: f64,
209 pub y: f64,
210 pub a: f64,
211 pub epsilon: f64,
212 pub sigma: f64,
213 pub x_threshold: f64,
214}
215
216impl CazellesMapNeuron {
217 pub fn new() -> Self {
218 Self {
219 x: 0.1,
220 y: 0.0,
221 a: 3.8,
222 epsilon: 0.01,
223 sigma: 0.5,
224 x_threshold: 0.9,
225 }
226 }
227 pub fn step(&mut self, current: f64) -> i32 {
228 let f = self.a * self.x * (1.0 - self.x);
229 let x_new = (f - self.y + current).clamp(-2.0, 2.0);
230 let y_new = self.y + self.epsilon * (self.x - self.sigma);
231 self.x = x_new;
232 self.y = y_new;
233 if self.x >= self.x_threshold {
234 1
235 } else {
236 0
237 }
238 }
239 pub fn reset(&mut self) {
240 self.x = 0.1;
241 self.y = 0.0;
242 }
243}
244impl Default for CazellesMapNeuron {
245 fn default() -> Self {
246 Self::new()
247 }
248}
249
250#[derive(Clone, Debug)]
252pub struct CourageNekorkinMapNeuron {
253 pub x: f64,
254 pub y: f64,
255 pub alpha: f64,
256 pub beta: f64,
257 pub j: f64,
258 pub x_threshold: f64,
259}
260
261impl CourageNekorkinMapNeuron {
262 pub fn new() -> Self {
263 Self {
264 x: 0.0,
265 y: 0.0,
266 alpha: 3.0,
267 beta: 0.001,
268 j: 0.1,
269 x_threshold: 1.0,
270 }
271 }
272 pub fn step(&mut self, current: f64) -> i32 {
273 let x_prev = self.x;
274 let f = if self.x < 0.0 {
275 self.alpha * self.x
276 } else {
277 self.alpha * self.x / (1.0 + self.alpha * self.x)
278 };
279 let x_new = (f + self.y + current + self.j).clamp(-1e6, 1e6);
280 let y_new = (self.y - self.beta * (self.x + 1.0)).clamp(-1e6, 1e6);
281 self.x = if x_new.is_finite() { x_new } else { 0.0 };
282 self.y = if y_new.is_finite() { y_new } else { 0.0 };
283 if self.x >= self.x_threshold && x_prev < self.x_threshold {
284 1
285 } else {
286 0
287 }
288 }
289 pub fn reset(&mut self) {
290 self.x = 0.0;
291 self.y = 0.0;
292 }
293}
294impl Default for CourageNekorkinMapNeuron {
295 fn default() -> Self {
296 Self::new()
297 }
298}
299
300#[derive(Clone, Debug)]
311pub struct AiharaMapNeuron {
312 pub x: f64,
313 pub y: f64,
314 pub k_f: f64, pub k_s: f64, pub alpha: f64, pub delta: f64, pub x_threshold: f64,
319}
320
321impl Default for AiharaMapNeuron {
322 fn default() -> Self {
323 Self::new()
324 }
325}
326
327impl AiharaMapNeuron {
328 pub fn new() -> Self {
329 Self {
330 x: 0.0,
331 y: 0.0,
332 k_f: 0.7,
333 k_s: 0.95,
334 alpha: 2.0,
335 delta: 0.05,
336 x_threshold: 0.5,
337 }
338 }
339
340 pub fn step(&mut self, current: f64) -> i32 {
341 let x_prev = self.x;
342 let sigmoid = 1.0 / (1.0 + (-(self.x + self.alpha)).exp());
343 let x_new = self.k_f * self.x * sigmoid - self.y + current;
344 let y_new = self.k_s * self.y + self.delta * self.x;
345
346 self.x = x_new.clamp(-10.0, 10.0);
347 self.y = y_new.clamp(-10.0, 10.0);
348
349 if !self.x.is_finite() {
350 self.x = 0.0;
351 }
352 if !self.y.is_finite() {
353 self.y = 0.0;
354 }
355
356 if self.x >= self.x_threshold && x_prev < self.x_threshold {
357 1
358 } else {
359 0
360 }
361 }
362
363 pub fn reset(&mut self) {
364 *self = Self::new();
365 }
366}
367
368#[derive(Clone, Debug)]
379pub struct KilincBhattMapNeuron {
380 pub x: f64,
381 pub theta: f64, pub k: f64, pub beta: f64, pub gamma: f64, pub theta_spike: f64, pub x_threshold: f64,
387}
388
389impl Default for KilincBhattMapNeuron {
390 fn default() -> Self {
391 Self::new()
392 }
393}
394
395impl KilincBhattMapNeuron {
396 pub fn new() -> Self {
397 Self {
398 x: 0.0,
399 theta: 0.0,
400 k: 1.5,
401 beta: 0.95,
402 gamma: 0.3,
403 theta_spike: 0.8,
404 x_threshold: 0.8,
405 }
406 }
407
408 pub fn step(&mut self, current: f64) -> i32 {
409 let x_prev = self.x;
410 let sig = 1.0 / (1.0 + (-(self.x - self.theta) * 4.0).exp());
411 let x_new = -self.x + self.k * sig + current;
412 let spiked = if self.x >= self.theta_spike { 1.0 } else { 0.0 };
413 let theta_new = self.beta * self.theta + self.gamma * spiked;
414
415 self.x = x_new.clamp(-5.0, 5.0);
416 self.theta = theta_new.clamp(-5.0, 5.0);
417
418 if !self.x.is_finite() {
419 self.x = 0.0;
420 }
421 if !self.theta.is_finite() {
422 self.theta = 0.0;
423 }
424
425 if self.x >= self.x_threshold && x_prev < self.x_threshold {
426 1
427 } else {
428 0
429 }
430 }
431
432 pub fn reset(&mut self) {
433 *self = Self::new();
434 }
435}
436
437#[derive(Clone, Debug)]
445pub struct ErmentroutKopellMapNeuron {
446 pub theta: f64, pub dt: f64,
448 pub gain: f64,
449 pub theta_threshold: f64,
450}
451
452impl Default for ErmentroutKopellMapNeuron {
453 fn default() -> Self {
454 Self::new()
455 }
456}
457
458impl ErmentroutKopellMapNeuron {
459 pub fn new() -> Self {
460 Self {
461 theta: 0.0,
462 dt: 0.1, gain: 1.0,
464 theta_threshold: std::f64::consts::PI,
465 }
466 }
467
468 pub fn step(&mut self, current: f64) -> i32 {
469 let input = self.gain * current;
470 let theta_prev = self.theta;
471
472 let d_theta = (1.0 - self.theta.cos()) + (1.0 + self.theta.cos()) * input;
473 self.theta += self.dt * d_theta;
474
475 let fired = if self.theta >= self.theta_threshold && theta_prev < self.theta_threshold {
477 1
478 } else {
479 0
480 };
481
482 let two_pi = 2.0 * std::f64::consts::PI;
484 if self.theta >= two_pi {
485 self.theta -= two_pi;
486 }
487 if self.theta < 0.0 {
488 self.theta += two_pi;
489 }
490
491 if !self.theta.is_finite() {
492 self.theta = 0.0;
493 }
494
495 fired
496 }
497
498 pub fn reset(&mut self) {
499 *self = Self::new();
500 }
501}
502
503#[cfg(test)]
504mod tests {
505 use super::*;
506
507 #[test]
508 fn chialvo_fires() {
509 let mut n = ChialvoMapNeuron::new();
510 let t: i32 = (0..1000).map(|_| n.step(1.0)).sum();
511 assert!(t > 0);
512 }
513 #[test]
514 fn rulkov_fires() {
515 let mut n = RulkovMapNeuron::new();
516 let t: i32 = (0..2000).map(|_| n.step(0.5)).sum();
517 assert!(t > 0);
518 }
519 #[test]
520 fn ibarz_fires() {
521 let mut n = IbarzTanakaMapNeuron::new();
522 let t: i32 = (0..2000).map(|_| n.step(2.0)).sum();
523 assert!(t > 0);
524 }
525 #[test]
526 fn medvedev_fires() {
527 let mut n = MedvedevMapNeuron {
528 x: 0.5,
529 ..Default::default()
530 };
531 let t: i32 = (0..500).map(|_| n.step(0.1)).sum();
532 assert!(t > 0);
533 }
534 #[test]
535 fn cazelles_fires() {
536 let mut n = CazellesMapNeuron::new();
537 let t: i32 = (0..200).map(|_| n.step(0.0)).sum();
538 assert!(t > 0);
539 }
540 #[test]
541 fn cournekorkin_fires() {
542 let mut n = CourageNekorkinMapNeuron::new();
543 let t: i32 = (0..200).map(|_| n.step(0.5)).sum();
544 assert!(t > 0);
545 }
546
547 #[test]
550 fn aihara_fires_with_input() {
551 let mut n = AiharaMapNeuron::new();
552 let t: i32 = (0..2000).map(|_| n.step(1.0)).sum();
553 assert!(t > 0, "Aihara must fire with input, got {t}");
554 }
555
556 #[test]
557 fn aihara_silent_without_input() {
558 let mut n = AiharaMapNeuron::new();
559 let t: i32 = (0..5000).map(|_| n.step(0.0)).sum();
560 assert_eq!(t, 0, "Aihara must be silent without input, got {t}");
561 }
562
563 #[test]
564 fn aihara_chaotic_dynamics() {
565 let mut n = AiharaMapNeuron::new();
567 let mut values = Vec::new();
568 for _ in 0..1000 {
569 n.step(0.5);
570 values.push(n.x);
571 }
572 let mean = values.iter().sum::<f64>() / values.len() as f64;
573 let var = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / values.len() as f64;
574 assert!(
575 var > 0.001,
576 "Trajectory should show variability (chaos), var={var}"
577 );
578 }
579
580 #[test]
581 fn aihara_negative_input_no_crash() {
582 let mut n = AiharaMapNeuron::new();
583 for _ in 0..10_000 {
584 n.step(-100.0);
585 }
586 assert!(n.x.is_finite());
587 }
588
589 #[test]
590 fn aihara_nan_input_stays_finite() {
591 let mut n = AiharaMapNeuron::new();
592 n.step(f64::NAN);
593 assert!(n.x.is_finite());
594 }
595
596 #[test]
597 fn aihara_extreme_input_bounded() {
598 let mut n = AiharaMapNeuron::new();
599 for _ in 0..1000 {
600 n.step(1e6);
601 }
602 assert!(n.x.is_finite() && n.x <= 1e6);
603 }
604
605 #[test]
606 fn aihara_reset_clears_state() {
607 let mut n = AiharaMapNeuron::new();
608 for _ in 0..100 {
609 n.step(1.0);
610 }
611 n.reset();
612 assert_eq!(n.x, 0.0);
613 assert_eq!(n.y, 0.0);
614 }
615
616 #[test]
617 fn aihara_rate_increases_with_input() {
618 let mut low = AiharaMapNeuron::new();
619 let mut high = AiharaMapNeuron::new();
620 let spikes_low: i32 = (0..5000).map(|_| low.step(0.5)).sum();
621 let spikes_high: i32 = (0..5000).map(|_| high.step(2.0)).sum();
622 assert!(
623 spikes_high >= spikes_low,
624 "Higher input should produce more spikes: high={spikes_high} vs low={spikes_low}"
625 );
626 }
627
628 #[test]
629 fn aihara_performance_100k_steps() {
630 let start = std::time::Instant::now();
631 let mut n = AiharaMapNeuron::new();
632 for _ in 0..100_000 {
633 std::hint::black_box(n.step(0.5));
634 }
635 let elapsed = start.elapsed();
636 assert!(
637 elapsed.as_millis() < 50,
638 "100k steps must complete in <50ms"
639 );
640 }
641
642 #[test]
645 fn kb_fires_with_input() {
646 let mut n = KilincBhattMapNeuron::new();
647 let t: i32 = (0..5000).map(|_| n.step(1.0)).sum();
648 assert!(t > 0, "KB must fire with input, got {t}");
649 }
650
651 #[test]
652 fn kb_silent_without_input() {
653 let mut n = KilincBhattMapNeuron::new();
654 let t: i32 = (0..5000).map(|_| n.step(0.0)).sum();
655 assert_eq!(t, 0, "KB must be silent without input, got {t}");
656 }
657
658 #[test]
659 fn kb_adaptation() {
660 let mut n = KilincBhattMapNeuron::new();
662 let early: i32 = (0..2000).map(|_| n.step(1.0)).sum();
663 let late: i32 = (0..2000).map(|_| n.step(1.0)).sum();
664 assert!(
665 early >= late,
666 "Adaptation should slow firing: early={early}, late={late}"
667 );
668 }
669
670 #[test]
671 fn kb_theta_increases_during_spiking() {
672 let mut n = KilincBhattMapNeuron::new();
673 let theta_before = n.theta;
674 for _ in 0..5000 {
675 n.step(1.5);
676 }
677 assert!(
678 n.theta > theta_before,
679 "Theta must increase during spiking, theta={}",
680 n.theta
681 );
682 }
683
684 #[test]
685 fn kb_negative_input_no_crash() {
686 let mut n = KilincBhattMapNeuron::new();
687 for _ in 0..10_000 {
688 n.step(-100.0);
689 }
690 assert!(n.x.is_finite());
691 }
692
693 #[test]
694 fn kb_nan_input_stays_finite() {
695 let mut n = KilincBhattMapNeuron::new();
696 n.step(f64::NAN);
697 assert!(n.x.is_finite());
698 }
699
700 #[test]
701 fn kb_extreme_input_bounded() {
702 let mut n = KilincBhattMapNeuron::new();
703 for _ in 0..1000 {
704 n.step(1e6);
705 }
706 assert!(n.x.is_finite() && n.x <= 5.0);
707 }
708
709 #[test]
710 fn kb_reset_clears_state() {
711 let mut n = KilincBhattMapNeuron::new();
712 for _ in 0..100 {
713 n.step(1.0);
714 }
715 n.reset();
716 assert_eq!(n.x, 0.0);
717 assert_eq!(n.theta, 0.0);
718 }
719
720 #[test]
721 fn kb_performance_100k_steps() {
722 let start = std::time::Instant::now();
723 let mut n = KilincBhattMapNeuron::new();
724 for _ in 0..100_000 {
725 std::hint::black_box(n.step(0.5));
726 }
727 let elapsed = start.elapsed();
728 assert!(
729 elapsed.as_millis() < 50,
730 "100k steps must complete in <50ms"
731 );
732 }
733
734 #[test]
737 fn ek_fires_with_input() {
738 let mut n = ErmentroutKopellMapNeuron::new();
739 let t: i32 = (0..5000).map(|_| n.step(0.5)).sum();
740 assert!(t > 0, "EK must fire with input, got {t}");
741 }
742
743 #[test]
744 fn ek_silent_without_input() {
745 let mut n = ErmentroutKopellMapNeuron::new();
747 let t: i32 = (0..5000).map(|_| n.step(-0.1)).sum();
748 assert_eq!(t, 0, "EK must be silent with negative input, got {t}");
749 }
750
751 #[test]
752 fn ek_type_i_excitability() {
753 let mut n_low = ErmentroutKopellMapNeuron::new();
755 let mut n_high = ErmentroutKopellMapNeuron::new();
756 let spikes_low: i32 = (0..10_000).map(|_| n_low.step(0.01)).sum();
757 let spikes_high: i32 = (0..10_000).map(|_| n_high.step(1.0)).sum();
758 assert!(
759 spikes_high > spikes_low,
760 "Higher input → higher rate: high={spikes_high} vs low={spikes_low}"
761 );
762 }
763
764 #[test]
765 fn ek_theta_wraps() {
766 let mut n = ErmentroutKopellMapNeuron::new();
768 for _ in 0..10_000 {
769 n.step(0.5);
770 }
771 let two_pi = 2.0 * std::f64::consts::PI;
772 assert!(
773 n.theta >= 0.0 && n.theta < two_pi,
774 "Theta must wrap to [0, 2pi), theta={}",
775 n.theta
776 );
777 }
778
779 #[test]
780 fn ek_negative_input_no_crash() {
781 let mut n = ErmentroutKopellMapNeuron::new();
782 for _ in 0..10_000 {
783 n.step(-100.0);
784 }
785 assert!(n.theta.is_finite());
786 }
787
788 #[test]
789 fn ek_nan_input_stays_finite() {
790 let mut n = ErmentroutKopellMapNeuron::new();
791 n.step(f64::NAN);
792 assert!(n.theta.is_finite());
793 }
794
795 #[test]
796 fn ek_extreme_input_bounded() {
797 let mut n = ErmentroutKopellMapNeuron::new();
798 for _ in 0..1000 {
799 n.step(1e6);
800 }
801 assert!(n.theta.is_finite());
802 }
803
804 #[test]
805 fn ek_reset_clears_state() {
806 let mut n = ErmentroutKopellMapNeuron::new();
807 for _ in 0..100 {
808 n.step(0.5);
809 }
810 n.reset();
811 assert_eq!(n.theta, 0.0);
812 }
813
814 #[test]
815 fn ek_performance_100k_steps() {
816 let start = std::time::Instant::now();
817 let mut n = ErmentroutKopellMapNeuron::new();
818 for _ in 0..100_000 {
819 std::hint::black_box(n.step(0.5));
820 }
821 let elapsed = start.elapsed();
822 assert!(
823 elapsed.as_millis() < 50,
824 "100k steps must complete in <50ms"
825 );
826 }
827
828 #[test]
831 fn cn_silent_without_input() {
832 let mut n = CourageNekorkinMapNeuron::new();
833 let _t: i32 = (0..5000).map(|_| n.step(0.0)).sum();
834 assert!(n.x.is_finite());
836 }
837
838 #[test]
839 fn cn_negative_input_no_crash() {
840 let mut n = CourageNekorkinMapNeuron::new();
841 for _ in 0..10_000 {
842 n.step(-100.0);
843 }
844 assert!(n.x.is_finite());
845 assert!(n.x >= -1e6);
846 }
847
848 #[test]
849 fn cn_nan_input_stays_finite() {
850 let mut n = CourageNekorkinMapNeuron::new();
851 n.step(f64::NAN);
852 assert!(n.x.is_finite());
853 }
854
855 #[test]
856 fn cn_extreme_input_bounded() {
857 let mut n = CourageNekorkinMapNeuron::new();
858 for _ in 0..1000 {
859 n.step(1e6);
860 }
861 assert!(n.x.is_finite() && n.x <= 1e6);
862 }
863
864 #[test]
865 fn cn_reset_clears_state() {
866 let mut n = CourageNekorkinMapNeuron::new();
867 for _ in 0..100 {
868 n.step(0.5);
869 }
870 n.reset();
871 assert_eq!(n.x, 0.0);
872 assert_eq!(n.y, 0.0);
873 }
874
875 #[test]
876 fn cn_saturation_function() {
877 let mut n = CourageNekorkinMapNeuron::new();
879 n.x = 0.5;
880 let _ = n.step(0.0);
882 assert!(n.x.is_finite());
883 }
884
885 #[test]
886 fn cn_rate_increases_with_input() {
887 let mut low = CourageNekorkinMapNeuron::new();
888 let mut high = CourageNekorkinMapNeuron::new();
889 let sp_low: i32 = (0..5000).map(|_| low.step(0.0)).sum();
890 let sp_high: i32 = (0..5000).map(|_| high.step(1.0)).sum();
891 assert!(
892 sp_high >= sp_low,
893 "Higher input should fire more: high={sp_high} vs low={sp_low}"
894 );
895 }
896
897 #[test]
898 fn cn_performance_100k_steps() {
899 let start = std::time::Instant::now();
900 let mut n = CourageNekorkinMapNeuron::new();
901 for _ in 0..100_000 {
902 std::hint::black_box(n.step(0.5));
903 }
904 let elapsed = start.elapsed();
905 assert!(
906 elapsed.as_millis() < 50,
907 "100k steps must complete in <50ms"
908 );
909 }
910}