Skip to main content

sc_neurocore_engine/neuron/
exp_if.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 — Exponential integrate-and-fire neuron
8
9/// Exponential IF (no adaptation). Fourcaud-Trocmé et al. 2003.
10#[derive(Clone, Debug)]
11pub struct ExpIfNeuron {
12    pub v: f64,
13    pub v_rest: f64,
14    pub v_reset: f64,
15    pub v_threshold: f64,
16    pub v_rh: f64,
17    pub delta_t: f64,
18    pub tau: f64,
19    pub dt: f64,
20    pub refractory_period: f64,
21    pub refractory_remaining: f64,
22    pub inv_delta_t: f64,
23    pub dt_div_tau: f64,
24}
25
26impl Default for ExpIfNeuron {
27    fn default() -> Self {
28        Self::new()
29    }
30}
31
32impl ExpIfNeuron {
33    pub fn new() -> Self {
34        Self {
35            v: -65.0,
36            v_rest: -65.0,
37            v_reset: -68.0,
38            v_threshold: 30.0,
39            v_rh: -59.9,
40            delta_t: 3.48,
41            tau: 10.0,
42            dt: 0.02,
43            refractory_period: 0.0,
44            refractory_remaining: 0.0,
45            inv_delta_t: 1.0 / 3.48,
46            dt_div_tau: 0.02 / 10.0,
47        }
48    }
49
50    pub fn step(&mut self, current: f64) -> i32 {
51        if !self.v.is_finite()
52            || !current.is_finite()
53            || !self.v_rest.is_finite()
54            || !self.v_reset.is_finite()
55            || !self.v_threshold.is_finite()
56            || !self.v_rh.is_finite()
57            || !self.delta_t.is_finite()
58            || !self.tau.is_finite()
59            || !self.dt.is_finite()
60            || !self.refractory_period.is_finite()
61            || !self.refractory_remaining.is_finite()
62            || self.delta_t <= 0.0
63            || self.tau <= 0.0
64            || self.dt <= 0.0
65            || self.refractory_period < 0.0
66            || self.refractory_remaining < 0.0
67            || self.refractory_remaining > self.refractory_period
68            || self.v_threshold <= self.v_rh
69            || self.v >= self.v_threshold
70            || self.v_rest >= self.v_threshold
71            || self.v_reset >= self.v_threshold
72        {
73            return 0;
74        }
75
76        if self.refractory_remaining > 0.0 {
77            self.refractory_remaining = (self.refractory_remaining - self.dt).max(0.0);
78            self.v = self.v_reset;
79            return 0;
80        }
81
82        let inv_delta_t = 1.0 / self.delta_t;
83        let k1 = self.rhs(self.v, current, inv_delta_t);
84        let k2 = self.rhs(self.v + 0.5 * self.dt * k1, current, inv_delta_t);
85        let k3 = self.rhs(self.v + 0.5 * self.dt * k2, current, inv_delta_t);
86        let k4 = self.rhs(self.v + self.dt * k3, current, inv_delta_t);
87        let next_v = self.v + (self.dt / 6.0) * (k1 + 2.0 * k2 + 2.0 * k3 + k4);
88        if !k1.is_finite()
89            || !k2.is_finite()
90            || !k3.is_finite()
91            || !k4.is_finite()
92            || !next_v.is_finite()
93        {
94            return 0;
95        }
96
97        self.inv_delta_t = inv_delta_t;
98        self.dt_div_tau = self.dt / self.tau;
99        if next_v >= self.v_threshold {
100            self.v = self.v_reset;
101            self.refractory_remaining = self.refractory_period;
102            1
103        } else {
104            self.v = next_v;
105            0
106        }
107    }
108
109    pub fn reset(&mut self) {
110        self.v = self.v_rest;
111        self.refractory_remaining = 0.0;
112    }
113
114    fn rhs(&self, v: f64, current: f64, inv_delta_t: f64) -> f64 {
115        if !v.is_finite() {
116            return f64::NAN;
117        }
118        let bounded_v = v.min(self.v_threshold);
119        let exp_arg = (bounded_v - self.v_rh) * inv_delta_t;
120        let exp_term = self.delta_t * exp_arg.exp();
121        (-(bounded_v - self.v_rest) + exp_term + current) / self.tau
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::ExpIfNeuron;
128
129    fn rk4_reference(neuron: &ExpIfNeuron, current: f64) -> f64 {
130        let rhs = |v: f64| {
131            let bounded_v = v.min(neuron.v_threshold);
132            let exp_arg = (bounded_v - neuron.v_rh) / neuron.delta_t;
133            (-(bounded_v - neuron.v_rest) + neuron.delta_t * exp_arg.exp() + current) / neuron.tau
134        };
135        let k1 = rhs(neuron.v);
136        let k2 = rhs(neuron.v + 0.5 * neuron.dt * k1);
137        let k3 = rhs(neuron.v + 0.5 * neuron.dt * k2);
138        let k4 = rhs(neuron.v + neuron.dt * k3);
139        neuron.v + (neuron.dt / 6.0) * (k1 + 2.0 * k2 + 2.0 * k3 + k4)
140    }
141
142    #[test]
143    fn optimised_step_matches_rk4_reference() {
144        let mut neuron = ExpIfNeuron::new();
145        neuron.v = -60.0;
146        let expected = rk4_reference(&neuron, 10.0);
147        assert_eq!(neuron.step(10.0), 0);
148        assert!((neuron.v - expected).abs() < 1e-12);
149    }
150
151    #[test]
152    fn strong_input_produces_spikes() {
153        let mut neuron = ExpIfNeuron::new();
154        let spikes: i32 = (0..2_000).map(|_| neuron.step(500.0)).sum();
155        assert!(spikes > 0);
156    }
157
158    #[test]
159    fn zero_input_remains_silent() {
160        let mut neuron = ExpIfNeuron::new();
161        let spikes: i32 = (0..500).map(|_| neuron.step(0.0)).sum();
162        assert_eq!(spikes, 0);
163    }
164
165    #[test]
166    fn negative_input_remains_silent() {
167        let mut neuron = ExpIfNeuron::new();
168        let spikes: i32 = (0..500).map(|_| neuron.step(-100.0)).sum();
169        assert_eq!(spikes, 0);
170    }
171
172    #[test]
173    fn configurable_step_matches_rk4_reference() {
174        let mut neuron = ExpIfNeuron::new();
175        neuron.v = -60.0;
176        neuron.dt = 0.25;
177        neuron.tau = 20.0;
178        let expected = rk4_reference(&neuron, 12.0);
179        assert_eq!(neuron.step(12.0), 0);
180        assert!((neuron.v - expected).abs() < 1e-12);
181    }
182
183    #[test]
184    fn reset_matches_fresh_neuron() {
185        let mut neuron = ExpIfNeuron::new();
186        for _ in 0..200 {
187            neuron.step(500.0);
188        }
189        neuron.reset();
190        let mut fresh = ExpIfNeuron::new();
191        let reset_spikes: i32 = (0..100).map(|_| neuron.step(500.0)).sum();
192        let fresh_spikes: i32 = (0..100).map(|_| fresh.step(500.0)).sum();
193        assert_eq!(reset_spikes, fresh_spikes);
194    }
195
196    #[test]
197    fn high_input_keeps_voltage_finite() {
198        let mut neuron = ExpIfNeuron::new();
199        for _ in 0..5_000 {
200            neuron.step(1_000.0);
201        }
202        assert!(neuron.v.is_finite());
203    }
204
205    #[test]
206    fn enrolled_event_counts_are_stable() {
207        for (current, expected) in [(0.0, 0), (5.0, 0), (20.0, 2)] {
208            let mut neuron = ExpIfNeuron::new();
209            let spikes: i32 = (0..1_000).map(|_| neuron.step(current)).sum();
210            assert_eq!(spikes, expected, "current={current}");
211        }
212    }
213
214    #[test]
215    fn refractory_hold_and_invalid_state_fail_closed() {
216        let mut neuron = ExpIfNeuron::new();
217        neuron.refractory_period = 1.7;
218        while neuron.step(50.0) == 0 {}
219        assert_eq!(neuron.refractory_remaining, 1.7);
220        for _ in 0..10 {
221            assert_eq!(neuron.step(50.0), 0);
222            assert_eq!(neuron.v, neuron.v_reset);
223        }
224        assert!((neuron.refractory_remaining - 1.5).abs() < 1.0e-12);
225
226        let voltage = neuron.v;
227        neuron.refractory_remaining = 2.0;
228        assert_eq!(neuron.step(0.0), 0);
229        assert_eq!((neuron.v, neuron.refractory_remaining), (voltage, 2.0));
230    }
231
232    #[test]
233    fn ten_thousand_steps_complete_within_smoke_limit() {
234        let mut neuron = ExpIfNeuron::new();
235        let start = std::time::Instant::now();
236        for _ in 0..10_000 {
237            neuron.step(500.0);
238        }
239        assert!(start.elapsed().as_millis() < 50);
240    }
241}