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
9mod simulation;
10
11/// Adaptive Exponential IF neuron. Brette & Gerstner 2005.
12/// PyO3 wrapper: `pyo3_neurons::PyAdExNeuron`
13#[derive(Clone, Debug)]
14pub struct AdExNeuron {
15    pub v: f64,
16    pub w: f64,
17    pub v_rest: f64,
18    pub v_reset: f64,
19    pub v_threshold: f64,
20    pub v_rh: f64,
21    pub delta_t: f64,
22    pub tau: f64,
23    pub tau_w: f64,
24    pub a: f64,
25    pub b: f64,
26    pub c_m: f64,
27    pub dt: f64,
28}
29
30impl Default for AdExNeuron {
31    fn default() -> Self {
32        Self::new()
33    }
34}
35
36impl AdExNeuron {
37    pub fn new() -> Self {
38        Self {
39            v: -65.0,
40            w: 0.0,
41            v_rest: -65.0,
42            v_reset: -68.0,
43            v_threshold: -50.0,
44            v_rh: -55.0,
45            delta_t: 2.0,
46            tau: 20.0,
47            tau_w: 100.0,
48            a: 0.5,
49            b: 7.0,
50            c_m: 200.0,
51            dt: 0.1,
52        }
53    }
54
55    /// Advance one maintained baseline-Euler step.
56    ///
57    /// This compatibility surface retains the historical zero-event result on
58    /// invalid input. New batch and binding code uses [`Self::try_step`] so a
59    /// rejected update cannot be mistaken for a valid quiet timestep.
60    pub fn step(&mut self, current: f64) -> i32 {
61        self.try_step(current).unwrap_or(0)
62    }
63
64    /// Advance one checked baseline-Euler step without partial mutation.
65    pub fn try_step(&mut self, current: f64) -> Result<i32, &'static str> {
66        if !self.v.is_finite()
67            || !self.w.is_finite()
68            || !self.v_rest.is_finite()
69            || !self.v_reset.is_finite()
70            || !self.v_threshold.is_finite()
71            || !self.v_rh.is_finite()
72            || !self.delta_t.is_finite()
73            || !self.tau.is_finite()
74            || !self.tau_w.is_finite()
75            || !self.a.is_finite()
76            || !self.b.is_finite()
77            || !self.c_m.is_finite()
78            || !self.dt.is_finite()
79            || !current.is_finite()
80            || self.delta_t <= 0.0
81            || self.tau <= 0.0
82            || self.tau_w <= 0.0
83            || self.c_m <= 0.0
84            || self.dt <= 0.0
85        {
86            return Err("invalid AdEx state, parameters, timestep, or input");
87        }
88
89        let exp_arg = ((self.v - self.v_rh) / self.delta_t).clamp(-20.0, 20.0);
90        let exp_term = self.delta_t * exp_arg.exp();
91        let dv = ((-(self.v - self.v_rest) + exp_term) / self.tau + (-self.w + current) / self.c_m)
92            * self.dt;
93        let dw = (self.a * (self.v - self.v_rest) - self.w) / self.tau_w * self.dt;
94        let next_v = self.v + dv;
95        let next_w = self.w + dw;
96        if !exp_term.is_finite()
97            || !dv.is_finite()
98            || !dw.is_finite()
99            || !next_v.is_finite()
100            || !next_w.is_finite()
101        {
102            return Err("non-finite AdEx integrator candidate");
103        }
104
105        if next_v >= self.v_threshold {
106            let spike_w = next_w + self.b;
107            if !spike_w.is_finite() {
108                return Err("non-finite AdEx spike-adaptation candidate");
109            }
110            self.v = self.v_reset;
111            self.w = spike_w;
112            Ok(1)
113        } else {
114            self.v = next_v;
115            self.w = next_w;
116            Ok(0)
117        }
118    }
119
120    pub fn reset(&mut self) {
121        self.v = self.v_rest;
122        self.w = 0.0;
123    }
124}
125
126#[cfg(test)]
127mod tests;