sc_neurocore_engine/neurons/cerebellar/
lugaro.rs1#[derive(Clone, Debug)]
25pub struct LugaroCell {
26 pub v: f64,
27 pub adapt: f64, pub v_rest: f64,
29 pub v_reset: f64,
30 pub v_threshold: f64,
31 pub tau_m: f64,
32 pub tau_adapt: f64,
33 pub a_adapt: f64, pub gain: f64,
35 pub serotonin: f64, pub dt: f64,
37}
38
39impl Default for LugaroCell {
40 fn default() -> Self {
41 Self::new()
42 }
43}
44
45impl LugaroCell {
46 pub fn new() -> Self {
47 Self {
48 v: -55.0,
49 adapt: 0.0,
50 v_rest: -55.0, v_reset: -65.0,
52 v_threshold: -48.0,
53 tau_m: 10.0,
54 tau_adapt: 150.0,
55 a_adapt: 0.05,
56 gain: 2.0,
57 serotonin: 0.0, dt: 0.5,
59 }
60 }
61
62 pub fn with_serotonin(serotonin_level: f64) -> Self {
64 let mut n = Self::new();
65 n.serotonin = serotonin_level.clamp(0.0, 1.0);
66 n
67 }
68
69 fn is_valid(&self) -> bool {
70 [
71 self.v,
72 self.adapt,
73 self.v_rest,
74 self.v_reset,
75 self.v_threshold,
76 self.tau_m,
77 self.tau_adapt,
78 self.a_adapt,
79 self.gain,
80 self.serotonin,
81 self.dt,
82 ]
83 .iter()
84 .all(|value| value.is_finite())
85 && self.tau_m > 0.0
86 && self.tau_adapt > 0.0
87 && self.dt > 0.0
88 && self.a_adapt >= 0.0
89 && self.gain >= 0.0
90 && (-100.0..=60.0).contains(&self.v)
91 && (0.0..=1.0).contains(&self.serotonin)
92 && self.adapt >= 0.0
93 && self.v_threshold > self.v_reset
94 && self.v_threshold > self.v_rest
95 }
96
97 pub fn step(&mut self, current: f64) -> i32 {
98 if !self.is_valid() || !current.is_finite() {
99 return 0;
100 }
101
102 let effective_gain = self.gain * (1.0 + 0.5 * self.serotonin);
104 let input = effective_gain * current;
105
106 let v_inf = self.v_rest + input - self.adapt;
108 let v_next = v_inf + (self.v - v_inf) * (-self.dt / self.tau_m).exp();
109
110 let adapt_inf = (self.a_adapt * (v_next - self.v_rest).max(0.0)).max(0.0);
112 let adapt_next =
113 (adapt_inf + (self.adapt - adapt_inf) * (-self.dt / self.tau_adapt).exp()).max(0.0);
114 if !v_next.is_finite() || !adapt_next.is_finite() {
115 return 0;
116 }
117
118 if v_next >= self.v_threshold {
120 self.v = self.v_reset;
121 self.adapt = adapt_next + 1.0; return 1;
123 }
124
125 self.v = v_next.clamp(-100.0, 60.0);
127 self.adapt = adapt_next;
128
129 0
130 }
131
132 pub fn reset(&mut self) {
133 *self = Self::new();
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
144 fn lugaro_fires_with_input() {
145 let mut n = LugaroCell::new();
146 let mut spikes = 0;
147 for _ in 0..10_000 {
148 spikes += n.step(5.0);
149 }
150 assert!(
151 spikes > 10,
152 "Lugaro must fire with excitatory input, got {spikes}"
153 );
154 }
155
156 #[test]
157 fn lugaro_low_threshold() {
158 let mut n = LugaroCell::new();
160 let mut spikes = 0;
161 for _ in 0..10_000 {
162 spikes += n.step(4.0);
163 }
164 assert!(
165 spikes > 10,
166 "Lugaro should fire easily with moderate input, got {spikes}"
167 );
168 }
169
170 #[test]
171 fn lugaro_adaptation() {
172 let mut n = LugaroCell::new();
173 let input = 10.0;
174 let mut spikes_early = 0;
175 for _ in 0..2000 {
176 spikes_early += n.step(input);
177 }
178 let mut spikes_late = 0;
179 for _ in 0..2000 {
180 spikes_late += n.step(input);
181 }
182 assert!(
183 spikes_early >= spikes_late,
184 "Adaptation should slow firing: early={spikes_early}, late={spikes_late}"
185 );
186 }
187
188 #[test]
189 fn lugaro_serotonin_increases_firing() {
190 let mut no_5ht = LugaroCell::new();
191 let mut with_5ht = LugaroCell::with_serotonin(1.0);
192
193 let input = 3.0;
194 let mut spikes_no = 0;
195 let mut spikes_5ht = 0;
196 for _ in 0..10_000 {
197 spikes_no += no_5ht.step(input);
198 spikes_5ht += with_5ht.step(input);
199 }
200 assert!(
201 spikes_5ht >= spikes_no,
202 "5-HT must increase firing: 5HT={spikes_5ht} vs none={spikes_no}"
203 );
204 }
205
206 #[test]
207 fn lugaro_negative_input_no_crash() {
208 let mut n = LugaroCell::new();
209 for _ in 0..10_000 {
210 n.step(-100.0);
211 }
212 assert!(n.v.is_finite());
213 assert!(n.v >= -100.0);
214 }
215
216 #[test]
217 fn lugaro_nan_input_stays_finite() {
218 let mut n = LugaroCell::new();
219 let before = n.clone();
220 n.step(f64::NAN);
221 assert!(n.v.is_finite());
222 assert_eq!(n.v, before.v);
223 assert_eq!(n.adapt, before.adapt);
224 }
225
226 #[test]
227 fn lugaro_corrupted_state_preserved_on_step() {
228 let mut n = LugaroCell::new();
229 n.adapt = f64::NAN;
230 let before = n.clone();
231 assert_eq!(n.step(5.0), 0);
232 assert_eq!(n.v, before.v);
233 assert!(n.adapt.is_nan());
234 }
235
236 #[test]
237 fn lugaro_invalid_voltage_preserved_on_step() {
238 let mut n = LugaroCell::new();
239 n.v = 60.1;
240 let before = n.clone();
241 assert_eq!(n.step(5.0), 0);
242 assert_eq!(n.v, before.v);
243 assert_eq!(n.adapt, before.adapt);
244 }
245
246 #[test]
247 fn lugaro_closed_form_membrane_and_adaptation_relaxation() {
248 let mut n = LugaroCell::new();
249 n.v = -56.0;
250 n.adapt = 0.2;
251 n.gain = 0.0;
252
253 let v_inf = n.v_rest - n.adapt;
254 let expected_v = exact_relax_lugaro(n.v, v_inf, n.tau_m, n.dt);
255 let adapt_inf = (n.a_adapt * (expected_v - n.v_rest).max(0.0)).max(0.0);
256 let expected_adapt = exact_relax_lugaro(n.adapt, adapt_inf, n.tau_adapt, n.dt).max(0.0);
257
258 assert_eq!(n.step(0.0), 0);
259 assert_close_lugaro(n.v, expected_v, 1e-12);
260 assert_close_lugaro(n.adapt, expected_adapt, 1e-12);
261 }
262
263 fn exact_relax_lugaro(value: f64, target: f64, tau: f64, dt: f64) -> f64 {
264 target + (value - target) * (-dt / tau).exp()
265 }
266
267 fn assert_close_lugaro(actual: f64, expected: f64, tolerance: f64) {
268 assert!(
269 (actual - expected).abs() <= tolerance,
270 "actual={actual:.16e} expected={expected:.16e} tolerance={tolerance:.3e}"
271 );
272 }
273
274 #[test]
275 fn lugaro_extreme_input_bounded() {
276 let mut n = LugaroCell::new();
277 for _ in 0..1000 {
278 n.step(1e6);
279 }
280 assert!(n.v.is_finite() && n.v <= 60.0);
281 }
282
283 #[test]
284 fn lugaro_reset_clears_state() {
285 let mut n = LugaroCell::new();
286 for _ in 0..1000 {
287 n.step(10.0);
288 }
289 n.reset();
290 assert_eq!(n.v, -55.0);
291 assert_eq!(n.adapt, 0.0);
292 assert_eq!(n.serotonin, 0.0);
293 }
294
295 #[test]
296 fn lugaro_adapt_increases_during_spiking() {
297 let mut n = LugaroCell::new();
298 let initial = n.adapt;
299 for _ in 0..5000 {
300 n.step(10.0);
301 }
302 assert!(
303 n.adapt > initial,
304 "Adaptation must increase during spiking, adapt={}",
305 n.adapt
306 );
307 }
308
309 #[test]
310 fn lugaro_performance_10k_steps() {
311 let start = std::time::Instant::now();
312 let mut n = LugaroCell::new();
313 for _ in 0..10_000 {
314 std::hint::black_box(n.step(5.0));
315 }
316 let elapsed = start.elapsed();
317 assert!(
318 elapsed.as_millis() < 50,
319 "10k steps must complete in <50ms, took {}ms",
320 elapsed.as_millis()
321 );
322 }
323
324 #[test]
325 fn lugaro_default_matches_constructor_contract() {
326 let default = LugaroCell::default();
327 let constructed = LugaroCell::new();
328 assert_eq!(default.v, constructed.v);
329 assert_eq!(default.adapt, constructed.adapt);
330 assert_eq!(default.serotonin, constructed.serotonin);
331 assert_eq!(default.dt, constructed.dt);
332 }
333}