sc_neurocore_engine/neurons/trivial/
stochastic_lif.rs1use rand::{RngExt, SeedableRng};
10use rand_xoshiro::Xoshiro256PlusPlus;
11
12#[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 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 for _ in 0..10 {
131 n.step(1.0);
132 }
133 let v_before = n.v;
134 n.step(f64::NAN);
135 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 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 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 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}