Skip to main content

sc_neurocore_engine/neuron/
lapicque.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 — Lapicque integrate-and-fire neuron
8
9/// Lapicque 1907 — classical RC integrate-and-fire.
10#[derive(Clone, Debug)]
11pub struct LapicqueNeuron {
12    pub v: f64,
13    pub v_rest: f64,
14    pub v_reset: f64,
15    pub v_threshold: f64,
16    pub tau: f64,
17    pub resistance: f64,
18    pub dt: f64,
19}
20
21impl LapicqueNeuron {
22    pub fn new(tau: f64, resistance: f64, threshold: f64, dt: f64) -> Self {
23        Self {
24            v: 0.0,
25            v_rest: 0.0,
26            v_reset: 0.0,
27            v_threshold: threshold,
28            tau,
29            resistance,
30            dt,
31        }
32    }
33
34    pub fn step(&mut self, current: f64) -> i32 {
35        if !self.v.is_finite()
36            || !self.v_rest.is_finite()
37            || !self.v_reset.is_finite()
38            || !self.v_threshold.is_finite()
39            || self.v_threshold <= self.v_rest
40            || self.v_threshold <= self.v_reset
41            || self.v >= self.v_threshold
42            || !self.tau.is_finite()
43            || self.tau <= 0.0
44            || !self.resistance.is_finite()
45            || self.resistance <= 0.0
46            || !self.dt.is_finite()
47            || self.dt <= 0.0
48            || !current.is_finite()
49        {
50            return 0;
51        }
52
53        let v_inf = self.v_rest + self.resistance * current;
54        let decay = (-self.dt / self.tau).exp();
55        let next_v = v_inf + (self.v - v_inf) * decay;
56        if !v_inf.is_finite() || !decay.is_finite() || !next_v.is_finite() {
57            return 0;
58        }
59        self.v = next_v;
60
61        if self.v >= self.v_threshold {
62            self.v = self.v_reset;
63            1
64        } else {
65            0
66        }
67    }
68
69    pub fn reset(&mut self) {
70        self.v = self.v_rest;
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::LapicqueNeuron;
77
78    fn neuron() -> LapicqueNeuron {
79        LapicqueNeuron::new(20.0, 1.0, 1.0, 1.0)
80    }
81
82    #[test]
83    fn sustained_input_produces_spikes() {
84        let mut neuron = neuron();
85        let spikes: i32 = (0..200).map(|_| neuron.step(5.0)).sum();
86        assert!(spikes > 0);
87    }
88
89    #[test]
90    fn reset_restores_resting_voltage() {
91        let mut neuron = neuron();
92        for _ in 0..50 {
93            neuron.step(5.0);
94        }
95        neuron.reset();
96        assert!(neuron.v.abs() < 1e-12);
97    }
98
99    #[test]
100    fn exact_flow_matches_closed_form() {
101        let mut neuron = LapicqueNeuron::new(20.0, 1.0, 1.0, 5.0);
102        neuron.v = 0.25;
103        let current = 0.5;
104        let v0 = neuron.v;
105        let v_inf = neuron.v_rest + neuron.resistance * current;
106        let euler =
107            v0 + (-(v0 - neuron.v_rest) + neuron.resistance * current) / neuron.tau * neuron.dt;
108        let expected = v_inf + (v0 - v_inf) * (-neuron.dt / neuron.tau).exp();
109        assert_eq!(neuron.step(current), 0);
110        assert!((neuron.v - expected).abs() < 1e-15);
111        assert!((neuron.v - euler).abs() > 1e-4);
112    }
113
114    #[test]
115    fn zero_input_remains_silent() {
116        let mut neuron = neuron();
117        let spikes: i32 = (0..500).map(|_| neuron.step(0.0)).sum();
118        assert_eq!(spikes, 0);
119    }
120
121    #[test]
122    fn negative_input_remains_silent() {
123        let mut neuron = neuron();
124        let spikes: i32 = (0..500).map(|_| neuron.step(-5.0)).sum();
125        assert_eq!(spikes, 0);
126    }
127
128    #[test]
129    fn invalid_state_does_not_mutate() {
130        let mut neuron = neuron();
131        neuron.v = 0.25;
132        neuron.tau = 0.0;
133        assert_eq!(neuron.step(1.0), 0);
134        assert_eq!(neuron.v, 0.25);
135    }
136
137    #[test]
138    fn reset_matches_fresh_neuron() {
139        let mut neuron = neuron();
140        for _ in 0..100 {
141            neuron.step(5.0);
142        }
143        neuron.reset();
144        let mut fresh = self::neuron();
145        let reset_spikes: i32 = (0..100).map(|_| neuron.step(5.0)).sum();
146        let fresh_spikes: i32 = (0..100).map(|_| fresh.step(5.0)).sum();
147        assert_eq!(reset_spikes, fresh_spikes);
148    }
149
150    #[test]
151    fn high_input_keeps_voltage_finite() {
152        let mut neuron = neuron();
153        for _ in 0..5_000 {
154            neuron.step(100.0);
155        }
156        assert!(neuron.v.is_finite());
157    }
158
159    #[test]
160    fn higher_resistance_does_not_reduce_spike_count() {
161        let mut low = LapicqueNeuron::new(20.0, 0.5, 1.0, 1.0);
162        let mut high = LapicqueNeuron::new(20.0, 2.0, 1.0, 1.0);
163        let low_spikes: i32 = (0..200).map(|_| low.step(1.0)).sum();
164        let high_spikes: i32 = (0..200).map(|_| high.step(1.0)).sum();
165        assert!(high_spikes >= low_spikes);
166    }
167
168    #[test]
169    fn ten_thousand_steps_complete_within_smoke_limit() {
170        let mut neuron = neuron();
171        let start = std::time::Instant::now();
172        for _ in 0..10_000 {
173            neuron.step(5.0);
174        }
175        assert!(start.elapsed().as_millis() < 50);
176    }
177
178    #[test]
179    fn sustained_pipeline_input_produces_many_spikes() {
180        let mut neuron = neuron();
181        let spikes: i32 = (0..10_000).map(|_| neuron.step(5.0)).sum();
182        assert!(spikes > 100, "got {spikes}");
183    }
184}