Skip to main content

sc_neurocore_engine/neuron/
izhikevich.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 — Floating-point Izhikevich neuron
8
9/// Izhikevich neuron (floating-point).
10///
11/// Standard model from IEEE TNN 14(6), 2003:
12///   v' = 0.04*v² + 5*v + 140 - u + I
13///   u' = a*(b*v - u)
14///   if v >= 30: v ← c, u ← u + d
15#[derive(Clone, Debug)]
16pub struct Izhikevich {
17    pub v: f64,
18    pub u: f64,
19    pub a: f64,
20    pub b: f64,
21    pub c: f64,
22    pub d: f64,
23    pub dt: f64,
24}
25
26impl Izhikevich {
27    /// Regular spiking defaults: a=0.02, b=0.2, c=-65, d=8, dt=1.0.
28    pub fn new(a: f64, b: f64, c: f64, d: f64, dt: f64) -> Self {
29        Self {
30            v: c,
31            u: b * c,
32            a,
33            b,
34            c,
35            d,
36            dt,
37        }
38    }
39
40    /// Regular spiking preset.
41    pub fn regular_spiking() -> Self {
42        Self::new(0.02, 0.2, -65.0, 8.0, 1.0)
43    }
44
45    /// Advance one step. Returns 1 on spike, 0 otherwise.
46    pub fn step(&mut self, current: f64) -> i32 {
47        let half = self.dt * 0.5;
48        for _ in 0..2 {
49            let dv = (0.04 * self.v * self.v + 5.0 * self.v + 140.0 - self.u + current) * half;
50            let du = (self.a * (self.b * self.v - self.u)) * half;
51            self.v += dv;
52            self.u += du;
53        }
54
55        if self.v >= 30.0 {
56            self.v = self.c;
57            self.u += self.d;
58            1
59        } else {
60            0
61        }
62    }
63
64    /// Reset to initial state.
65    pub fn reset(&mut self) {
66        self.v = self.c;
67        self.u = self.b * self.c;
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::Izhikevich;
74
75    #[test]
76    fn regular_spiking_preset_fires_with_current() {
77        let mut neuron = Izhikevich::regular_spiking();
78        let spikes: i32 = (0..100).map(|_| neuron.step(10.0)).sum();
79        assert!(spikes > 0, "RS neuron must fire with I=10");
80    }
81
82    #[test]
83    fn regular_spiking_preset_is_silent_without_input() {
84        let mut neuron = Izhikevich::regular_spiking();
85        let spikes: i32 = (0..100).map(|_| neuron.step(0.0)).sum();
86        assert_eq!(spikes, 0, "no spikes without input");
87    }
88
89    #[test]
90    fn reset_restores_initial_state() {
91        let mut neuron = Izhikevich::regular_spiking();
92        for _ in 0..50 {
93            neuron.step(10.0);
94        }
95        neuron.reset();
96        assert_eq!(neuron.v, neuron.c);
97        assert!((neuron.u - neuron.b * neuron.c).abs() < 1e-12);
98    }
99
100    #[test]
101    fn chattering_preset_fires_more_than_regular_spiking() {
102        let mut chattering = Izhikevich::new(0.02, 0.2, -50.0, 2.0, 1.0);
103        let mut regular_spiking = Izhikevich::regular_spiking();
104        let mut chattering_spikes = 0;
105        let mut regular_spikes = 0;
106        for _ in 0..200 {
107            chattering_spikes += chattering.step(10.0);
108            regular_spikes += regular_spiking.step(10.0);
109        }
110        assert!(
111            chattering_spikes > regular_spikes,
112            "chattering ({chattering_spikes}) should fire more than RS ({regular_spikes})"
113        );
114    }
115}