sc_neurocore_engine/neuron/
lapicque.rs1#[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}