Skip to main content

sc_neurocore_engine/neuron/
adex.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 — Adaptive exponential integrate-and-fire neuron
8
9/// Adaptive Exponential IF neuron. Brette & Gerstner 2005.
10/// PyO3 wrapper: `pyo3_neurons::PyAdExNeuron`
11#[derive(Clone, Debug)]
12pub struct AdExNeuron {
13    pub v: f64,
14    pub w: f64,
15    pub v_rest: f64,
16    pub v_reset: f64,
17    pub v_threshold: f64,
18    pub v_rh: f64,
19    pub delta_t: f64,
20    pub tau: f64,
21    pub tau_w: f64,
22    pub a: f64,
23    pub b: f64,
24    pub c_m: f64,
25    pub dt: f64,
26}
27
28impl Default for AdExNeuron {
29    fn default() -> Self {
30        Self::new()
31    }
32}
33
34impl AdExNeuron {
35    pub fn new() -> Self {
36        Self {
37            v: -65.0,
38            w: 0.0,
39            v_rest: -65.0,
40            v_reset: -68.0,
41            v_threshold: -50.0,
42            v_rh: -55.0,
43            delta_t: 2.0,
44            tau: 20.0,
45            tau_w: 100.0,
46            a: 0.5,
47            b: 7.0,
48            c_m: 200.0,
49            dt: 0.1,
50        }
51    }
52
53    pub fn step(&mut self, current: f64) -> i32 {
54        if !self.v.is_finite()
55            || !self.w.is_finite()
56            || !self.v_rest.is_finite()
57            || !self.v_reset.is_finite()
58            || !self.v_threshold.is_finite()
59            || !self.v_rh.is_finite()
60            || !self.delta_t.is_finite()
61            || !self.tau.is_finite()
62            || !self.tau_w.is_finite()
63            || !self.a.is_finite()
64            || !self.b.is_finite()
65            || !self.c_m.is_finite()
66            || !self.dt.is_finite()
67            || !current.is_finite()
68            || self.delta_t <= 0.0
69            || self.tau <= 0.0
70            || self.tau_w <= 0.0
71            || self.c_m <= 0.0
72            || self.dt <= 0.0
73        {
74            return 0;
75        }
76
77        let exp_arg = ((self.v - self.v_rh) / self.delta_t).clamp(-20.0, 20.0);
78        let exp_term = self.delta_t * exp_arg.exp();
79        let dv = ((-(self.v - self.v_rest) + exp_term) / self.tau + (-self.w + current) / self.c_m)
80            * self.dt;
81        let dw = (self.a * (self.v - self.v_rest) - self.w) / self.tau_w * self.dt;
82        let next_v = self.v + dv;
83        let next_w = self.w + dw;
84        if !exp_term.is_finite()
85            || !dv.is_finite()
86            || !dw.is_finite()
87            || !next_v.is_finite()
88            || !next_w.is_finite()
89        {
90            return 0;
91        }
92
93        if next_v >= self.v_threshold {
94            let spike_w = next_w + self.b;
95            if !spike_w.is_finite() {
96                return 0;
97            }
98            self.v = self.v_reset;
99            self.w = spike_w;
100            1
101        } else {
102            self.v = next_v;
103            self.w = next_w;
104            0
105        }
106    }
107
108    pub fn reset(&mut self) {
109        self.v = self.v_rest;
110        self.w = 0.0;
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::AdExNeuron;
117
118    #[test]
119    fn strong_input_produces_spikes() {
120        let mut neuron = AdExNeuron::new();
121        let spikes: i32 = (0..2_000).map(|_| neuron.step(500.0)).sum();
122        assert!(spikes > 0, "AdEx must fire with strong input");
123    }
124
125    #[test]
126    fn adaptation_does_not_increase_late_rate() {
127        let mut neuron = AdExNeuron::new();
128        let first: i32 = (0..1_000).map(|_| neuron.step(400.0)).sum();
129        let second: i32 = (0..1_000).map(|_| neuron.step(400.0)).sum();
130        assert!(second <= first + 5, "first={first}, second={second}");
131    }
132
133    #[test]
134    fn matches_python_golden_spike_counts() {
135        for (current, expected) in [(0.0, 0), (200.0, 4), (500.0, 12)] {
136            let mut neuron = AdExNeuron::new();
137            let spikes: i32 = (0..1_000).map(|_| neuron.step(current)).sum();
138            assert_eq!(spikes, expected, "current={current}");
139        }
140    }
141
142    #[test]
143    fn invalid_input_is_mutation_free() {
144        let mut neuron = AdExNeuron::new();
145        let before = (neuron.v, neuron.w);
146        assert_eq!(neuron.step(f64::INFINITY), 0);
147        assert_eq!((neuron.v, neuron.w), before);
148    }
149
150    #[test]
151    fn nonfinite_candidate_is_mutation_free() {
152        let mut neuron = AdExNeuron::new();
153        neuron.dt = 1.0e308;
154        let before = (neuron.v, neuron.w);
155        assert_eq!(neuron.step(1.0e308), 0);
156        assert_eq!((neuron.v, neuron.w), before);
157    }
158
159    #[test]
160    fn no_input_remains_silent() {
161        let mut neuron = AdExNeuron::new();
162        let spikes: i32 = (0..1_000).map(|_| neuron.step(0.0)).sum();
163        assert_eq!(spikes, 0);
164    }
165
166    #[test]
167    fn negative_current_remains_silent() {
168        let mut neuron = AdExNeuron::new();
169        let spikes: i32 = (0..500).map(|_| neuron.step(-100.0)).sum();
170        assert_eq!(spikes, 0);
171    }
172
173    #[test]
174    fn reset_matches_fresh_neuron() {
175        let mut neuron = AdExNeuron::new();
176        for _ in 0..200 {
177            neuron.step(500.0);
178        }
179        assert!(neuron.w > 0.0);
180        neuron.reset();
181        assert_eq!(neuron.v, neuron.v_rest);
182        assert_eq!(neuron.w, 0.0);
183
184        let mut fresh = AdExNeuron::new();
185        let reset_spikes: i32 = (0..100).map(|_| neuron.step(500.0)).sum();
186        let fresh_spikes: i32 = (0..100).map(|_| fresh.step(500.0)).sum();
187        assert_eq!(reset_spikes, fresh_spikes);
188    }
189
190    #[test]
191    fn sustained_high_input_keeps_state_finite() {
192        let mut neuron = AdExNeuron::new();
193        for _ in 0..5_000 {
194            neuron.step(1_000.0);
195        }
196        assert!(neuron.v.is_finite());
197        assert!(neuron.w.is_finite());
198    }
199
200    #[test]
201    fn sustained_input_produces_many_spikes() {
202        let mut neuron = AdExNeuron::new();
203        let spikes: i32 = (0..10_000).map(|_| neuron.step(500.0)).sum();
204        assert!(spikes > 100, "got {spikes}");
205        assert!(neuron.v.is_finite());
206    }
207
208    #[test]
209    fn ten_thousand_steps_complete_within_smoke_limit() {
210        let mut neuron = AdExNeuron::new();
211        let start = std::time::Instant::now();
212        for _ in 0..10_000 {
213            neuron.step(500.0);
214        }
215        assert!(start.elapsed().as_millis() < 50);
216    }
217}