sc_neurocore_engine/neuron/
exp_if.rs1#[derive(Clone, Debug)]
11pub struct ExpIfNeuron {
12 pub v: f64,
13 pub v_rest: f64,
14 pub v_reset: f64,
15 pub v_threshold: f64,
16 pub v_rh: f64,
17 pub delta_t: f64,
18 pub tau: f64,
19 pub dt: f64,
20 pub refractory_period: f64,
21 pub refractory_remaining: f64,
22 pub inv_delta_t: f64,
23 pub dt_div_tau: f64,
24}
25
26impl Default for ExpIfNeuron {
27 fn default() -> Self {
28 Self::new()
29 }
30}
31
32impl ExpIfNeuron {
33 pub fn new() -> Self {
34 Self {
35 v: -65.0,
36 v_rest: -65.0,
37 v_reset: -68.0,
38 v_threshold: 30.0,
39 v_rh: -59.9,
40 delta_t: 3.48,
41 tau: 10.0,
42 dt: 0.02,
43 refractory_period: 0.0,
44 refractory_remaining: 0.0,
45 inv_delta_t: 1.0 / 3.48,
46 dt_div_tau: 0.02 / 10.0,
47 }
48 }
49
50 pub fn step(&mut self, current: f64) -> i32 {
51 if !self.v.is_finite()
52 || !current.is_finite()
53 || !self.v_rest.is_finite()
54 || !self.v_reset.is_finite()
55 || !self.v_threshold.is_finite()
56 || !self.v_rh.is_finite()
57 || !self.delta_t.is_finite()
58 || !self.tau.is_finite()
59 || !self.dt.is_finite()
60 || !self.refractory_period.is_finite()
61 || !self.refractory_remaining.is_finite()
62 || self.delta_t <= 0.0
63 || self.tau <= 0.0
64 || self.dt <= 0.0
65 || self.refractory_period < 0.0
66 || self.refractory_remaining < 0.0
67 || self.refractory_remaining > self.refractory_period
68 || self.v_threshold <= self.v_rh
69 || self.v >= self.v_threshold
70 || self.v_rest >= self.v_threshold
71 || self.v_reset >= self.v_threshold
72 {
73 return 0;
74 }
75
76 if self.refractory_remaining > 0.0 {
77 self.refractory_remaining = (self.refractory_remaining - self.dt).max(0.0);
78 self.v = self.v_reset;
79 return 0;
80 }
81
82 let inv_delta_t = 1.0 / self.delta_t;
83 let k1 = self.rhs(self.v, current, inv_delta_t);
84 let k2 = self.rhs(self.v + 0.5 * self.dt * k1, current, inv_delta_t);
85 let k3 = self.rhs(self.v + 0.5 * self.dt * k2, current, inv_delta_t);
86 let k4 = self.rhs(self.v + self.dt * k3, current, inv_delta_t);
87 let next_v = self.v + (self.dt / 6.0) * (k1 + 2.0 * k2 + 2.0 * k3 + k4);
88 if !k1.is_finite()
89 || !k2.is_finite()
90 || !k3.is_finite()
91 || !k4.is_finite()
92 || !next_v.is_finite()
93 {
94 return 0;
95 }
96
97 self.inv_delta_t = inv_delta_t;
98 self.dt_div_tau = self.dt / self.tau;
99 if next_v >= self.v_threshold {
100 self.v = self.v_reset;
101 self.refractory_remaining = self.refractory_period;
102 1
103 } else {
104 self.v = next_v;
105 0
106 }
107 }
108
109 pub fn reset(&mut self) {
110 self.v = self.v_rest;
111 self.refractory_remaining = 0.0;
112 }
113
114 fn rhs(&self, v: f64, current: f64, inv_delta_t: f64) -> f64 {
115 if !v.is_finite() {
116 return f64::NAN;
117 }
118 let bounded_v = v.min(self.v_threshold);
119 let exp_arg = (bounded_v - self.v_rh) * inv_delta_t;
120 let exp_term = self.delta_t * exp_arg.exp();
121 (-(bounded_v - self.v_rest) + exp_term + current) / self.tau
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::ExpIfNeuron;
128
129 fn rk4_reference(neuron: &ExpIfNeuron, current: f64) -> f64 {
130 let rhs = |v: f64| {
131 let bounded_v = v.min(neuron.v_threshold);
132 let exp_arg = (bounded_v - neuron.v_rh) / neuron.delta_t;
133 (-(bounded_v - neuron.v_rest) + neuron.delta_t * exp_arg.exp() + current) / neuron.tau
134 };
135 let k1 = rhs(neuron.v);
136 let k2 = rhs(neuron.v + 0.5 * neuron.dt * k1);
137 let k3 = rhs(neuron.v + 0.5 * neuron.dt * k2);
138 let k4 = rhs(neuron.v + neuron.dt * k3);
139 neuron.v + (neuron.dt / 6.0) * (k1 + 2.0 * k2 + 2.0 * k3 + k4)
140 }
141
142 #[test]
143 fn optimised_step_matches_rk4_reference() {
144 let mut neuron = ExpIfNeuron::new();
145 neuron.v = -60.0;
146 let expected = rk4_reference(&neuron, 10.0);
147 assert_eq!(neuron.step(10.0), 0);
148 assert!((neuron.v - expected).abs() < 1e-12);
149 }
150
151 #[test]
152 fn strong_input_produces_spikes() {
153 let mut neuron = ExpIfNeuron::new();
154 let spikes: i32 = (0..2_000).map(|_| neuron.step(500.0)).sum();
155 assert!(spikes > 0);
156 }
157
158 #[test]
159 fn zero_input_remains_silent() {
160 let mut neuron = ExpIfNeuron::new();
161 let spikes: i32 = (0..500).map(|_| neuron.step(0.0)).sum();
162 assert_eq!(spikes, 0);
163 }
164
165 #[test]
166 fn negative_input_remains_silent() {
167 let mut neuron = ExpIfNeuron::new();
168 let spikes: i32 = (0..500).map(|_| neuron.step(-100.0)).sum();
169 assert_eq!(spikes, 0);
170 }
171
172 #[test]
173 fn configurable_step_matches_rk4_reference() {
174 let mut neuron = ExpIfNeuron::new();
175 neuron.v = -60.0;
176 neuron.dt = 0.25;
177 neuron.tau = 20.0;
178 let expected = rk4_reference(&neuron, 12.0);
179 assert_eq!(neuron.step(12.0), 0);
180 assert!((neuron.v - expected).abs() < 1e-12);
181 }
182
183 #[test]
184 fn reset_matches_fresh_neuron() {
185 let mut neuron = ExpIfNeuron::new();
186 for _ in 0..200 {
187 neuron.step(500.0);
188 }
189 neuron.reset();
190 let mut fresh = ExpIfNeuron::new();
191 let reset_spikes: i32 = (0..100).map(|_| neuron.step(500.0)).sum();
192 let fresh_spikes: i32 = (0..100).map(|_| fresh.step(500.0)).sum();
193 assert_eq!(reset_spikes, fresh_spikes);
194 }
195
196 #[test]
197 fn high_input_keeps_voltage_finite() {
198 let mut neuron = ExpIfNeuron::new();
199 for _ in 0..5_000 {
200 neuron.step(1_000.0);
201 }
202 assert!(neuron.v.is_finite());
203 }
204
205 #[test]
206 fn enrolled_event_counts_are_stable() {
207 for (current, expected) in [(0.0, 0), (5.0, 0), (20.0, 2)] {
208 let mut neuron = ExpIfNeuron::new();
209 let spikes: i32 = (0..1_000).map(|_| neuron.step(current)).sum();
210 assert_eq!(spikes, expected, "current={current}");
211 }
212 }
213
214 #[test]
215 fn refractory_hold_and_invalid_state_fail_closed() {
216 let mut neuron = ExpIfNeuron::new();
217 neuron.refractory_period = 1.7;
218 while neuron.step(50.0) == 0 {}
219 assert_eq!(neuron.refractory_remaining, 1.7);
220 for _ in 0..10 {
221 assert_eq!(neuron.step(50.0), 0);
222 assert_eq!(neuron.v, neuron.v_reset);
223 }
224 assert!((neuron.refractory_remaining - 1.5).abs() < 1.0e-12);
225
226 let voltage = neuron.v;
227 neuron.refractory_remaining = 2.0;
228 assert_eq!(neuron.step(0.0), 0);
229 assert_eq!((neuron.v, neuron.refractory_remaining), (voltage, 2.0));
230 }
231
232 #[test]
233 fn ten_thousand_steps_complete_within_smoke_limit() {
234 let mut neuron = ExpIfNeuron::new();
235 let start = std::time::Instant::now();
236 for _ in 0..10_000 {
237 neuron.step(500.0);
238 }
239 assert!(start.elapsed().as_millis() < 50);
240 }
241}