Skip to main content

sc_neurocore_engine/neurons/
trivial.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Commercial license available
3// © Concepts 1996–2026 Miroslav Šotek. All rights reserved.
4// © Code 2020–2026 Miroslav Šotek. All rights reserved.
5// ORCID: 0009-0009-3560-0851
6// Contact: www.anulum.li | protoscience@anulum.li
7// SC-NeuroCore — Simple integrate-and-fire variants
8
9/// Simple integrate-and-fire variants.
10use rand::{RngExt, SeedableRng};
11use rand_xoshiro::Xoshiro256PlusPlus;
12
13/// Quadratic Integrate-and-Fire — canonical Type-I excitability.
14/// dv/dt = v² + I, reset at v_peak.
15#[derive(Clone, Debug)]
16pub struct QuadraticIFNeuron {
17    pub v: f64,
18    pub v_reset: f64,
19    pub v_peak: f64,
20    pub dt: f64,
21}
22
23impl QuadraticIFNeuron {
24    pub fn new(v_reset: f64, v_peak: f64, dt: f64) -> Self {
25        Self {
26            v: v_reset,
27            v_reset,
28            v_peak,
29            dt,
30        }
31    }
32
33    fn valid_numeric_contract(&self) -> bool {
34        self.v.is_finite()
35            && self.v_reset.is_finite()
36            && self.v_peak.is_finite()
37            && self.dt.is_finite()
38            && self.v < self.v_peak
39            && self.v_reset < self.v_peak
40            && self.dt > 0.0
41    }
42
43    pub fn step(&mut self, current: f64) -> i32 {
44        if !self.valid_numeric_contract() || !current.is_finite() {
45            return 0;
46        }
47        let increment = (self.v * self.v + current) * self.dt;
48        let next_v = self.v + increment;
49        if !(increment.is_finite() && next_v.is_finite()) {
50            return 0;
51        }
52        self.v = next_v;
53        if self.v >= self.v_peak {
54            self.v = self.v_reset;
55            1
56        } else {
57            0
58        }
59    }
60
61    pub fn reset(&mut self) {
62        self.v = self.v_reset;
63    }
64}
65
66impl Default for QuadraticIFNeuron {
67    fn default() -> Self {
68        Self::new(-1.0, 1.0, 0.01)
69    }
70}
71
72/// Theta neuron — Ermentrout & Kopell canonical form.
73/// dθ/dt = (1 - cosθ) + (1 + cosθ)·I, spike at θ crossing π.
74#[derive(Clone, Debug)]
75pub struct ThetaNeuron {
76    pub theta: f64,
77    pub dt: f64,
78}
79
80impl ThetaNeuron {
81    pub fn new(dt: f64) -> Self {
82        Self { theta: 0.0, dt }
83    }
84
85    pub fn step(&mut self, current: f64) -> i32 {
86        let prev = self.theta;
87        self.theta += ((1.0 - self.theta.cos()) + (1.0 + self.theta.cos()) * current) * self.dt;
88        if self.theta >= std::f64::consts::PI && prev < std::f64::consts::PI {
89            self.theta -= 2.0 * std::f64::consts::PI;
90            1
91        } else {
92            0
93        }
94    }
95
96    pub fn reset(&mut self) {
97        self.theta = 0.0;
98    }
99}
100
101impl Default for ThetaNeuron {
102    fn default() -> Self {
103        Self::new(0.01)
104    }
105}
106
107/// Perfect integrator — no leak, pure capacitive charging.
108#[derive(Clone, Debug)]
109pub struct PerfectIntegratorNeuron {
110    pub v: f64,
111    pub c_m: f64,
112    pub v_threshold: f64,
113    pub v_reset: f64,
114    pub dt: f64,
115}
116
117impl PerfectIntegratorNeuron {
118    pub fn new(c_m: f64, v_threshold: f64, dt: f64) -> Self {
119        Self {
120            v: 0.0,
121            c_m,
122            v_threshold,
123            v_reset: 0.0,
124            dt,
125        }
126    }
127
128    pub fn step(&mut self, current: f64) -> i32 {
129        self.v += (current / self.c_m) * self.dt;
130        if self.v >= self.v_threshold {
131            self.v = self.v_reset;
132            1
133        } else {
134            0
135        }
136    }
137
138    pub fn reset(&mut self) {
139        self.v = self.v_reset;
140    }
141}
142
143impl Default for PerfectIntegratorNeuron {
144    fn default() -> Self {
145        Self::new(1.0, 1.0, 0.1)
146    }
147}
148
149/// Gated LIF — learnable decay and input gates.
150#[derive(Clone, Debug)]
151pub struct GatedLIFNeuron {
152    pub v: f64,
153    pub gate_v: f64,
154    pub gate_i: f64,
155    pub v_threshold: f64,
156    pub dt: f64,
157}
158
159impl GatedLIFNeuron {
160    pub fn new(gate_v: f64, gate_i: f64, v_threshold: f64) -> Self {
161        Self {
162            v: 0.0,
163            gate_v,
164            gate_i,
165            v_threshold,
166            dt: 1.0,
167        }
168    }
169
170    pub fn step(&mut self, current: f64) -> i32 {
171        self.v = self.gate_v * self.v + self.gate_i * current;
172        if self.v >= self.v_threshold {
173            self.v -= self.v_threshold;
174            1
175        } else {
176            0
177        }
178    }
179
180    pub fn reset(&mut self) {
181        self.v = 0.0;
182    }
183}
184
185impl Default for GatedLIFNeuron {
186    fn default() -> Self {
187        Self::new(0.9, 1.0, 1.0)
188    }
189}
190
191/// Nonlinear LIF — cubic f-I curve with adaptation. Touboul & Brette 2008.
192#[derive(Clone, Debug)]
193pub struct NonlinearLIFNeuron {
194    pub v: f64,
195    pub w: f64,
196    pub v_rest: f64,
197    pub v_crit: f64,
198    pub v_threshold: f64,
199    pub v_reset: f64,
200    pub a: f64,
201    pub b: f64,
202    pub tau_w: f64,
203    pub c_m: f64,
204    pub dt: f64,
205}
206
207impl NonlinearLIFNeuron {
208    pub fn new() -> Self {
209        Self {
210            v: -65.0,
211            w: 0.0,
212            v_rest: -65.0,
213            v_crit: -40.0,
214            v_threshold: -20.0,
215            v_reset: -65.0,
216            a: 0.04,
217            b: 0.5,
218            tau_w: 100.0,
219            c_m: 1.0,
220            dt: 0.1,
221        }
222    }
223
224    pub fn step(&mut self, current: f64) -> i32 {
225        let v_prev = self.v;
226        let cubic = self.a * (self.v - self.v_rest) * (self.v - self.v_crit);
227        self.v += (cubic - self.w + current) / self.c_m * self.dt;
228        self.w += (self.b * (self.v - self.v_rest) - self.w) / self.tau_w * self.dt;
229        if self.v >= self.v_threshold && v_prev < self.v_threshold {
230            self.v = self.v_reset;
231            1
232        } else {
233            0
234        }
235    }
236
237    pub fn reset(&mut self) {
238        self.v = self.v_rest;
239        self.w = 0.0;
240    }
241}
242
243impl Default for NonlinearLIFNeuron {
244    fn default() -> Self {
245        Self::new()
246    }
247}
248
249/// Spike-Frequency Adaptation LIF. Benda & Herz 2003.
250#[derive(Clone, Debug)]
251pub struct SFANeuron {
252    pub v: f64,
253    pub g_sfa: f64,
254    pub v_rest: f64,
255    pub v_reset: f64,
256    pub v_threshold: f64,
257    pub tau_m: f64,
258    pub tau_sfa: f64,
259    pub delta_g: f64,
260    pub e_k: f64,
261    pub resistance: f64,
262    pub dt: f64,
263}
264
265impl SFANeuron {
266    pub fn new() -> Self {
267        Self {
268            v: -70.0,
269            g_sfa: 0.0,
270            v_rest: -70.0,
271            v_reset: -70.0,
272            v_threshold: -50.0,
273            tau_m: 10.0,
274            tau_sfa: 200.0,
275            delta_g: 0.5,
276            e_k: -80.0,
277            resistance: 1.0,
278            dt: 1.0,
279        }
280    }
281
282    pub fn step(&mut self, current: f64) -> i32 {
283        self.v += (-(self.v - self.v_rest) - self.g_sfa * (self.v - self.e_k)
284            + self.resistance * current)
285            / self.tau_m
286            * self.dt;
287        self.g_sfa *= (-self.dt / self.tau_sfa).exp();
288        if self.v >= self.v_threshold {
289            self.v = self.v_reset;
290            self.g_sfa += self.delta_g;
291            1
292        } else {
293            0
294        }
295    }
296
297    pub fn reset(&mut self) {
298        self.v = self.v_rest;
299        self.g_sfa = 0.0;
300    }
301}
302
303impl Default for SFANeuron {
304    fn default() -> Self {
305        Self::new()
306    }
307}
308
309/// Multi-timescale Adaptive Threshold. Kobayashi et al. 2009.
310#[derive(Clone, Debug)]
311pub struct MATNeuron {
312    pub v: f64,
313    pub theta1: f64,
314    pub theta2: f64,
315    pub v_rest: f64,
316    pub v_reset: f64,
317    pub v_threshold_base: f64,
318    pub tau_m: f64,
319    pub tau_1: f64,
320    pub tau_2: f64,
321    pub h1: f64,
322    pub h2: f64,
323    pub resistance: f64,
324    pub dt: f64,
325}
326
327impl MATNeuron {
328    pub fn new() -> Self {
329        Self {
330            v: -70.0,
331            theta1: 0.0,
332            theta2: 0.0,
333            v_rest: -70.0,
334            v_reset: -70.0,
335            v_threshold_base: -50.0,
336            tau_m: 10.0,
337            tau_1: 10.0,
338            tau_2: 200.0,
339            h1: 5.0,
340            h2: 3.0,
341            resistance: 1.0,
342            dt: 1.0,
343        }
344    }
345
346    pub fn step(&mut self, current: f64) -> i32 {
347        self.v += (-(self.v - self.v_rest) + self.resistance * current) / self.tau_m * self.dt;
348        self.theta1 *= (-self.dt / self.tau_1).exp();
349        self.theta2 *= (-self.dt / self.tau_2).exp();
350        let threshold = self.v_threshold_base + self.theta1 + self.theta2;
351        if self.v >= threshold {
352            self.v = self.v_reset;
353            self.theta1 += self.h1;
354            self.theta2 += self.h2;
355            1
356        } else {
357            0
358        }
359    }
360
361    pub fn reset(&mut self) {
362        self.v = self.v_rest;
363        self.theta1 = 0.0;
364        self.theta2 = 0.0;
365    }
366}
367
368impl Default for MATNeuron {
369    fn default() -> Self {
370        Self::new()
371    }
372}
373
374/// Escape-rate neuron — stochastic IF with exponential hazard. Gerstner 2000.
375#[derive(Clone, Debug)]
376pub struct EscapeRateNeuron {
377    pub v: f64,
378    pub v_rest: f64,
379    pub v_reset: f64,
380    pub v_threshold: f64,
381    pub tau_m: f64,
382    pub rho_0: f64,
383    pub delta_u: f64,
384    pub resistance: f64,
385    pub dt: f64,
386    rng: Xoshiro256PlusPlus,
387}
388
389impl EscapeRateNeuron {
390    pub fn new(seed: u64) -> Self {
391        Self {
392            v: -70.0,
393            v_rest: -70.0,
394            v_reset: -70.0,
395            v_threshold: -50.0,
396            tau_m: 10.0,
397            rho_0: 0.001,
398            delta_u: 3.0,
399            resistance: 1.0,
400            dt: 1.0,
401            rng: Xoshiro256PlusPlus::seed_from_u64(seed),
402        }
403    }
404
405    pub fn step(&mut self, current: f64) -> i32 {
406        self.v += (-(self.v - self.v_rest) + self.resistance * current) / self.tau_m * self.dt;
407        let rate = self.rho_0 * ((self.v - self.v_threshold) / self.delta_u).exp();
408        if self.rng.random::<f64>() < rate * self.dt {
409            self.v = self.v_reset;
410            1
411        } else {
412            0
413        }
414    }
415
416    pub fn reset(&mut self) {
417        self.v = self.v_rest;
418    }
419}
420
421/// KLIF — learnable LIF with scaling factor k. Eshraghian et al. 2021.
422#[derive(Clone, Debug)]
423pub struct KLIFNeuron {
424    pub v: f64,
425    pub k: f64,
426    pub alpha: f64,
427    pub v_threshold: f64,
428    pub v_reset: f64,
429}
430
431impl KLIFNeuron {
432    pub fn new(tau: f64, k: f64, dt: f64) -> Self {
433        Self {
434            v: 0.0,
435            k,
436            alpha: (-dt / tau).exp(),
437            v_threshold: 1.0,
438            v_reset: 0.0,
439        }
440    }
441
442    pub fn step(&mut self, current: f64) -> i32 {
443        self.v = self.alpha * self.v + self.k * current;
444        if self.v >= self.v_threshold {
445            self.v = self.v_reset;
446            1
447        } else {
448            0
449        }
450    }
451
452    pub fn reset(&mut self) {
453        self.v = 0.0;
454    }
455}
456
457impl Default for KLIFNeuron {
458    fn default() -> Self {
459        Self::new(10.0, 1.0, 1.0)
460    }
461}
462
463/// Inhibitory LIF — LIF with self-inhibition trace.
464#[derive(Clone, Debug)]
465pub struct InhibitoryLIFNeuron {
466    pub v: f64,
467    pub inh_trace: f64,
468    pub alpha_m: f64,
469    pub alpha_inh: f64,
470    pub v_threshold: f64,
471    pub inh_strength: f64,
472}
473
474impl InhibitoryLIFNeuron {
475    pub fn new(tau_m: f64, tau_inh: f64, dt: f64) -> Self {
476        Self {
477            v: 0.0,
478            inh_trace: 0.0,
479            alpha_m: (-dt / tau_m).exp(),
480            alpha_inh: (-dt / tau_inh).exp(),
481            v_threshold: 1.0,
482            inh_strength: 0.5,
483        }
484    }
485
486    pub fn step(&mut self, current: f64) -> i32 {
487        self.inh_trace *= self.alpha_inh;
488        self.v = self.alpha_m * self.v + current - self.inh_strength * self.inh_trace;
489        if self.v >= self.v_threshold {
490            self.v = 0.0;
491            self.inh_trace += 1.0;
492            1
493        } else {
494            0
495        }
496    }
497
498    pub fn reset(&mut self) {
499        self.v = 0.0;
500        self.inh_trace = 0.0;
501    }
502}
503
504impl Default for InhibitoryLIFNeuron {
505    fn default() -> Self {
506        Self::new(10.0, 5.0, 1.0)
507    }
508}
509
510/// Complementary LIF — dual-path excitatory/inhibitory.
511#[derive(Clone, Debug)]
512pub struct ComplementaryLIFNeuron {
513    pub v_pos: f64,
514    pub v_neg: f64,
515    pub alpha: f64,
516    pub v_threshold: f64,
517}
518
519impl ComplementaryLIFNeuron {
520    pub fn new(tau: f64, dt: f64, v_threshold: f64) -> Self {
521        Self {
522            v_pos: 0.0,
523            v_neg: 0.0,
524            alpha: (-dt / tau).exp(),
525            v_threshold,
526        }
527    }
528
529    pub fn step(&mut self, current: f64) -> i32 {
530        let inp_pos = current.max(0.0);
531        let inp_neg = (-current).max(0.0);
532        self.v_pos = self.alpha * self.v_pos + inp_pos;
533        self.v_neg = self.alpha * self.v_neg + inp_neg;
534        let diff = self.v_pos - self.v_neg;
535        if diff >= self.v_threshold {
536            self.v_pos = 0.0;
537            self.v_neg = 0.0;
538            1
539        } else if diff <= -self.v_threshold {
540            self.v_pos = 0.0;
541            self.v_neg = 0.0;
542            -1
543        } else {
544            0
545        }
546    }
547
548    pub fn reset(&mut self) {
549        self.v_pos = 0.0;
550        self.v_neg = 0.0;
551    }
552}
553
554impl Default for ComplementaryLIFNeuron {
555    fn default() -> Self {
556        Self::new(10.0, 1.0, 1.0)
557    }
558}
559
560/// Parametric LIF — learnable decay via sigmoid(a). Fang et al. 2021.
561#[derive(Clone, Debug)]
562pub struct ParametricLIFNeuron {
563    pub v: f64,
564    pub a: f64,
565    pub threshold: f64,
566}
567
568impl ParametricLIFNeuron {
569    pub fn new(a: f64, threshold: f64) -> Self {
570        Self {
571            v: 0.0,
572            a,
573            threshold,
574        }
575    }
576
577    pub fn step(&mut self, current: f64) -> i32 {
578        let alpha = 1.0 / (1.0 + (-self.a).exp());
579        let spike = if self.v >= self.threshold { 1 } else { 0 };
580        self.v = alpha * self.v * (1.0 - spike as f64) + current;
581        spike
582    }
583
584    pub fn reset(&mut self) {
585        self.v = 0.0;
586    }
587}
588
589impl Default for ParametricLIFNeuron {
590    fn default() -> Self {
591        Self::new(0.0, 1.0)
592    }
593}
594
595/// Non-resetting LIF with adaptive threshold. Brette 2004.
596#[derive(Clone, Debug)]
597pub struct NonResettingLIFNeuron {
598    pub v: f64,
599    pub theta: f64,
600    pub v_rest: f64,
601    pub theta_rest: f64,
602    pub delta_theta: f64,
603    pub tau_m: f64,
604    pub tau_theta: f64,
605    pub r_m: f64,
606    pub dt: f64,
607}
608
609impl NonResettingLIFNeuron {
610    pub fn new() -> Self {
611        Self {
612            v: -65.0,
613            theta: -50.0,
614            v_rest: -65.0,
615            theta_rest: -50.0,
616            delta_theta: 5.0,
617            tau_m: 10.0,
618            tau_theta: 50.0,
619            r_m: 1.0,
620            dt: 0.1,
621        }
622    }
623
624    pub fn step(&mut self, current: f64) -> i32 {
625        self.v += (-(self.v - self.v_rest) + self.r_m * current) / self.tau_m * self.dt;
626        self.theta += (-(self.theta - self.theta_rest)) / self.tau_theta * self.dt;
627        if self.v >= self.theta {
628            self.theta += self.delta_theta;
629            1
630        } else {
631            0
632        }
633    }
634
635    pub fn reset(&mut self) {
636        self.v = self.v_rest;
637        self.theta = self.theta_rest;
638    }
639}
640
641impl Default for NonResettingLIFNeuron {
642    fn default() -> Self {
643        Self::new()
644    }
645}
646
647/// Adaptive-threshold IF. Platkiewicz & Brette 2010.
648#[derive(Clone, Debug)]
649pub struct AdaptiveThresholdIFNeuron {
650    pub v: f64,
651    pub theta: f64,
652    pub v_rest: f64,
653    pub v_reset: f64,
654    pub theta_rest: f64,
655    pub delta_theta: f64,
656    pub tau_m: f64,
657    pub tau_theta: f64,
658    pub dt: f64,
659}
660
661impl AdaptiveThresholdIFNeuron {
662    pub fn new() -> Self {
663        Self {
664            v: -65.0,
665            theta: -50.0,
666            v_rest: -65.0,
667            v_reset: -65.0,
668            theta_rest: -50.0,
669            delta_theta: 5.0,
670            tau_m: 10.0,
671            tau_theta: 50.0,
672            dt: 0.1,
673        }
674    }
675
676    pub fn step(&mut self, current: f64) -> i32 {
677        self.v += (-(self.v - self.v_rest) + current) / self.tau_m * self.dt;
678        self.theta += (-(self.theta - self.theta_rest)) / self.tau_theta * self.dt;
679        if self.v >= self.theta {
680            self.v = self.v_reset;
681            self.theta += self.delta_theta;
682            1
683        } else {
684            0
685        }
686    }
687
688    pub fn reset(&mut self) {
689        self.v = self.v_rest;
690        self.theta = self.theta_rest;
691    }
692}
693
694impl Default for AdaptiveThresholdIFNeuron {
695    fn default() -> Self {
696        Self::new()
697    }
698}
699
700/// Sigma-Delta neuron — first-order delta modulation.
701#[derive(Clone, Debug)]
702pub struct SigmaDeltaNeuron {
703    pub sigma: f64,
704    pub v_threshold: f64,
705}
706
707impl SigmaDeltaNeuron {
708    pub fn new(v_threshold: f64) -> Self {
709        Self {
710            sigma: 0.0,
711            v_threshold,
712        }
713    }
714
715    pub fn step(&mut self, current: f64) -> i32 {
716        self.sigma += current;
717        if self.sigma >= self.v_threshold {
718            self.sigma -= self.v_threshold;
719            1
720        } else if self.sigma <= -self.v_threshold {
721            self.sigma += self.v_threshold;
722            -1
723        } else {
724            0
725        }
726    }
727
728    pub fn reset(&mut self) {
729        self.sigma = 0.0;
730    }
731}
732
733impl Default for SigmaDeltaNeuron {
734    fn default() -> Self {
735        Self::new(1.0)
736    }
737}
738
739/// Energy-aware LIF — metabolic cost modulates gain. Sengupta et al. 2013.
740#[derive(Clone, Debug)]
741pub struct EnergyLIFNeuron {
742    pub v: f64,
743    pub epsilon: f64,
744    pub v_rest: f64,
745    pub v_reset: f64,
746    pub v_threshold: f64,
747    pub tau_m: f64,
748    pub tau_e: f64,
749    pub alpha: f64,
750    pub epsilon_0: f64,
751    pub resistance: f64,
752    pub dt: f64,
753}
754
755impl EnergyLIFNeuron {
756    pub fn new() -> Self {
757        Self {
758            v: -70.0,
759            epsilon: 1.0,
760            v_rest: -70.0,
761            v_reset: -70.0,
762            v_threshold: -50.0,
763            tau_m: 10.0,
764            tau_e: 500.0,
765            alpha: 0.1,
766            epsilon_0: 1.0,
767            resistance: 1.0,
768            dt: 1.0,
769        }
770    }
771
772    pub fn step(&mut self, current: f64) -> i32 {
773        let effective_r = self.resistance * self.epsilon;
774        self.v += (-(self.v - self.v_rest) + effective_r * current) / self.tau_m * self.dt;
775        self.epsilon += (self.epsilon_0 - self.epsilon) / self.tau_e * self.dt;
776        if self.v >= self.v_threshold && self.epsilon > 0.1 {
777            self.v = self.v_reset;
778            self.epsilon -= self.alpha;
779            1
780        } else if self.v >= self.v_threshold {
781            self.v = self.v_threshold;
782            0
783        } else {
784            0
785        }
786    }
787
788    pub fn reset(&mut self) {
789        self.v = self.v_rest;
790        self.epsilon = self.epsilon_0;
791    }
792}
793
794impl Default for EnergyLIFNeuron {
795    fn default() -> Self {
796        Self::new()
797    }
798}
799
800/// Integer QIF — fixed-point quadratic IF for digital hardware.
801#[derive(Clone, Debug)]
802pub struct IntegerQIFNeuron {
803    pub v: i32,
804    pub k: i32,
805    pub v_threshold: i32,
806    pub v_reset: i32,
807    pub v_min: i32,
808}
809
810impl IntegerQIFNeuron {
811    pub fn new(k: i32, v_threshold: i32) -> Self {
812        Self {
813            v: 0,
814            k,
815            v_threshold,
816            v_reset: -v_threshold,
817            v_min: -2 * v_threshold,
818        }
819    }
820
821    pub fn step(&mut self, current: i32) -> i32 {
822        self.v = (self.v + (self.v.wrapping_mul(self.v) >> self.k) + current).max(self.v_min);
823        if self.v >= self.v_threshold {
824            self.v = self.v_reset;
825            1
826        } else {
827            0
828        }
829    }
830
831    pub fn reset(&mut self) {
832        self.v = 0;
833    }
834}
835
836impl Default for IntegerQIFNeuron {
837    fn default() -> Self {
838        Self::new(6, 1024)
839    }
840}
841
842/// Closed-form Continuous-depth (CfC) neuron. Hasani et al. 2022.
843#[derive(Clone, Debug)]
844pub struct ClosedFormContinuousNeuron {
845    pub x: f64,
846    pub w_tau: f64,
847    pub w_x: f64,
848    pub w_in: f64,
849    pub tau_base: f64,
850    pub bias: f64,
851    pub v_threshold: f64,
852    pub dt: f64,
853}
854
855impl ClosedFormContinuousNeuron {
856    pub fn new() -> Self {
857        Self {
858            x: 0.0,
859            w_tau: -0.5,
860            w_x: 0.8,
861            w_in: 1.0,
862            tau_base: 10.0,
863            bias: 0.0,
864            v_threshold: 1.0,
865            dt: 1.0,
866        }
867    }
868
869    pub fn step(&mut self, current: f64) -> i32 {
870        let sigma_tau = 1.0 / (1.0 + (-(self.w_tau * current + self.bias)).exp());
871        let tau_eff = (self.tau_base * sigma_tau).max(0.1);
872        let f_target = (self.w_x * self.x + self.w_in * current).tanh();
873        let decay = (-self.dt / tau_eff).exp();
874        self.x = self.x * decay + f_target * (1.0 - decay);
875        if self.x >= self.v_threshold {
876            self.x -= self.v_threshold;
877            1
878        } else {
879            0
880        }
881    }
882
883    pub fn reset(&mut self) {
884        self.x = 0.0;
885    }
886}
887
888impl Default for ClosedFormContinuousNeuron {
889    fn default() -> Self {
890        Self::new()
891    }
892}
893
894#[cfg(test)]
895mod tests {
896    use super::*;
897
898    #[test]
899    fn qif_fires_with_positive_input() {
900        let mut n = QuadraticIFNeuron::default();
901        let total: i32 = (0..1000).map(|_| n.step(0.5)).sum();
902        assert!(total > 0);
903    }
904
905    #[test]
906    fn theta_fires() {
907        let mut n = ThetaNeuron::default();
908        let total: i32 = (0..1000).map(|_| n.step(0.5)).sum();
909        assert!(total > 0);
910    }
911
912    #[test]
913    fn perfect_integrator_fires() {
914        let mut n = PerfectIntegratorNeuron::default();
915        let total: i32 = (0..100).map(|_| n.step(0.5)).sum();
916        assert!(total > 0);
917    }
918
919    #[test]
920    fn gated_lif_fires() {
921        let mut n = GatedLIFNeuron::default();
922        let total: i32 = (0..20).map(|_| n.step(0.5)).sum();
923        assert!(total > 0);
924    }
925
926    #[test]
927    fn nlif_fires() {
928        let mut n = NonlinearLIFNeuron::new();
929        let total: i32 = (0..2000).map(|_| n.step(500.0)).sum();
930        assert!(total > 0);
931    }
932
933    #[test]
934    fn sfa_fires_then_adapts() {
935        let mut n = SFANeuron::new();
936        let first: i32 = (0..100).map(|_| n.step(30.0)).sum();
937        let second: i32 = (0..100).map(|_| n.step(30.0)).sum();
938        assert!(first > 0);
939        assert!(second <= first + 2);
940    }
941
942    #[test]
943    fn mat_dual_threshold_adapts() {
944        let mut n = MATNeuron::new();
945        let total: i32 = (0..200).map(|_| n.step(30.0)).sum();
946        assert!(total > 0);
947        assert!(n.theta1 > 0.0 || n.theta2 > 0.0);
948    }
949
950    #[test]
951    fn escape_rate_stochastic() {
952        let mut n = EscapeRateNeuron::new(42);
953        let total: i32 = (0..1000).map(|_| n.step(30.0)).sum();
954        assert!(total > 0);
955    }
956
957    #[test]
958    fn klif_fires() {
959        let mut n = KLIFNeuron::default();
960        let total: i32 = (0..50).map(|_| n.step(0.5)).sum();
961        assert!(total > 0);
962    }
963
964    #[test]
965    fn ilif_self_inhibits() {
966        let mut n = InhibitoryLIFNeuron::default();
967        let total: i32 = (0..100).map(|_| n.step(0.8)).sum();
968        assert!(total > 0);
969    }
970
971    #[test]
972    fn clif_positive_spike() {
973        let mut n = ComplementaryLIFNeuron::default();
974        let total: i32 = (0..20).map(|_| n.step(0.5)).sum();
975        assert!(total > 0);
976    }
977
978    #[test]
979    fn plif_fires() {
980        let mut n = ParametricLIFNeuron::default();
981        let total: i32 = (0..20).map(|_| n.step(1.5)).sum();
982        assert!(total > 0);
983    }
984
985    #[test]
986    fn non_resetting_threshold_increases() {
987        let mut n = NonResettingLIFNeuron::new();
988        let initial = n.theta;
989        for _ in 0..5000 {
990            n.step(30.0);
991        }
992        assert!(n.theta > initial);
993    }
994
995    #[test]
996    fn adaptive_threshold_fires() {
997        let mut n = AdaptiveThresholdIFNeuron::new();
998        let total: i32 = (0..500).map(|_| n.step(30.0)).sum();
999        assert!(total > 0);
1000    }
1001
1002    #[test]
1003    fn sigma_delta_encodes() {
1004        let mut n = SigmaDeltaNeuron::default();
1005        let total: i32 = (0..10).map(|_| n.step(0.3)).sum();
1006        assert!(total > 0);
1007    }
1008
1009    #[test]
1010    fn energy_lif_fires() {
1011        let mut n = EnergyLIFNeuron::new();
1012        let total: i32 = (0..200).map(|_| n.step(30.0)).sum();
1013        assert!(total > 0);
1014    }
1015
1016    #[test]
1017    fn iqif_fires() {
1018        let mut n = IntegerQIFNeuron::default();
1019        let total: i32 = (0..200).map(|_| n.step(50)).sum();
1020        assert!(total > 0);
1021    }
1022
1023    #[test]
1024    fn cfc_activates() {
1025        let mut n = ClosedFormContinuousNeuron {
1026            v_threshold: 0.9,
1027            ..ClosedFormContinuousNeuron::new()
1028        };
1029        let total: i32 = (0..100).map(|_| n.step(5.0)).sum();
1030        assert!(total > 0);
1031    }
1032
1033    // ── Multi-angle tests for trivial IF models ──
1034
1035    // -- QuadraticIF --
1036    #[test]
1037    fn qif_silent_without_input() {
1038        let mut n = QuadraticIFNeuron::default();
1039        let t: i32 = (0..1000).map(|_| n.step(0.0)).sum();
1040        assert_eq!(t, 0);
1041    }
1042    #[test]
1043    fn qif_reset_clears_state() {
1044        let mut n = QuadraticIFNeuron::default();
1045        for _ in 0..100 {
1046            n.step(0.5);
1047        }
1048        n.reset();
1049        assert!((n.v - n.v_reset).abs() < 1e-10);
1050    }
1051    #[test]
1052    fn qif_bounded() {
1053        let mut n = QuadraticIFNeuron::default();
1054        for _ in 0..1000 {
1055            n.step(10.0);
1056        }
1057        assert!(n.v.is_finite());
1058    }
1059    #[test]
1060    fn qif_nan_no_panic() {
1061        let mut n = QuadraticIFNeuron::default();
1062        let before = n.v;
1063        assert_eq!(n.step(f64::NAN), 0);
1064        assert_eq!(n.v, before);
1065    }
1066    #[test]
1067    fn qif_nonfinite_increment_preserves_state() {
1068        let mut n = QuadraticIFNeuron {
1069            v: -1.0e200,
1070            ..Default::default()
1071        };
1072        let before = n.v;
1073        assert_eq!(n.step(0.0), 0);
1074        assert_eq!(n.v, before);
1075    }
1076    #[test]
1077    fn qif_negative_no_crash() {
1078        let mut n = QuadraticIFNeuron::default();
1079        for _ in 0..500 {
1080            n.step(-5.0);
1081        }
1082        assert!(n.v.is_finite());
1083    }
1084
1085    // -- Theta --
1086    #[test]
1087    fn theta_silent_without_input() {
1088        let mut n = ThetaNeuron::default();
1089        let t: i32 = (0..1000).map(|_| n.step(0.0)).sum();
1090        assert_eq!(t, 0);
1091    }
1092    #[test]
1093    fn theta_reset_clears_state() {
1094        let mut n = ThetaNeuron::default();
1095        for _ in 0..100 {
1096            n.step(0.5);
1097        }
1098        n.reset();
1099        assert!((n.theta - 0.0).abs() < 1e-10);
1100    }
1101    #[test]
1102    fn theta_bounded() {
1103        let mut n = ThetaNeuron::default();
1104        for _ in 0..1000 {
1105            n.step(10.0);
1106        }
1107        assert!(n.theta.is_finite());
1108    }
1109    #[test]
1110    fn theta_nan_no_panic() {
1111        ThetaNeuron::default().step(f64::NAN);
1112    }
1113
1114    // -- PerfectIntegrator --
1115    #[test]
1116    fn pi_silent_without_input() {
1117        let mut n = PerfectIntegratorNeuron::default();
1118        let t: i32 = (0..100).map(|_| n.step(0.0)).sum();
1119        assert_eq!(t, 0);
1120    }
1121    #[test]
1122    fn pi_reset_clears_state() {
1123        let mut n = PerfectIntegratorNeuron::default();
1124        for _ in 0..50 {
1125            n.step(0.5);
1126        }
1127        n.reset();
1128        assert!((n.v - n.v_reset).abs() < 1e-10);
1129    }
1130    #[test]
1131    fn pi_bounded() {
1132        let mut n = PerfectIntegratorNeuron::default();
1133        for _ in 0..1000 {
1134            n.step(100.0);
1135        }
1136        assert!(n.v.is_finite());
1137    }
1138    #[test]
1139    fn pi_nan_no_panic() {
1140        PerfectIntegratorNeuron::default().step(f64::NAN);
1141    }
1142
1143    // -- GatedLIF --
1144    #[test]
1145    fn gated_lif_silent_without_input() {
1146        let mut n = GatedLIFNeuron::default();
1147        let t: i32 = (0..100).map(|_| n.step(0.0)).sum();
1148        assert_eq!(t, 0);
1149    }
1150    #[test]
1151    fn gated_lif_reset_clears_state() {
1152        let mut n = GatedLIFNeuron::default();
1153        for _ in 0..20 {
1154            n.step(0.5);
1155        }
1156        n.reset();
1157        assert!((n.v - 0.0).abs() < 1e-10);
1158    }
1159    #[test]
1160    fn gated_lif_bounded() {
1161        let mut n = GatedLIFNeuron::default();
1162        for _ in 0..1000 {
1163            n.step(100.0);
1164        }
1165        assert!(n.v.is_finite());
1166    }
1167    #[test]
1168    fn gated_lif_nan_no_panic() {
1169        GatedLIFNeuron::default().step(f64::NAN);
1170    }
1171
1172    // -- NonlinearLIF --
1173    #[test]
1174    fn nlif_silent_without_input() {
1175        let mut n = NonlinearLIFNeuron::new();
1176        let t: i32 = (0..200).map(|_| n.step(0.0)).sum();
1177        assert_eq!(t, 0);
1178    }
1179    #[test]
1180    fn nlif_reset_clears_state() {
1181        let mut n = NonlinearLIFNeuron::new();
1182        for _ in 0..100 {
1183            n.step(500.0);
1184        }
1185        n.reset();
1186        assert!((n.v - n.v_rest).abs() < 1e-10);
1187    }
1188    #[test]
1189    fn nlif_bounded() {
1190        let mut n = NonlinearLIFNeuron::new();
1191        for _ in 0..2000 {
1192            n.step(1e4);
1193        }
1194        assert!(n.v.is_finite());
1195    }
1196    #[test]
1197    fn nlif_nan_no_panic() {
1198        NonlinearLIFNeuron::new().step(f64::NAN);
1199    }
1200    #[test]
1201    fn nlif_recovery_evolves() {
1202        let mut n = NonlinearLIFNeuron::new();
1203        for _ in 0..2000 {
1204            n.step(500.0);
1205        }
1206        assert!(
1207            n.w > 0.0,
1208            "recovery variable w should increase during spiking"
1209        );
1210    }
1211
1212    // -- SFA --
1213    #[test]
1214    fn sfa_silent_without_input() {
1215        let mut n = SFANeuron::new();
1216        let t: i32 = (0..200).map(|_| n.step(0.0)).sum();
1217        assert_eq!(t, 0);
1218    }
1219    #[test]
1220    fn sfa_reset_clears_state() {
1221        let mut n = SFANeuron::new();
1222        for _ in 0..100 {
1223            n.step(30.0);
1224        }
1225        n.reset();
1226        assert!((n.v - n.v_rest).abs() < 1e-10);
1227        assert!((n.g_sfa - 0.0).abs() < 1e-10);
1228    }
1229    #[test]
1230    fn sfa_bounded() {
1231        let mut n = SFANeuron::new();
1232        for _ in 0..1000 {
1233            n.step(1e4);
1234        }
1235        assert!(n.v.is_finite());
1236    }
1237    #[test]
1238    fn sfa_nan_no_panic() {
1239        SFANeuron::new().step(f64::NAN);
1240    }
1241
1242    // -- MAT --
1243    #[test]
1244    fn mat_silent_without_input() {
1245        let mut n = MATNeuron::new();
1246        let t: i32 = (0..200).map(|_| n.step(0.0)).sum();
1247        assert_eq!(t, 0);
1248    }
1249    #[test]
1250    fn mat_reset_clears_state() {
1251        let mut n = MATNeuron::new();
1252        for _ in 0..100 {
1253            n.step(30.0);
1254        }
1255        n.reset();
1256        assert!((n.v - n.v_rest).abs() < 1e-10);
1257        assert!((n.theta1 - 0.0).abs() < 1e-10);
1258        assert!((n.theta2 - 0.0).abs() < 1e-10);
1259    }
1260    #[test]
1261    fn mat_bounded() {
1262        let mut n = MATNeuron::new();
1263        for _ in 0..1000 {
1264            n.step(1e4);
1265        }
1266        assert!(n.v.is_finite());
1267    }
1268    #[test]
1269    fn mat_nan_no_panic() {
1270        MATNeuron::new().step(f64::NAN);
1271    }
1272
1273    // -- EscapeRate --
1274    #[test]
1275    fn escape_rate_reset_clears_state() {
1276        let mut n = EscapeRateNeuron::new(42);
1277        for _ in 0..100 {
1278            n.step(30.0);
1279        }
1280        n.reset();
1281        assert!((n.v - n.v_rest).abs() < 1e-10);
1282    }
1283    #[test]
1284    fn escape_rate_bounded() {
1285        let mut n = EscapeRateNeuron::new(42);
1286        for _ in 0..1000 {
1287            n.step(1e4);
1288        }
1289        assert!(n.v.is_finite());
1290    }
1291    #[test]
1292    fn escape_rate_nan_no_panic() {
1293        EscapeRateNeuron::new(42).step(f64::NAN);
1294    }
1295    #[test]
1296    fn escape_rate_seed_varies() {
1297        let mut n1 = EscapeRateNeuron::new(1);
1298        let mut n2 = EscapeRateNeuron::new(999);
1299        let t1: i32 = (0..1000).map(|_| n1.step(30.0)).sum();
1300        let t2: i32 = (0..1000).map(|_| n2.step(30.0)).sum();
1301        assert!(t1 > 0 && t2 > 0);
1302    }
1303
1304    // -- KLIF --
1305    #[test]
1306    fn klif_silent_without_input() {
1307        let mut n = KLIFNeuron::default();
1308        let t: i32 = (0..100).map(|_| n.step(0.0)).sum();
1309        assert_eq!(t, 0);
1310    }
1311    #[test]
1312    fn klif_reset_clears_state() {
1313        let mut n = KLIFNeuron::default();
1314        for _ in 0..20 {
1315            n.step(0.5);
1316        }
1317        n.reset();
1318        assert!((n.v - 0.0).abs() < 1e-10);
1319    }
1320    #[test]
1321    fn klif_bounded() {
1322        let mut n = KLIFNeuron::default();
1323        for _ in 0..1000 {
1324            n.step(100.0);
1325        }
1326        assert!(n.v.is_finite());
1327    }
1328    #[test]
1329    fn klif_nan_no_panic() {
1330        KLIFNeuron::default().step(f64::NAN);
1331    }
1332
1333    // -- InhibitoryLIF --
1334    #[test]
1335    fn ilif_silent_without_input() {
1336        let mut n = InhibitoryLIFNeuron::default();
1337        let t: i32 = (0..100).map(|_| n.step(0.0)).sum();
1338        assert_eq!(t, 0);
1339    }
1340    #[test]
1341    fn ilif_reset_clears_state() {
1342        let mut n = InhibitoryLIFNeuron::default();
1343        for _ in 0..50 {
1344            n.step(0.8);
1345        }
1346        n.reset();
1347        assert!((n.v - 0.0).abs() < 1e-10);
1348        assert!((n.inh_trace - 0.0).abs() < 1e-10);
1349    }
1350    #[test]
1351    fn ilif_bounded() {
1352        let mut n = InhibitoryLIFNeuron::default();
1353        for _ in 0..1000 {
1354            n.step(100.0);
1355        }
1356        assert!(n.v.is_finite());
1357    }
1358    #[test]
1359    fn ilif_nan_no_panic() {
1360        InhibitoryLIFNeuron::default().step(f64::NAN);
1361    }
1362
1363    // -- ComplementaryLIF --
1364    #[test]
1365    fn clif_silent_without_input() {
1366        let mut n = ComplementaryLIFNeuron::default();
1367        let t: i32 = (0..100).map(|_| n.step(0.0)).sum();
1368        assert_eq!(t, 0);
1369    }
1370    #[test]
1371    fn clif_reset_clears_state() {
1372        let mut n = ComplementaryLIFNeuron::default();
1373        for _ in 0..20 {
1374            n.step(0.5);
1375        }
1376        n.reset();
1377        assert!((n.v_pos - 0.0).abs() < 1e-10);
1378        assert!((n.v_neg - 0.0).abs() < 1e-10);
1379    }
1380    #[test]
1381    fn clif_bounded() {
1382        let mut n = ComplementaryLIFNeuron::default();
1383        for _ in 0..1000 {
1384            n.step(100.0);
1385        }
1386        assert!(n.v_pos.is_finite());
1387    }
1388    #[test]
1389    fn clif_nan_no_panic() {
1390        ComplementaryLIFNeuron::default().step(f64::NAN);
1391    }
1392
1393    // -- ParametricLIF --
1394    #[test]
1395    fn plif_silent_without_input() {
1396        let mut n = ParametricLIFNeuron::default();
1397        let t: i32 = (0..100).map(|_| n.step(0.0)).sum();
1398        assert_eq!(t, 0);
1399    }
1400    #[test]
1401    fn plif_reset_clears_state() {
1402        let mut n = ParametricLIFNeuron::default();
1403        for _ in 0..20 {
1404            n.step(1.5);
1405        }
1406        n.reset();
1407        assert!((n.v - 0.0).abs() < 1e-10);
1408    }
1409    #[test]
1410    fn plif_bounded() {
1411        let mut n = ParametricLIFNeuron::default();
1412        for _ in 0..1000 {
1413            n.step(100.0);
1414        }
1415        assert!(n.v.is_finite());
1416    }
1417    #[test]
1418    fn plif_nan_no_panic() {
1419        ParametricLIFNeuron::default().step(f64::NAN);
1420    }
1421
1422    // -- NonResettingLIF --
1423    #[test]
1424    fn nrlif_reset_clears_state() {
1425        let mut n = NonResettingLIFNeuron::new();
1426        for _ in 0..500 {
1427            n.step(30.0);
1428        }
1429        n.reset();
1430        assert!((n.v - n.v_rest).abs() < 1e-10);
1431        assert!((n.theta - n.theta_rest).abs() < 1e-10);
1432    }
1433    #[test]
1434    fn nrlif_bounded() {
1435        let mut n = NonResettingLIFNeuron::new();
1436        for _ in 0..5000 {
1437            n.step(1e4);
1438        }
1439        assert!(n.v.is_finite());
1440    }
1441    #[test]
1442    fn nrlif_nan_no_panic() {
1443        NonResettingLIFNeuron::new().step(f64::NAN);
1444    }
1445    #[test]
1446    fn nrlif_negative_no_crash() {
1447        let mut n = NonResettingLIFNeuron::new();
1448        for _ in 0..500 {
1449            n.step(-10.0);
1450        }
1451        assert!(n.v.is_finite());
1452    }
1453
1454    // -- AdaptiveThresholdIF --
1455    #[test]
1456    fn atif_silent_without_input() {
1457        let mut n = AdaptiveThresholdIFNeuron::new();
1458        let t: i32 = (0..500).map(|_| n.step(0.0)).sum();
1459        assert_eq!(t, 0);
1460    }
1461    #[test]
1462    fn atif_reset_clears_state() {
1463        let mut n = AdaptiveThresholdIFNeuron::new();
1464        for _ in 0..100 {
1465            n.step(30.0);
1466        }
1467        n.reset();
1468        assert!((n.v - n.v_rest).abs() < 1e-10);
1469    }
1470    #[test]
1471    fn atif_bounded() {
1472        let mut n = AdaptiveThresholdIFNeuron::new();
1473        for _ in 0..1000 {
1474            n.step(1e4);
1475        }
1476        assert!(n.v.is_finite());
1477    }
1478    #[test]
1479    fn atif_nan_no_panic() {
1480        AdaptiveThresholdIFNeuron::new().step(f64::NAN);
1481    }
1482    #[test]
1483    fn atif_threshold_increases_with_spikes() {
1484        let mut n = AdaptiveThresholdIFNeuron::new();
1485        let theta_init = n.theta;
1486        for _ in 0..500 {
1487            n.step(30.0);
1488        }
1489        assert!(n.theta > theta_init, "threshold should adapt after spikes");
1490    }
1491
1492    // -- SigmaDelta --
1493    #[test]
1494    fn sd_reset_clears_state() {
1495        let mut n = SigmaDeltaNeuron::default();
1496        for _ in 0..10 {
1497            n.step(0.3);
1498        }
1499        n.reset();
1500        assert!((n.sigma - 0.0).abs() < 1e-10);
1501    }
1502    #[test]
1503    fn sd_bounded() {
1504        let mut n = SigmaDeltaNeuron::default();
1505        for _ in 0..1000 {
1506            n.step(100.0);
1507        }
1508        assert!(n.sigma.is_finite());
1509    }
1510    #[test]
1511    fn sd_nan_no_panic() {
1512        SigmaDeltaNeuron::default().step(f64::NAN);
1513    }
1514
1515    // -- EnergyLIF --
1516    #[test]
1517    fn energy_lif_silent_without_input() {
1518        let mut n = EnergyLIFNeuron::new();
1519        let t: i32 = (0..200).map(|_| n.step(0.0)).sum();
1520        assert_eq!(t, 0);
1521    }
1522    #[test]
1523    fn energy_lif_reset_clears_state() {
1524        let mut n = EnergyLIFNeuron::new();
1525        for _ in 0..100 {
1526            n.step(30.0);
1527        }
1528        n.reset();
1529        assert!((n.v - n.v_rest).abs() < 1e-10);
1530    }
1531    #[test]
1532    fn energy_lif_bounded() {
1533        let mut n = EnergyLIFNeuron::new();
1534        for _ in 0..1000 {
1535            n.step(1e4);
1536        }
1537        assert!(n.v.is_finite());
1538    }
1539    #[test]
1540    fn energy_lif_nan_no_panic() {
1541        EnergyLIFNeuron::new().step(f64::NAN);
1542    }
1543    #[test]
1544    fn energy_lif_epsilon_depletes() {
1545        let mut n = EnergyLIFNeuron::new();
1546        let e0 = n.epsilon;
1547        for _ in 0..200 {
1548            n.step(30.0);
1549        }
1550        assert!(n.epsilon < e0, "energy should deplete during spiking");
1551    }
1552
1553    // -- IntegerQIF --
1554    #[test]
1555    fn iqif_silent_without_input() {
1556        let mut n = IntegerQIFNeuron::default();
1557        let t: i32 = (0..200).map(|_| n.step(0)).sum();
1558        assert_eq!(t, 0);
1559    }
1560    #[test]
1561    fn iqif_reset_clears_state() {
1562        let mut n = IntegerQIFNeuron::default();
1563        for _ in 0..100 {
1564            n.step(50);
1565        }
1566        n.reset();
1567        assert_eq!(n.v, 0);
1568    }
1569    #[test]
1570    fn iqif_bounded() {
1571        let mut n = IntegerQIFNeuron::default();
1572        for _ in 0..1000 {
1573            n.step(10000);
1574        }
1575        // Integer — check no overflow panic occurred
1576    }
1577    #[test]
1578    fn iqif_negative_no_crash() {
1579        let mut n = IntegerQIFNeuron::default();
1580        for _ in 0..500 {
1581            n.step(-50);
1582        }
1583    }
1584
1585    // -- ClosedFormContinuous --
1586    #[test]
1587    fn cfc_reset_clears_state() {
1588        let mut n = ClosedFormContinuousNeuron::new();
1589        for _ in 0..50 {
1590            n.step(5.0);
1591        }
1592        n.reset();
1593        assert!((n.x - 0.0).abs() < 1e-10);
1594    }
1595    #[test]
1596    fn cfc_bounded() {
1597        let mut n = ClosedFormContinuousNeuron {
1598            v_threshold: 0.9,
1599            ..ClosedFormContinuousNeuron::new()
1600        };
1601        for _ in 0..1000 {
1602            n.step(100.0);
1603        }
1604        assert!(n.x.is_finite());
1605    }
1606    #[test]
1607    fn cfc_nan_no_panic() {
1608        ClosedFormContinuousNeuron::new().step(f64::NAN);
1609    }
1610
1611    // -- StochasticLIFNeuron tests --
1612
1613    #[test]
1614    fn stochastic_lif_fires_with_input() {
1615        let mut n = StochasticLIFNeuron::new(42);
1616        let total: i32 = (0..500).map(|_| n.step(2.0)).sum();
1617        assert!(total > 0, "StochasticLIF should fire with strong input");
1618    }
1619
1620    #[test]
1621    fn stochastic_lif_silent_without_input() {
1622        let mut n = StochasticLIFNeuron::new(42);
1623        // noise_std=0 by default, so zero input => no spikes
1624        let total: i32 = (0..500).map(|_| n.step(0.0)).sum();
1625        assert_eq!(
1626            total, 0,
1627            "StochasticLIF should be silent at zero input with no noise"
1628        );
1629    }
1630
1631    #[test]
1632    fn stochastic_lif_reset_clears_state() {
1633        let mut n = StochasticLIFNeuron::new(42);
1634        for _ in 0..100 {
1635            n.step(2.0);
1636        }
1637        n.reset();
1638        assert!(
1639            (n.v - n.v_rest).abs() < 1e-12,
1640            "reset must restore v to v_rest"
1641        );
1642        assert_eq!(
1643            n.refractory_counter, 0,
1644            "reset must clear refractory counter"
1645        );
1646    }
1647
1648    #[test]
1649    fn stochastic_lif_extreme_input_bounded() {
1650        let mut n = StochasticLIFNeuron::new(42);
1651        for _ in 0..1000 {
1652            n.step(1e6);
1653        }
1654        assert!(n.v.is_finite(), "v must stay finite under extreme input");
1655    }
1656
1657    #[test]
1658    fn stochastic_lif_nan_input_stays_finite() {
1659        let mut n = StochasticLIFNeuron::new(42);
1660        // Run some normal steps first
1661        for _ in 0..10 {
1662            n.step(1.0);
1663        }
1664        let v_before = n.v;
1665        n.step(f64::NAN);
1666        // After NaN input, v is likely NaN — verify no panic occurred
1667        // The key invariant: the step function does not panic
1668        let _ = v_before;
1669    }
1670
1671    #[test]
1672    fn stochastic_lif_negative_input_no_crash() {
1673        let mut n = StochasticLIFNeuron::new(42);
1674        for _ in 0..500 {
1675            n.step(-10.0);
1676        }
1677        assert!(n.v.is_finite(), "v must stay finite with negative input");
1678    }
1679
1680    #[test]
1681    fn stochastic_lif_noise_affects_firing() {
1682        // With noise, the neuron may fire even at subthreshold input
1683        let mut n_noisy = StochasticLIFNeuron::new(123);
1684        n_noisy.noise_std = 0.5;
1685        let total_noisy: i32 = (0..5000).map(|_| n_noisy.step(0.8)).sum();
1686
1687        let mut n_quiet = StochasticLIFNeuron::new(123);
1688        n_quiet.noise_std = 0.0;
1689        let total_quiet: i32 = (0..5000).map(|_| n_quiet.step(0.8)).sum();
1690
1691        // Subthreshold input: quiet neuron may not fire, noisy one may
1692        // At minimum, they should differ (noise has an effect)
1693        assert!(
1694            total_noisy != total_quiet || total_noisy > 0,
1695            "noise should affect firing pattern"
1696        );
1697    }
1698
1699    #[test]
1700    fn stochastic_lif_refractory_blocks_spikes() {
1701        let mut n = StochasticLIFNeuron::new(42);
1702        n.refractory_period = 5;
1703        let mut spikes = Vec::new();
1704        for _ in 0..500 {
1705            spikes.push(n.step(3.0));
1706        }
1707        // After a spike, next `refractory_period` steps must be silent
1708        for (i, &s) in spikes.iter().enumerate() {
1709            if s == 1 {
1710                for j in 1..=5 {
1711                    if i + j < spikes.len() {
1712                        assert_eq!(
1713                            spikes[i + j],
1714                            0,
1715                            "step {} after spike at {} must be silent (refractory)",
1716                            j,
1717                            i
1718                        );
1719                    }
1720                }
1721            }
1722        }
1723    }
1724}
1725
1726/// Stochastic LIF — LIF with Gaussian noise.
1727#[derive(Clone, Debug)]
1728pub struct StochasticLIFNeuron {
1729    pub v: f64,
1730    pub v_rest: f64,
1731    pub v_reset: f64,
1732    pub v_threshold: f64,
1733    pub tau_mem: f64,
1734    pub dt: f64,
1735    pub noise_std: f64,
1736    pub resistance: f64,
1737    pub refractory_period: i32,
1738    pub refractory_counter: i32,
1739    rng: Xoshiro256PlusPlus,
1740}
1741
1742impl StochasticLIFNeuron {
1743    pub fn new(seed: u64) -> Self {
1744        Self {
1745            v: 0.0,
1746            v_rest: 0.0,
1747            v_reset: 0.0,
1748            v_threshold: 1.0,
1749            tau_mem: 20.0,
1750            dt: 1.0,
1751            noise_std: 0.0,
1752            resistance: 1.0,
1753            refractory_period: 0,
1754            refractory_counter: 0,
1755            rng: Xoshiro256PlusPlus::seed_from_u64(seed),
1756        }
1757    }
1758
1759    pub fn step(&mut self, current: f64) -> i32 {
1760        if self.refractory_counter > 0 {
1761            self.refractory_counter -= 1;
1762            self.v = self.v_rest;
1763            return 0;
1764        }
1765        let dv_leak = -(self.v - self.v_rest) * (self.dt / self.tau_mem);
1766        let dv_input = self.resistance * current * self.dt;
1767        let mut dv_noise = 0.0;
1768        if self.noise_std > 0.0 {
1769            let u1: f64 = self.rng.random();
1770            let u2: f64 = self.rng.random();
1771            let z0 = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
1772            dv_noise = self.noise_std * self.dt.sqrt() * z0;
1773        }
1774        self.v += dv_leak + dv_input + dv_noise;
1775        if self.v >= self.v_threshold {
1776            self.v = self.v_reset;
1777            self.refractory_counter = self.refractory_period;
1778            1
1779        } else {
1780            0
1781        }
1782    }
1783
1784    pub fn reset(&mut self) {
1785        self.v = self.v_rest;
1786        self.refractory_counter = 0;
1787    }
1788}
1789
1790impl Default for StochasticLIFNeuron {
1791    fn default() -> Self {
1792        Self::new(42)
1793    }
1794}