Skip to main content

sc_neurocore_engine/neurons/hardware/
neurogrid.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 — NeuroGrid Neuron Emulator
8
9/// NeuroGrid — Boahen 2014 subthreshold analog 2-compartment.
10#[derive(Clone, Debug)]
11pub struct NeuroGridNeuron {
12    pub v_s: f64,
13    pub v_d: f64,
14    pub tau_s: f64,
15    pub tau_d: f64,
16    pub g_c: f64,
17    pub delta_t: f64,
18    pub v_rest: f64,
19    pub v_threshold: f64,
20    pub v_peak: f64,
21    pub v_reset: f64,
22    pub dt: f64,
23}
24
25impl NeuroGridNeuron {
26    pub fn new() -> Self {
27        Self {
28            v_s: -65.0,
29            v_d: -65.0,
30            tau_s: 20.0,
31            tau_d: 50.0,
32            g_c: 0.5,
33            delta_t: 2.0,
34            v_rest: -65.0,
35            v_threshold: -50.0,
36            v_peak: 20.0,
37            v_reset: -65.0,
38            dt: 0.1,
39        }
40    }
41    fn valid(&self) -> bool {
42        self.v_s.is_finite()
43            && self.v_d.is_finite()
44            && self.tau_s.is_finite()
45            && self.tau_s > 0.0
46            && self.tau_d.is_finite()
47            && self.tau_d > 0.0
48            && self.g_c.is_finite()
49            && self.g_c >= 0.0
50            && self.delta_t.is_finite()
51            && self.delta_t > 0.0
52            && self.v_rest.is_finite()
53            && self.v_threshold.is_finite()
54            && self.v_peak.is_finite()
55            && self.v_reset.is_finite()
56            && self.dt.is_finite()
57            && self.dt > 0.0
58    }
59
60    fn derivatives(&self, v_s: f64, v_d: f64, current: f64) -> (f64, f64) {
61        let v_s_eff = v_s.min(self.v_peak);
62        let dv_d = (-(v_d - self.v_rest) + current - self.g_c * (v_d - v_s_eff)) / self.tau_d;
63        let exp_arg = ((v_s_eff - self.v_threshold) / self.delta_t).min(20.0);
64        let exp_term = self.delta_t * exp_arg.exp();
65        let dv_s = (-(v_s_eff - self.v_rest) + exp_term + self.g_c * (v_d - v_s_eff)) / self.tau_s;
66        (dv_s, dv_d)
67    }
68
69    fn rk4_substep(&self, v_s: f64, v_d: f64, current: f64) -> (f64, f64) {
70        let dt = self.dt;
71        let (k1s, k1d) = self.derivatives(v_s, v_d, current);
72        let (k2s, k2d) = self.derivatives(v_s + 0.5 * dt * k1s, v_d + 0.5 * dt * k1d, current);
73        let (k3s, k3d) = self.derivatives(v_s + 0.5 * dt * k2s, v_d + 0.5 * dt * k2d, current);
74        let (k4s, k4d) = self.derivatives(v_s + dt * k3s, v_d + dt * k3d, current);
75        (
76            v_s + dt * (k1s + 2.0 * k2s + 2.0 * k3s + k4s) / 6.0,
77            v_d + dt * (k1d + 2.0 * k2d + 2.0 * k3d + k4d) / 6.0,
78        )
79    }
80
81    pub fn step(&mut self, current: f64) -> i32 {
82        if !current.is_finite() || !self.valid() {
83            return 0;
84        }
85        let (next_v_s, next_v_d) = self.rk4_substep(self.v_s, self.v_d, current);
86        if !next_v_s.is_finite() || !next_v_d.is_finite() {
87            return 0;
88        }
89        self.v_d = next_v_d;
90        if next_v_s >= self.v_peak {
91            self.v_s = self.v_reset;
92            1
93        } else {
94            self.v_s = next_v_s;
95            0
96        }
97    }
98    pub fn reset(&mut self) {
99        self.v_s = -65.0;
100        self.v_d = -65.0;
101    }
102}
103impl Default for NeuroGridNeuron {
104    fn default() -> Self {
105        Self::new()
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn neurogrid_fires() {
115        let mut n = NeuroGridNeuron::new();
116        let t: i32 = (0..2000).map(|_| n.step(500.0)).sum();
117        assert!(t > 0);
118    }
119    #[test]
120    fn neurogrid_silent() {
121        let mut n = NeuroGridNeuron::new();
122        let t: i32 = (0..200).map(|_| n.step(0.0)).sum();
123        assert_eq!(t, 0);
124    }
125    #[test]
126    fn neurogrid_reset() {
127        let mut n = NeuroGridNeuron::new();
128        for _ in 0..100 {
129            n.step(500.0);
130        }
131        n.reset();
132        assert!((n.v_s - (-65.0)).abs() < 1e-10);
133    }
134    #[test]
135    fn neurogrid_bounded() {
136        let mut n = NeuroGridNeuron::new();
137        for _ in 0..2000 {
138            n.step(1e4);
139        }
140        assert!(n.v_s.is_finite());
141    }
142    #[test]
143    fn neurogrid_nan_no_panic() {
144        NeuroGridNeuron::new().step(f64::NAN);
145    }
146    #[test]
147    fn neurogrid_rk4_anchor() {
148        let mut n = NeuroGridNeuron::new();
149        let spikes: i32 = (0..20_000).map(|_| n.step(100.0)).sum();
150        assert_eq!(spikes, 94);
151        assert!(n.v_s.is_finite());
152        assert!(n.v_d.is_finite());
153    }
154    #[test]
155    fn neurogrid_invalid_input_preserves_state() {
156        let mut n = NeuroGridNeuron::new();
157        for _ in 0..10 {
158            n.step(100.0);
159        }
160        let old = (n.v_s, n.v_d);
161        assert_eq!(n.step(f64::INFINITY), 0);
162        assert_eq!((n.v_s, n.v_d), old);
163    }
164}