1#[inline]
18pub fn mask(value: i32, width: u32) -> i16 {
19 assert!(
20 width > 0 && width <= 32,
21 "mask width must be 1..=32, got {width}"
22 );
23 let m = (1_i64 << width) - 1;
24 let v = (value as i64) & m;
25 let shift = 64 - width;
26 ((v << shift) >> shift) as i16
27}
28
29#[derive(Clone, Debug)]
31pub struct FixedPointLif {
32 pub v: i16,
34 pub refractory_counter: i32,
36 pub data_width: u32,
38 pub fraction: u32,
40 pub v_rest: i16,
42 pub v_reset: i16,
44 pub v_threshold: i16,
46 pub refractory_period: i32,
48}
49
50impl FixedPointLif {
51 pub fn new(
53 data_width: u32,
54 fraction: u32,
55 v_rest: i16,
56 v_reset: i16,
57 v_threshold: i16,
58 refractory_period: i32,
59 ) -> Self {
60 Self {
61 v: v_rest,
62 refractory_counter: 0,
63 data_width,
64 fraction,
65 v_rest,
66 v_reset,
67 v_threshold,
68 refractory_period,
69 }
70 }
71
72 #[allow(non_snake_case)]
76 pub fn step(&mut self, leak_k: i16, gain_k: i16, i_t: i16, noise_in: i16) -> (i32, i16) {
77 let w = self.data_width;
78
79 if self.refractory_counter > 0 {
81 self.refractory_counter -= 1;
82 self.v = self.v_rest;
83 return (0, mask(self.v_rest as i32, w));
84 }
85
86 let diff = mask((self.v_rest as i32) - (self.v as i32), 2 * w) as i32;
87 let dv_leak = mask((diff * (leak_k as i32)) >> self.fraction, self.data_width);
88 let dv_in = mask(
89 ((i_t as i32) * (gain_k as i32)) >> self.fraction,
90 self.data_width,
91 );
92
93 let v_next = mask(
94 (self.v as i32) + (dv_leak as i32) + (dv_in as i32) + (noise_in as i32),
95 self.data_width,
96 );
97
98 if v_next >= self.v_threshold {
99 self.v = self.v_reset;
100 self.refractory_counter = self.refractory_period;
101 (1, mask(self.v_reset as i32, w))
102 } else {
103 self.v = v_next;
104 (0, mask(v_next as i32, w))
105 }
106 }
107
108 pub fn reset(&mut self) {
110 self.v = self.v_rest;
111 self.refractory_counter = 0;
112 }
113}
114
115#[derive(Clone, Debug)]
122pub struct Izhikevich {
123 pub v: f64,
124 pub u: f64,
125 pub a: f64,
126 pub b: f64,
127 pub c: f64,
128 pub d: f64,
129 pub dt: f64,
130}
131
132impl Izhikevich {
133 pub fn new(a: f64, b: f64, c: f64, d: f64, dt: f64) -> Self {
135 Self {
136 v: c,
137 u: b * c,
138 a,
139 b,
140 c,
141 d,
142 dt,
143 }
144 }
145
146 pub fn regular_spiking() -> Self {
148 Self::new(0.02, 0.2, -65.0, 8.0, 1.0)
149 }
150
151 pub fn step(&mut self, current: f64) -> i32 {
153 let half = self.dt * 0.5;
155 for _ in 0..2 {
156 let dv = (0.04 * self.v * self.v + 5.0 * self.v + 140.0 - self.u + current) * half;
157 let du = (self.a * (self.b * self.v - self.u)) * half;
158 self.v += dv;
159 self.u += du;
160 }
161
162 if self.v >= 30.0 {
163 self.v = self.c;
164 self.u += self.d;
165 1
166 } else {
167 0
168 }
169 }
170
171 pub fn reset(&mut self) {
173 self.v = self.c;
174 self.u = self.b * self.c;
175 }
176}
177
178#[derive(Clone, Debug)]
182pub struct BitstreamAverager {
183 buffer: Vec<u8>,
184 index: usize,
185 filled: bool,
186 running_sum: u64,
187}
188
189impl BitstreamAverager {
190 pub fn new(window: usize) -> Self {
191 assert!(window > 0, "window must be > 0");
192 Self {
193 buffer: vec![0; window],
194 index: 0,
195 filled: false,
196 running_sum: 0,
197 }
198 }
199
200 pub fn push(&mut self, bit: u8) {
201 debug_assert!(bit <= 1, "bit must be 0 or 1");
202 let old = self.buffer[self.index];
203 self.buffer[self.index] = bit;
204
205 if self.filled {
206 self.running_sum = self.running_sum - old as u64 + bit as u64;
207 } else {
208 self.running_sum += bit as u64;
209 }
210
211 self.index += 1;
212 if self.index == self.buffer.len() {
213 self.index = 0;
214 self.filled = true;
215 }
216 }
217
218 pub fn estimate(&self) -> f64 {
219 if !self.filled {
220 if self.index == 0 {
221 return 0.0;
222 }
223 return self.running_sum as f64 / self.index as f64;
224 }
225 self.running_sum as f64 / self.buffer.len() as f64
226 }
227
228 pub fn reset(&mut self) {
229 self.buffer.fill(0);
230 self.index = 0;
231 self.filled = false;
232 self.running_sum = 0;
233 }
234
235 pub fn window(&self) -> usize {
236 self.buffer.len()
237 }
238}
239
240#[derive(Clone, Debug)]
245pub struct HomeostaticLif {
246 pub v: f64,
247 pub v_threshold: f64,
248 pub v_rest: f64,
249 pub v_reset: f64,
250 pub rate_trace: f64,
251 pub target_rate: f64,
252 pub adaptation_rate: f64,
253 pub trace_decay: f64,
254 initial_threshold: f64,
255}
256
257impl HomeostaticLif {
258 pub fn new(target_rate: f64, adaptation_rate: f64, trace_decay: f64) -> Self {
259 Self {
260 v: 0.0,
261 v_threshold: 1.0,
262 v_rest: 0.0,
263 v_reset: 0.0,
264 rate_trace: 0.0,
265 target_rate,
266 adaptation_rate,
267 trace_decay,
268 initial_threshold: 1.0,
269 }
270 }
271
272 pub fn with_defaults() -> Self {
273 Self::new(0.1, 0.01, 0.95)
274 }
275
276 pub fn step(&mut self, current: f64) -> i32 {
278 let tau = 20.0;
280 self.v += (-(self.v - self.v_rest) + current) / tau;
281
282 let spike = if self.v >= self.v_threshold {
283 self.v = self.v_reset;
284 1
285 } else {
286 0
287 };
288
289 self.rate_trace =
291 self.rate_trace * self.trace_decay + spike as f64 * (1.0 - self.trace_decay);
292
293 let error = self.rate_trace - self.target_rate;
295 self.v_threshold += self.adaptation_rate * error;
296 self.v_threshold = self.v_threshold.clamp(0.1, self.initial_threshold * 10.0);
297
298 spike
299 }
300
301 pub fn reset(&mut self) {
302 self.v = self.v_rest;
303 self.rate_trace = 0.0;
304 self.v_threshold = self.initial_threshold;
305 }
306}
307
308#[derive(Clone, Debug)]
313pub struct DendriticNeuron {
314 pub threshold: f64,
315 last_current: f64,
316}
317
318impl DendriticNeuron {
319 pub fn new(threshold: f64) -> Self {
320 Self {
321 threshold,
322 last_current: 0.0,
323 }
324 }
325
326 pub fn with_defaults() -> Self {
327 Self::new(0.5)
328 }
329
330 pub fn step(&mut self, input_a: f64, input_b: f64) -> i32 {
331 self.last_current = input_a + input_b - 2.0 * input_a * input_b;
332 if self.last_current > self.threshold {
333 1
334 } else {
335 0
336 }
337 }
338
339 pub fn reset(&mut self) {
340 self.last_current = 0.0;
341 }
342}
343
344#[derive(Clone, Debug)]
347pub struct AdExNeuron {
348 pub v: f64,
349 pub w: f64,
350 pub v_rest: f64,
351 pub v_reset: f64,
352 pub v_threshold: f64,
353 pub v_rh: f64,
354 pub delta_t: f64,
355 pub tau: f64,
356 pub tau_w: f64,
357 pub a: f64,
358 pub b: f64,
359 pub c_m: f64,
360 pub dt: f64,
361}
362
363impl Default for AdExNeuron {
364 fn default() -> Self {
365 Self::new()
366 }
367}
368
369impl AdExNeuron {
370 pub fn new() -> Self {
371 Self {
372 v: -65.0,
373 w: 0.0,
374 v_rest: -65.0,
375 v_reset: -68.0,
376 v_threshold: -50.0,
377 v_rh: -55.0,
378 delta_t: 2.0,
379 tau: 20.0,
380 tau_w: 100.0,
381 a: 0.5,
382 b: 7.0,
383 c_m: 200.0,
384 dt: 0.1,
385 }
386 }
387
388 pub fn step(&mut self, current: f64) -> i32 {
389 let exp_arg = ((self.v - self.v_rh) / self.delta_t).clamp(-20.0, 20.0);
391 let exp_term = self.delta_t * exp_arg.exp();
392 let dv = ((-(self.v - self.v_rest) + exp_term) / self.tau + (-self.w + current) / self.c_m)
393 * self.dt;
394 let dw = (self.a * (self.v - self.v_rest) - self.w) / self.tau_w * self.dt;
395 self.v += dv;
396 self.w += dw;
397
398 if self.v >= self.v_threshold {
399 self.v = self.v_reset;
400 self.w += self.b;
401 1
402 } else {
403 0
404 }
405 }
406
407 pub fn reset(&mut self) {
408 self.v = self.v_rest;
409 self.w = 0.0;
410 }
411}
412
413#[derive(Clone, Debug)]
415pub struct ExpIfNeuron {
416 pub v: f64,
417 pub v_rest: f64,
418 pub v_reset: f64,
419 pub v_threshold: f64,
420 pub v_rh: f64,
421 pub delta_t: f64,
422 pub tau: f64,
423 pub dt: f64,
424 pub inv_delta_t: f64,
426 pub dt_div_tau: f64,
428}
429
430impl Default for ExpIfNeuron {
431 fn default() -> Self {
432 Self::new()
433 }
434}
435
436impl ExpIfNeuron {
437 pub fn new() -> Self {
438 Self {
439 v: -65.0,
440 v_rest: -65.0,
441 v_reset: -68.0,
442 v_threshold: -50.0,
443 v_rh: -55.0,
444 delta_t: 2.0,
445 tau: 20.0,
446 dt: 0.1,
447 inv_delta_t: 1.0 / 2.0,
448 dt_div_tau: 0.1 / 20.0,
449 }
450 }
451
452 pub fn step(&mut self, current: f64) -> i32 {
453 if !self.v.is_finite()
454 || !current.is_finite()
455 || !self.v_rest.is_finite()
456 || !self.v_reset.is_finite()
457 || !self.v_threshold.is_finite()
458 || !self.v_rh.is_finite()
459 || !self.delta_t.is_finite()
460 || !self.tau.is_finite()
461 || !self.dt.is_finite()
462 || self.delta_t <= 0.0
463 || self.tau <= 0.0
464 || self.dt <= 0.0
465 {
466 return 0;
467 }
468
469 self.inv_delta_t = 1.0 / self.delta_t;
470 self.dt_div_tau = self.dt / self.tau;
471 let k1 = self.rhs(self.v, current);
472 let k2 = self.rhs(self.v + 0.5 * self.dt * k1, current);
473 let k3 = self.rhs(self.v + 0.5 * self.dt * k2, current);
474 let k4 = self.rhs(self.v + self.dt * k3, current);
475 let next_v = self.v + self.dt * (k1 + 2.0 * k2 + 2.0 * k3 + k4) / 6.0;
476 if !k1.is_finite()
477 || !k2.is_finite()
478 || !k3.is_finite()
479 || !k4.is_finite()
480 || !next_v.is_finite()
481 {
482 return 0;
483 }
484 self.v = next_v;
485
486 if self.v >= self.v_threshold {
487 self.v = self.v_reset;
488 1
489 } else {
490 0
491 }
492 }
493
494 pub fn reset(&mut self) {
495 self.v = self.v_rest;
496 }
497
498 fn rhs(&self, v: f64, current: f64) -> f64 {
499 let exp_arg = ((v - self.v_rh) * self.inv_delta_t).clamp(-20.0, 20.0);
500 let exp_term = self.delta_t * exp_arg.exp();
501 (-(v - self.v_rest) + exp_term + current) / self.tau
502 }
503}
504
505#[derive(Clone, Debug)]
507pub struct LapicqueNeuron {
508 pub v: f64,
509 pub v_rest: f64,
510 pub v_reset: f64,
511 pub v_threshold: f64,
512 pub tau: f64,
513 pub resistance: f64,
514 pub dt: f64,
515}
516
517impl LapicqueNeuron {
518 pub fn new(tau: f64, resistance: f64, threshold: f64, dt: f64) -> Self {
519 Self {
520 v: 0.0,
521 v_rest: 0.0,
522 v_reset: 0.0,
523 v_threshold: threshold,
524 tau,
525 resistance,
526 dt,
527 }
528 }
529
530 pub fn step(&mut self, current: f64) -> i32 {
531 if !self.v.is_finite()
532 || !self.v_rest.is_finite()
533 || !self.v_reset.is_finite()
534 || !self.v_threshold.is_finite()
535 || self.v_threshold <= self.v_rest
536 || self.v_threshold <= self.v_reset
537 || self.v >= self.v_threshold
538 || !self.tau.is_finite()
539 || self.tau <= 0.0
540 || !self.resistance.is_finite()
541 || self.resistance <= 0.0
542 || !self.dt.is_finite()
543 || self.dt <= 0.0
544 || !current.is_finite()
545 {
546 return 0;
547 }
548
549 let v_inf = self.v_rest + self.resistance * current;
550 let decay = (-self.dt / self.tau).exp();
551 let next_v = v_inf + (self.v - v_inf) * decay;
552 if !v_inf.is_finite() || !decay.is_finite() || !next_v.is_finite() {
553 return 0;
554 }
555 self.v = next_v;
556
557 if self.v >= self.v_threshold {
558 self.v = self.v_reset;
559 1
560 } else {
561 0
562 }
563 }
564
565 pub fn reset(&mut self) {
566 self.v = self.v_rest;
567 }
568}
569
570#[cfg(test)]
571mod tests {
572
573 #[test]
574 fn test_exp_if_optimisation_parity() {
575 let mut n = ExpIfNeuron::new();
576 n.v = -60.0;
577 let current = 10.0;
578
579 let rhs = |v: f64| {
580 let exp_arg = ((v - n.v_rh) / n.delta_t).clamp(-20.0, 20.0);
581 (-(v - n.v_rest) + n.delta_t * exp_arg.exp() + current) / n.tau
582 };
583 let k1 = rhs(n.v);
584 let k2 = rhs(n.v + 0.5 * n.dt * k1);
585 let k3 = rhs(n.v + 0.5 * n.dt * k2);
586 let k4 = rhs(n.v + n.dt * k3);
587 let expected_dv = n.dt * (k1 + 2.0 * k2 + 2.0 * k3 + k4) / 6.0;
588
589 n.step(current);
590 let got_dv = n.v - (-60.0); assert!(
594 (got_dv - expected_dv).abs() < 1e-12,
595 "Logic mismatch in ExpIfNeuron: got {}, expected {}",
596 got_dv,
597 expected_dv
598 );
599 }
600
601 use super::{
602 mask, AdExNeuron, BitstreamAverager, DendriticNeuron, ExpIfNeuron, FixedPointLif,
603 HomeostaticLif, Izhikevich, LapicqueNeuron,
604 };
605
606 #[test]
607 fn mask_branchless_matches_original() {
608 for &width in &[16_u32, 32] {
609 for value in [
610 -32768_i32,
611 -1,
612 0,
613 1,
614 32767,
615 65535,
616 -65536,
617 i16::MAX as i32,
618 i16::MIN as i32,
619 ] {
620 let result = mask(value, width);
621
622 let m = (1_i64 << width) - 1;
623 let mut v = (value as i64) & m;
624 if v >= (1_i64 << (width - 1)) {
625 v -= 1_i64 << width;
626 }
627 let expected = if width >= 32 {
628 v as i32 as i16
629 } else {
630 v as i16
631 };
632
633 assert_eq!(
634 result, expected,
635 "mask({value}, {width}): got {result}, expected {expected}"
636 );
637 }
638 }
639 }
640
641 #[test]
642 fn lif_fires_with_refractory_period() {
643 let mut n = FixedPointLif::new(16, 8, 0, 0, 256, 2);
645 let mut spikes = Vec::new();
646 for _ in 0..30 {
647 let (s, _) = n.step(1, 256, 50, 0);
648 spikes.push(s);
649 }
650 let total: i32 = spikes.iter().sum();
651 assert!(total > 0, "neuron must fire with refractory_period=2");
652 for (i, &s) in spikes.iter().enumerate() {
654 if s == 1 && i + 2 < spikes.len() {
655 assert_eq!(spikes[i + 1], 0, "step {} should be refractory", i + 1);
656 assert_eq!(spikes[i + 2], 0, "step {} should be refractory", i + 2);
657 }
658 }
659 }
660
661 #[test]
662 fn lif_fires_without_refractory() {
663 let mut n = FixedPointLif::new(16, 8, 0, 0, 256, 0);
664 let mut total = 0;
665 for _ in 0..20 {
666 let (s, _) = n.step(1, 256, 50, 0);
667 total += s;
668 }
669 assert!(total > 0, "neuron must fire with refractory_period=0");
670 }
671
672 #[test]
675 fn izhikevich_regular_spiking_fires() {
676 let mut n = Izhikevich::regular_spiking();
677 let mut total = 0;
678 for _ in 0..100 {
679 total += n.step(10.0);
680 }
681 assert!(total > 0, "RS neuron must fire with I=10");
682 }
683
684 #[test]
685 fn izhikevich_no_spike_without_input() {
686 let mut n = Izhikevich::regular_spiking();
687 let mut total = 0;
688 for _ in 0..100 {
689 total += n.step(0.0);
690 }
691 assert_eq!(total, 0, "no spikes without input");
692 }
693
694 #[test]
695 fn izhikevich_reset_clears_state() {
696 let mut n = Izhikevich::regular_spiking();
697 for _ in 0..50 {
698 n.step(10.0);
699 }
700 n.reset();
701 assert_eq!(n.v, n.c);
702 assert!((n.u - n.b * n.c).abs() < 1e-12);
703 }
704
705 #[test]
706 fn izhikevich_chattering_fires_more() {
707 let mut ch = Izhikevich::new(0.02, 0.2, -50.0, 2.0, 1.0);
709 let mut rs = Izhikevich::regular_spiking();
710 let mut ch_spikes = 0;
711 let mut rs_spikes = 0;
712 for _ in 0..200 {
713 ch_spikes += ch.step(10.0);
714 rs_spikes += rs.step(10.0);
715 }
716 assert!(
717 ch_spikes > rs_spikes,
718 "chattering ({ch_spikes}) should fire more than RS ({rs_spikes})"
719 );
720 }
721
722 #[test]
725 fn averager_all_ones() {
726 let mut avg = BitstreamAverager::new(100);
727 for _ in 0..100 {
728 avg.push(1);
729 }
730 assert!((avg.estimate() - 1.0).abs() < 1e-12);
731 }
732
733 #[test]
734 fn averager_all_zeros() {
735 let mut avg = BitstreamAverager::new(50);
736 for _ in 0..50 {
737 avg.push(0);
738 }
739 assert!(avg.estimate().abs() < 1e-12);
740 }
741
742 #[test]
743 fn averager_half() {
744 let mut avg = BitstreamAverager::new(100);
745 for i in 0..100 {
746 avg.push((i % 2) as u8);
747 }
748 assert!((avg.estimate() - 0.5).abs() < 1e-12);
749 }
750
751 #[test]
752 fn averager_sliding_window() {
753 let mut avg = BitstreamAverager::new(4);
754 for &b in &[1_u8, 1, 0, 0] {
756 avg.push(b);
757 }
758 assert!((avg.estimate() - 0.5).abs() < 1e-12);
759 avg.push(1);
762 assert!((avg.estimate() - 0.5).abs() < 1e-12);
765 avg.push(1);
767 assert!((avg.estimate() - 0.5).abs() < 1e-12);
768 avg.push(1);
770 assert!((avg.estimate() - 0.75).abs() < 1e-12);
771 }
772
773 #[test]
774 fn averager_partial_fill() {
775 let mut avg = BitstreamAverager::new(100);
776 avg.push(1);
777 avg.push(0);
778 assert!((avg.estimate() - 0.5).abs() < 1e-12);
779 }
780
781 #[test]
782 fn averager_empty_returns_zero() {
783 let avg = BitstreamAverager::new(10);
784 assert!(avg.estimate().abs() < 1e-12);
785 }
786
787 #[test]
790 fn homeostatic_fires_with_strong_input() {
791 let mut n = HomeostaticLif::with_defaults();
792 let mut total = 0;
793 for _ in 0..200 {
794 total += n.step(25.0);
795 }
796 assert!(total > 0, "must fire with strong input");
797 }
798
799 #[test]
800 fn homeostatic_threshold_adapts() {
801 let mut n = HomeostaticLif::with_defaults();
802 let initial = n.v_threshold;
803 for _ in 0..500 {
804 n.step(25.0);
805 }
806 assert!(
807 (n.v_threshold - initial).abs() > 1e-6,
808 "threshold must adapt"
809 );
810 }
811
812 #[test]
813 fn homeostatic_no_fire_without_input() {
814 let mut n = HomeostaticLif::with_defaults();
815 let mut total = 0;
816 for _ in 0..100 {
817 total += n.step(0.0);
818 }
819 assert_eq!(total, 0);
820 }
821
822 #[test]
823 fn homeostatic_threshold_bounded() {
824 let mut n = HomeostaticLif::with_defaults();
825 for _ in 0..10000 {
826 n.step(50.0);
827 }
828 assert!(n.v_threshold >= 0.1);
829 assert!(n.v_threshold <= 10.0);
830 }
831
832 #[test]
835 fn dendritic_xor_truth_table() {
836 let mut n = DendriticNeuron::new(0.5);
837 assert_eq!(n.step(0.0, 0.0), 0); assert_eq!(n.step(1.0, 0.0), 1); assert_eq!(n.step(0.0, 1.0), 1); assert_eq!(n.step(1.0, 1.0), 0); }
842
843 #[test]
844 fn dendritic_subthreshold() {
845 let mut n = DendriticNeuron::new(0.5);
846 assert_eq!(n.step(0.2, 0.1), 0);
847 }
848
849 #[test]
850 fn dendritic_reset() {
851 let mut n = DendriticNeuron::with_defaults();
852 n.step(1.0, 0.0);
853 n.reset();
854 assert!((n.last_current).abs() < 1e-12);
855 }
856
857 #[test]
858 fn averager_reset() {
859 let mut avg = BitstreamAverager::new(10);
860 for _ in 0..10 {
861 avg.push(1);
862 }
863 avg.reset();
864 assert!(avg.estimate().abs() < 1e-12);
865 }
866
867 #[test]
870 fn adex_fires_with_input() {
871 let mut n = AdExNeuron::new();
872 let mut total = 0;
873 for _ in 0..2000 {
874 total += n.step(500.0);
875 }
876 assert!(total > 0, "AdEx must fire with strong input");
877 }
878
879 #[test]
880 fn adex_adaptation_reduces_rate() {
881 let mut n = AdExNeuron::new();
882 let first_100: i32 = (0..1000).map(|_| n.step(400.0)).sum();
883 let next_100: i32 = (0..1000).map(|_| n.step(400.0)).sum();
884 assert!(
886 next_100 <= first_100 + 5,
887 "adaptation should not increase rate: first={first_100}, next={next_100}"
888 );
889 }
890
891 #[test]
894 fn expif_fires() {
895 let mut n = ExpIfNeuron::new();
896 let mut total = 0;
897 for _ in 0..2000 {
898 total += n.step(500.0);
899 }
900 assert!(total > 0, "ExpIF must fire");
901 }
902
903 #[test]
904 fn expif_no_fire_without_input() {
905 let mut n = ExpIfNeuron::new();
906 let total: i32 = (0..500).map(|_| n.step(0.0)).sum();
907 assert_eq!(total, 0);
908 }
909
910 #[test]
911 fn expif_matches_rk4_reference() {
912 let mut n = ExpIfNeuron::new();
913 n.v = -60.0;
914 n.dt = 0.25;
915 n.tau = 20.0;
916 let rhs = |v: f64, s: &ExpIfNeuron, current: f64| {
917 let exp_arg = ((v - s.v_rh) / s.delta_t).clamp(-20.0, 20.0);
918 (-(v - s.v_rest) + s.delta_t * exp_arg.exp() + current) / s.tau
919 };
920 let current = 12.0;
921 let k1 = rhs(n.v, &n, current);
922 let k2 = rhs(n.v + 0.5 * n.dt * k1, &n, current);
923 let k3 = rhs(n.v + 0.5 * n.dt * k2, &n, current);
924 let k4 = rhs(n.v + n.dt * k3, &n, current);
925 let expected = n.v + n.dt * (k1 + 2.0 * k2 + 2.0 * k3 + k4) / 6.0;
926
927 assert_eq!(n.step(current), 0);
928 assert!((n.v - expected).abs() < 1e-12);
929 }
930
931 #[test]
934 fn lapicque_fires() {
935 let mut n = LapicqueNeuron::new(20.0, 1.0, 1.0, 1.0);
936 let mut total = 0;
937 for _ in 0..200 {
938 total += n.step(5.0);
939 }
940 assert!(total > 0, "Lapicque must fire with sustained input");
941 }
942
943 #[test]
944 fn lapicque_reset() {
945 let mut n = LapicqueNeuron::new(20.0, 1.0, 1.0, 1.0);
946 for _ in 0..50 {
947 n.step(5.0);
948 }
949 n.reset();
950 assert!((n.v).abs() < 1e-12);
951 }
952
953 #[test]
954 fn lapicque_exact_flow_matches_closed_form() {
955 let mut n = LapicqueNeuron::new(20.0, 1.0, 1.0, 5.0);
956 n.v = 0.25;
957 let current = 0.5;
958 let v0 = n.v;
959 let v_inf = n.v_rest + n.resistance * current;
960 let euler = v0 + (-(v0 - n.v_rest) + n.resistance * current) / n.tau * n.dt;
961 let expected = v_inf + (v0 - v_inf) * (-n.dt / n.tau).exp();
962
963 assert_eq!(n.step(current), 0);
964 assert!((n.v - expected).abs() < 1e-15);
965 assert!((n.v - euler).abs() > 1e-4);
966 }
967
968 #[test]
971 fn adex_no_fire_without_input() {
972 let mut n = AdExNeuron::new();
973 let total: i32 = (0..1000).map(|_| n.step(0.0)).sum();
974 assert_eq!(total, 0);
975 }
976
977 #[test]
978 fn adex_negative_current_no_fire() {
979 let mut n = AdExNeuron::new();
980 let total: i32 = (0..500).map(|_| n.step(-100.0)).sum();
981 assert_eq!(total, 0, "negative current must not cause spikes");
982 }
983
984 #[test]
985 fn adex_reset_roundtrip() {
986 let mut n = AdExNeuron::new();
987 for _ in 0..200 {
988 n.step(500.0);
989 }
990 assert!(n.w > 0.0, "w must grow during spiking");
991 n.reset();
992 assert_eq!(n.v, n.v_rest);
993 assert_eq!(n.w, 0.0);
994 let mut fresh = AdExNeuron::new();
996 let r1: i32 = (0..100).map(|_| n.step(500.0)).sum();
997 let r2: i32 = (0..100).map(|_| fresh.step(500.0)).sum();
998 assert_eq!(r1, r2, "reset neuron must match fresh neuron");
999 }
1000
1001 #[test]
1002 fn adex_voltage_bounded() {
1003 let mut n = AdExNeuron::new();
1004 for _ in 0..5000 {
1005 n.step(1000.0);
1006 }
1007 assert!(n.v.is_finite(), "voltage must stay finite");
1008 assert!(n.w.is_finite(), "adaptation must stay finite");
1009 }
1010
1011 #[test]
1012 fn adex_pipeline_sustained_spiking() {
1013 let mut n = AdExNeuron::new();
1014 let spikes: i32 = (0..10000).map(|_| n.step(500.0)).sum();
1015 assert!(
1016 spikes > 100,
1017 "sustained input should produce many spikes: got {spikes}"
1018 );
1019 assert!(n.v.is_finite());
1020 }
1021
1022 #[test]
1023 fn adex_performance_10k_steps() {
1024 let mut n = AdExNeuron::new();
1025 let start = std::time::Instant::now();
1026 for _ in 0..10_000 {
1027 n.step(500.0);
1028 }
1029 let elapsed = start.elapsed();
1030 assert!(
1031 elapsed.as_millis() < 50,
1032 "10k steps took too long: {:?}",
1033 elapsed
1034 );
1035 }
1036
1037 #[test]
1040 fn expif_negative_current_no_fire() {
1041 let mut n = ExpIfNeuron::new();
1042 let total: i32 = (0..500).map(|_| n.step(-100.0)).sum();
1043 assert_eq!(total, 0);
1044 }
1045
1046 #[test]
1047 fn expif_reset_roundtrip() {
1048 let mut n = ExpIfNeuron::new();
1049 for _ in 0..200 {
1050 n.step(500.0);
1051 }
1052 n.reset();
1053 assert_eq!(n.v, n.v_rest);
1054 let mut fresh = ExpIfNeuron::new();
1055 let r1: i32 = (0..100).map(|_| n.step(500.0)).sum();
1056 let r2: i32 = (0..100).map(|_| fresh.step(500.0)).sum();
1057 assert_eq!(r1, r2);
1058 }
1059
1060 #[test]
1061 fn expif_voltage_bounded() {
1062 let mut n = ExpIfNeuron::new();
1063 for _ in 0..5000 {
1064 n.step(1000.0);
1065 }
1066 assert!(n.v.is_finite());
1067 }
1068
1069 #[test]
1070 fn expif_fires_more_than_adex() {
1071 let mut eif = ExpIfNeuron::new();
1073 let mut adex = AdExNeuron::new();
1074 let eif_spikes: i32 = (0..5000).map(|_| eif.step(500.0)).sum();
1075 let adex_spikes: i32 = (0..5000).map(|_| adex.step(500.0)).sum();
1076 assert!(
1077 eif_spikes >= adex_spikes,
1078 "ExpIF ({eif_spikes}) should fire >= AdEx ({adex_spikes}) due to no adaptation"
1079 );
1080 }
1081
1082 #[test]
1083 fn expif_performance_10k_steps() {
1084 let mut n = ExpIfNeuron::new();
1085 let start = std::time::Instant::now();
1086 for _ in 0..10_000 {
1087 n.step(500.0);
1088 }
1089 let elapsed = start.elapsed();
1090 assert!(
1091 elapsed.as_millis() < 50,
1092 "10k steps took too long: {:?}",
1093 elapsed
1094 );
1095 }
1096
1097 #[test]
1100 fn lapicque_no_fire_without_input() {
1101 let mut n = LapicqueNeuron::new(20.0, 1.0, 1.0, 1.0);
1102 let total: i32 = (0..500).map(|_| n.step(0.0)).sum();
1103 assert_eq!(total, 0);
1104 }
1105
1106 #[test]
1107 fn lapicque_negative_current_no_fire() {
1108 let mut n = LapicqueNeuron::new(20.0, 1.0, 1.0, 1.0);
1109 let total: i32 = (0..500).map(|_| n.step(-5.0)).sum();
1110 assert_eq!(total, 0);
1111 }
1112
1113 #[test]
1114 fn lapicque_invalid_state_does_not_mutate() {
1115 let mut n = LapicqueNeuron::new(20.0, 1.0, 1.0, 1.0);
1116 n.v = 0.25;
1117 n.tau = 0.0;
1118 assert_eq!(n.step(1.0), 0);
1119 assert_eq!(n.v, 0.25);
1120 }
1121
1122 #[test]
1123 fn lapicque_reset_roundtrip() {
1124 let mut n = LapicqueNeuron::new(20.0, 1.0, 1.0, 1.0);
1125 for _ in 0..100 {
1126 n.step(5.0);
1127 }
1128 n.reset();
1129 assert_eq!(n.v, n.v_rest);
1130 let mut fresh = LapicqueNeuron::new(20.0, 1.0, 1.0, 1.0);
1131 let r1: i32 = (0..100).map(|_| n.step(5.0)).sum();
1132 let r2: i32 = (0..100).map(|_| fresh.step(5.0)).sum();
1133 assert_eq!(r1, r2);
1134 }
1135
1136 #[test]
1137 fn lapicque_voltage_bounded() {
1138 let mut n = LapicqueNeuron::new(20.0, 1.0, 1.0, 1.0);
1139 for _ in 0..5000 {
1140 n.step(100.0);
1141 }
1142 assert!(n.v.is_finite());
1143 }
1144
1145 #[test]
1146 fn lapicque_higher_resistance_fires_faster() {
1147 let mut lo = LapicqueNeuron::new(20.0, 0.5, 1.0, 1.0);
1148 let mut hi = LapicqueNeuron::new(20.0, 2.0, 1.0, 1.0);
1149 let lo_spikes: i32 = (0..200).map(|_| lo.step(1.0)).sum();
1150 let hi_spikes: i32 = (0..200).map(|_| hi.step(1.0)).sum();
1151 assert!(
1152 hi_spikes >= lo_spikes,
1153 "higher R ({hi_spikes}) should fire >= lower R ({lo_spikes})"
1154 );
1155 }
1156
1157 #[test]
1158 fn lapicque_performance_10k_steps() {
1159 let mut n = LapicqueNeuron::new(20.0, 1.0, 1.0, 1.0);
1160 let start = std::time::Instant::now();
1161 for _ in 0..10_000 {
1162 n.step(5.0);
1163 }
1164 let elapsed = start.elapsed();
1165 assert!(
1166 elapsed.as_millis() < 50,
1167 "10k steps took too long: {:?}",
1168 elapsed
1169 );
1170 }
1171
1172 #[test]
1173 fn lapicque_pipeline_sustained_spiking() {
1174 let mut n = LapicqueNeuron::new(20.0, 1.0, 1.0, 1.0);
1175 let spikes: i32 = (0..10000).map(|_| n.step(5.0)).sum();
1176 assert!(
1177 spikes > 100,
1178 "sustained input should produce many spikes: got {spikes}"
1179 );
1180 }
1181}