Skip to main content

sc_neurocore_engine/neurons/trivial/
complementary_lif.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 — Complementary LIF Neuron
8
9/// Complementary LIF — dual-path excitatory/inhibitory.
10#[derive(Clone, Debug)]
11pub struct ComplementaryLIFNeuron {
12    pub v_pos: f64,
13    pub v_neg: f64,
14    pub alpha: f64,
15    pub v_threshold: f64,
16}
17
18impl ComplementaryLIFNeuron {
19    pub fn new(tau: f64, dt: f64, v_threshold: f64) -> Self {
20        Self {
21            v_pos: 0.0,
22            v_neg: 0.0,
23            alpha: (-dt / tau).exp(),
24            v_threshold,
25        }
26    }
27
28    pub fn step(&mut self, current: f64) -> i32 {
29        let inp_pos = current.max(0.0);
30        let inp_neg = (-current).max(0.0);
31        self.v_pos = self.alpha * self.v_pos + inp_pos;
32        self.v_neg = self.alpha * self.v_neg + inp_neg;
33        let diff = self.v_pos - self.v_neg;
34        if diff >= self.v_threshold {
35            self.v_pos = 0.0;
36            self.v_neg = 0.0;
37            1
38        } else if diff <= -self.v_threshold {
39            self.v_pos = 0.0;
40            self.v_neg = 0.0;
41            -1
42        } else {
43            0
44        }
45    }
46
47    pub fn reset(&mut self) {
48        self.v_pos = 0.0;
49        self.v_neg = 0.0;
50    }
51}
52
53impl Default for ComplementaryLIFNeuron {
54    fn default() -> Self {
55        Self::new(10.0, 1.0, 1.0)
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn clif_positive_spike() {
65        let mut n = ComplementaryLIFNeuron::default();
66        let total: i32 = (0..20).map(|_| n.step(0.5)).sum();
67        assert!(total > 0);
68    }
69    #[test]
70    fn clif_silent_without_input() {
71        let mut n = ComplementaryLIFNeuron::default();
72        let t: i32 = (0..100).map(|_| n.step(0.0)).sum();
73        assert_eq!(t, 0);
74    }
75    #[test]
76    fn clif_reset_clears_state() {
77        let mut n = ComplementaryLIFNeuron::default();
78        for _ in 0..20 {
79            n.step(0.5);
80        }
81        n.reset();
82        assert!((n.v_pos - 0.0).abs() < 1e-10);
83        assert!((n.v_neg - 0.0).abs() < 1e-10);
84    }
85    #[test]
86    fn clif_bounded() {
87        let mut n = ComplementaryLIFNeuron::default();
88        for _ in 0..1000 {
89            n.step(100.0);
90        }
91        assert!(n.v_pos.is_finite());
92    }
93    #[test]
94    fn clif_nan_no_panic() {
95        ComplementaryLIFNeuron::default().step(f64::NAN);
96    }
97}