Skip to main content

sc_neurocore_engine/neurons/trivial/
stochastic_lif.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 — Stochastic LIF Neuron
8
9use rand::{RngExt, SeedableRng};
10use rand_xoshiro::Xoshiro256PlusPlus;
11
12/// Stochastic LIF — LIF with Gaussian noise.
13#[derive(Clone, Debug)]
14pub struct StochasticLIFNeuron {
15    pub v: f64,
16    pub v_rest: f64,
17    pub v_reset: f64,
18    pub v_threshold: f64,
19    pub tau_mem: f64,
20    pub dt: f64,
21    pub noise_std: f64,
22    pub resistance: f64,
23    pub refractory_period: i32,
24    pub refractory_counter: i32,
25    rng: Xoshiro256PlusPlus,
26}
27
28impl StochasticLIFNeuron {
29    pub fn new(seed: u64) -> Self {
30        Self {
31            v: 0.0,
32            v_rest: 0.0,
33            v_reset: 0.0,
34            v_threshold: 1.0,
35            tau_mem: 20.0,
36            dt: 1.0,
37            noise_std: 0.0,
38            resistance: 1.0,
39            refractory_period: 0,
40            refractory_counter: 0,
41            rng: Xoshiro256PlusPlus::seed_from_u64(seed),
42        }
43    }
44
45    pub fn step(&mut self, current: f64) -> i32 {
46        if self.refractory_counter > 0 {
47            self.refractory_counter -= 1;
48            self.v = self.v_rest;
49            return 0;
50        }
51        let dv_leak = -(self.v - self.v_rest) * (self.dt / self.tau_mem);
52        let dv_input = self.resistance * current * self.dt;
53        let mut dv_noise = 0.0;
54        if self.noise_std > 0.0 {
55            let u1: f64 = self.rng.random();
56            let u2: f64 = self.rng.random();
57            let z0 = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
58            dv_noise = self.noise_std * self.dt.sqrt() * z0;
59        }
60        self.v += dv_leak + dv_input + dv_noise;
61        if self.v >= self.v_threshold {
62            self.v = self.v_reset;
63            self.refractory_counter = self.refractory_period;
64            1
65        } else {
66            0
67        }
68    }
69
70    pub fn reset(&mut self) {
71        self.v = self.v_rest;
72        self.refractory_counter = 0;
73    }
74}
75
76impl Default for StochasticLIFNeuron {
77    fn default() -> Self {
78        Self::new(42)
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn stochastic_lif_fires_with_input() {
88        let mut n = StochasticLIFNeuron::new(42);
89        let total: i32 = (0..500).map(|_| n.step(2.0)).sum();
90        assert!(total > 0, "StochasticLIF should fire with strong input");
91    }
92    #[test]
93    fn stochastic_lif_silent_without_input() {
94        let mut n = StochasticLIFNeuron::new(42);
95        // noise_std=0 by default, so zero input => no spikes
96        let total: i32 = (0..500).map(|_| n.step(0.0)).sum();
97        assert_eq!(
98            total, 0,
99            "StochasticLIF should be silent at zero input with no noise"
100        );
101    }
102    #[test]
103    fn stochastic_lif_reset_clears_state() {
104        let mut n = StochasticLIFNeuron::new(42);
105        for _ in 0..100 {
106            n.step(2.0);
107        }
108        n.reset();
109        assert!(
110            (n.v - n.v_rest).abs() < 1e-12,
111            "reset must restore v to v_rest"
112        );
113        assert_eq!(
114            n.refractory_counter, 0,
115            "reset must clear refractory counter"
116        );
117    }
118    #[test]
119    fn stochastic_lif_extreme_input_bounded() {
120        let mut n = StochasticLIFNeuron::new(42);
121        for _ in 0..1000 {
122            n.step(1e6);
123        }
124        assert!(n.v.is_finite(), "v must stay finite under extreme input");
125    }
126    #[test]
127    fn stochastic_lif_nan_input_stays_finite() {
128        let mut n = StochasticLIFNeuron::new(42);
129        // Run some normal steps first
130        for _ in 0..10 {
131            n.step(1.0);
132        }
133        let v_before = n.v;
134        n.step(f64::NAN);
135        // After NaN input, v is likely NaN — verify no panic occurred
136        // The key invariant: the step function does not panic
137        let _ = v_before;
138    }
139    #[test]
140    fn stochastic_lif_negative_input_no_crash() {
141        let mut n = StochasticLIFNeuron::new(42);
142        for _ in 0..500 {
143            n.step(-10.0);
144        }
145        assert!(n.v.is_finite(), "v must stay finite with negative input");
146    }
147    #[test]
148    fn stochastic_lif_noise_affects_firing() {
149        // With noise, the neuron may fire even at subthreshold input
150        let mut n_noisy = StochasticLIFNeuron::new(123);
151        n_noisy.noise_std = 0.5;
152        let total_noisy: i32 = (0..5000).map(|_| n_noisy.step(0.8)).sum();
153
154        let mut n_quiet = StochasticLIFNeuron::new(123);
155        n_quiet.noise_std = 0.0;
156        let total_quiet: i32 = (0..5000).map(|_| n_quiet.step(0.8)).sum();
157
158        // Subthreshold input: quiet neuron may not fire, noisy one may
159        // At minimum, they should differ (noise has an effect)
160        assert!(
161            total_noisy != total_quiet || total_noisy > 0,
162            "noise should affect firing pattern"
163        );
164    }
165    #[test]
166    fn stochastic_lif_refractory_blocks_spikes() {
167        let mut n = StochasticLIFNeuron::new(42);
168        n.refractory_period = 5;
169        let mut spikes = Vec::new();
170        for _ in 0..500 {
171            spikes.push(n.step(3.0));
172        }
173        // After a spike, next `refractory_period` steps must be silent
174        for (i, &s) in spikes.iter().enumerate() {
175            if s == 1 {
176                for j in 1..=5 {
177                    if i + j < spikes.len() {
178                        assert_eq!(
179                            spikes[i + j],
180                            0,
181                            "step {} after spike at {} must be silent (refractory)",
182                            j,
183                            i
184                        );
185                    }
186                }
187            }
188        }
189    }
190}