Skip to main content

sc_neurocore_engine/neurons/
simple_spiking.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 — Two-dimensional and higher spiking neuron models
8
9//! Two-dimensional and higher spiking neuron models.
10
11use rand::{RngExt, SeedableRng};
12use rand_xoshiro::Xoshiro256PlusPlus;
13
14/// FitzHugh-Nagumo 1961 — 2D qualitative spike model.
15#[derive(Clone, Debug)]
16pub struct FitzHughNagumoNeuron {
17    pub v: f64,
18    pub w: f64,
19    pub a: f64,
20    pub b: f64,
21    pub epsilon: f64,
22    pub dt: f64,
23    pub v_threshold: f64,
24}
25
26impl FitzHughNagumoNeuron {
27    pub fn new() -> Self {
28        Self {
29            v: -1.0,
30            w: -0.5,
31            a: 0.7,
32            b: 0.8,
33            epsilon: 0.08,
34            dt: 0.1,
35            v_threshold: 1.0,
36        }
37    }
38
39    pub fn step(&mut self, current: f64) -> i32 {
40        if !(current.is_finite() && self.is_valid()) {
41            return -1;
42        }
43        let v_prev = self.v;
44        let Some((new_v, new_w)) = self.rk4_candidate(current) else {
45            return -1;
46        };
47        if !(new_v.is_finite() && new_w.is_finite()) {
48            return -1;
49        }
50        self.v = new_v;
51        self.w = new_w;
52        if self.v >= self.v_threshold && v_prev < self.v_threshold {
53            1
54        } else {
55            0
56        }
57    }
58
59    pub fn reset(&mut self) {
60        self.v = -1.0;
61        self.w = -0.5;
62    }
63
64    /// Run `n_steps` RK4 updates under a constant input, returning the `v` trace
65    /// and the upward-crossing spike count. Reuses `step` (RK4) so the trace is
66    /// bit-identical to the per-step path and — because the right-hand side is
67    /// exact arithmetic (`v.powi(3)` is `v*v*v`) — to the Python reference. The
68    /// final state is left in `self.v` / `self.w`.
69    pub fn simulate(&mut self, n_steps: usize, current: f64) -> (Vec<f64>, i64) {
70        let mut trace = Vec::with_capacity(n_steps);
71        let mut spikes: i64 = 0;
72        for _ in 0..n_steps {
73            let spiked = self.step(current);
74            trace.push(self.v);
75            if spiked == 1 {
76                spikes += 1;
77            }
78        }
79        (trace, spikes)
80    }
81
82    fn is_valid(&self) -> bool {
83        self.v.is_finite()
84            && self.w.is_finite()
85            && self.a.is_finite()
86            && self.b.is_finite()
87            && self.epsilon.is_finite()
88            && self.dt.is_finite()
89            && self.v_threshold.is_finite()
90            && self.b > 0.0
91            && self.epsilon > 0.0
92            && self.dt > 0.0
93    }
94
95    fn rhs(&self, v: f64, w: f64, current: f64) -> Option<(f64, f64)> {
96        if !(v.is_finite() && w.is_finite() && current.is_finite()) {
97            return None;
98        }
99        let dv = v - v.powi(3) / 3.0 - w + current;
100        let dw = self.epsilon * (v + self.a - self.b * w);
101        if dv.is_finite() && dw.is_finite() {
102            Some((dv, dw))
103        } else {
104            None
105        }
106    }
107
108    fn rk4_candidate(&self, current: f64) -> Option<(f64, f64)> {
109        let (v0, w0, dt) = (self.v, self.w, self.dt);
110        let (k1v, k1w) = self.rhs(v0, w0, current)?;
111        let (k2v, k2w) = self.rhs(v0 + 0.5 * dt * k1v, w0 + 0.5 * dt * k1w, current)?;
112        let (k3v, k3w) = self.rhs(v0 + 0.5 * dt * k2v, w0 + 0.5 * dt * k2w, current)?;
113        let (k4v, k4w) = self.rhs(v0 + dt * k3v, w0 + dt * k3w, current)?;
114        Some((
115            v0 + dt * (k1v + 2.0 * k2v + 2.0 * k3v + k4v) / 6.0,
116            w0 + dt * (k1w + 2.0 * k2w + 2.0 * k3w + k4w) / 6.0,
117        ))
118    }
119}
120impl Default for FitzHughNagumoNeuron {
121    fn default() -> Self {
122        Self::new()
123    }
124}
125
126/// Morris-Lecar 1981 — barnacle muscle fiber.
127#[derive(Clone, Debug)]
128pub struct MorrisLecarNeuron {
129    pub v: f64,
130    pub w: f64,
131    pub c_m: f64,
132    pub g_ca: f64,
133    pub g_k: f64,
134    pub g_l: f64,
135    pub e_ca: f64,
136    pub e_k: f64,
137    pub e_l: f64,
138    pub v1: f64,
139    pub v2: f64,
140    pub v3: f64,
141    pub v4: f64,
142    pub phi: f64,
143    pub dt: f64,
144    pub v_threshold: f64,
145}
146
147impl MorrisLecarNeuron {
148    pub fn new() -> Self {
149        Self {
150            v: -60.0,
151            w: 0.0,
152            c_m: 20.0,
153            g_ca: 4.0,
154            g_k: 8.0,
155            g_l: 2.0,
156            e_ca: 120.0,
157            e_k: -84.0,
158            e_l: -60.0,
159            v1: -1.2,
160            v2: 18.0,
161            v3: 12.0,
162            v4: 17.4,
163            phi: 1.0 / 15.0,
164            dt: 0.1,
165            v_threshold: 0.0,
166        }
167    }
168    fn valid_numeric_contract(&self) -> bool {
169        self.v.is_finite()
170            && self.w.is_finite()
171            && self.c_m.is_finite()
172            && self.g_ca.is_finite()
173            && self.g_k.is_finite()
174            && self.g_l.is_finite()
175            && self.e_ca.is_finite()
176            && self.e_k.is_finite()
177            && self.e_l.is_finite()
178            && self.v1.is_finite()
179            && self.v2.is_finite()
180            && self.v3.is_finite()
181            && self.v4.is_finite()
182            && self.phi.is_finite()
183            && self.dt.is_finite()
184            && self.v_threshold.is_finite()
185            && self.c_m > 0.0
186            && self.g_ca > 0.0
187            && self.g_k > 0.0
188            && self.g_l > 0.0
189            && self.v2 > 0.0
190            && self.v4 > 0.0
191            && self.phi > 0.0
192            && self.dt > 0.0
193            && (0.0..=1.0).contains(&self.w)
194    }
195
196    fn m_inf(&self, v: f64) -> f64 {
197        0.5 * (1.0 + ((v - self.v1) / self.v2).tanh())
198    }
199
200    fn w_inf(&self, v: f64) -> f64 {
201        0.5 * (1.0 + ((v - self.v3) / self.v4).tanh())
202    }
203
204    fn lambda(&self, v: f64) -> f64 {
205        self.phi * ((v - self.v3) / (2.0 * self.v4)).cosh()
206    }
207
208    fn rhs(&self, v: f64, w: f64, current: f64) -> Option<(f64, f64)> {
209        if !(v.is_finite() && w.is_finite() && current.is_finite() && (0.0..=1.0).contains(&w)) {
210            return None;
211        }
212        let m_inf = self.m_inf(v);
213        let w_inf = self.w_inf(v);
214        let lam = self.lambda(v);
215        if !(m_inf.is_finite() && w_inf.is_finite() && lam.is_finite()) {
216            return None;
217        }
218        let i_ca = self.g_ca * m_inf * (v - self.e_ca);
219        let i_k = self.g_k * w * (v - self.e_k);
220        let i_l = self.g_l * (v - self.e_l);
221        let dv = (-i_ca - i_k - i_l + current) / self.c_m;
222        let dw = lam * (w_inf - w);
223        if dv.is_finite() && dw.is_finite() {
224            Some((dv, dw))
225        } else {
226            None
227        }
228    }
229
230    pub fn step(&mut self, current: f64) -> i32 {
231        if !self.valid_numeric_contract() || !current.is_finite() {
232            return 0;
233        }
234        let v_prev = self.v;
235        let Some((k1_v, k1_w)) = self.rhs(self.v, self.w, current) else {
236            return 0;
237        };
238        let Some((k2_v, k2_w)) = self.rhs(
239            self.v + 0.5 * self.dt * k1_v,
240            self.w + 0.5 * self.dt * k1_w,
241            current,
242        ) else {
243            return 0;
244        };
245        let Some((k3_v, k3_w)) = self.rhs(
246            self.v + 0.5 * self.dt * k2_v,
247            self.w + 0.5 * self.dt * k2_w,
248            current,
249        ) else {
250            return 0;
251        };
252        let Some((k4_v, k4_w)) =
253            self.rhs(self.v + self.dt * k3_v, self.w + self.dt * k3_w, current)
254        else {
255            return 0;
256        };
257        let next_v = self.v + self.dt * (k1_v + 2.0 * k2_v + 2.0 * k3_v + k4_v) / 6.0;
258        let next_w = self.w + self.dt * (k1_w + 2.0 * k2_w + 2.0 * k3_w + k4_w) / 6.0;
259        if !(next_v.is_finite() && next_w.is_finite() && (0.0..=1.0).contains(&next_w)) {
260            return 0;
261        }
262        self.v = next_v;
263        self.w = next_w;
264        if self.v >= self.v_threshold && v_prev < self.v_threshold {
265            1
266        } else {
267            0
268        }
269    }
270    pub fn reset(&mut self) {
271        self.v = -60.0;
272        self.w = 0.0;
273    }
274}
275impl Default for MorrisLecarNeuron {
276    fn default() -> Self {
277        Self::new()
278    }
279}
280
281/// Hindmarsh-Rose 1984 — 3D chaotic bursting model.
282#[derive(Clone, Debug)]
283pub struct HindmarshRoseNeuron {
284    pub x: f64,
285    pub y: f64,
286    pub z: f64,
287    pub b: f64,
288    pub r: f64,
289    pub s: f64,
290    pub x_rest: f64,
291    pub dt: f64,
292    pub x_threshold: f64,
293}
294
295impl HindmarshRoseNeuron {
296    pub fn new() -> Self {
297        Self {
298            x: -1.6,
299            y: -10.0,
300            z: 2.0,
301            b: 3.0,
302            r: 0.001,
303            s: 4.0,
304            x_rest: -1.6,
305            dt: 0.1,
306            x_threshold: 1.0,
307        }
308    }
309    fn valid_state(&self) -> bool {
310        self.x.is_finite()
311            && self.y.is_finite()
312            && self.z.is_finite()
313            && self.b.is_finite()
314            && self.r.is_finite()
315            && self.s.is_finite()
316            && self.x_rest.is_finite()
317            && self.dt.is_finite()
318            && self.x_threshold.is_finite()
319            && self.r > 0.0
320            && self.s > 0.0
321            && self.dt > 0.0
322    }
323    fn derivatives(&self, x: f64, y: f64, z: f64, current: f64) -> Option<(f64, f64, f64)> {
324        if !(x.is_finite() && y.is_finite() && z.is_finite() && current.is_finite()) {
325            return None;
326        }
327        let derivative = (
328            y - x.powi(3) + self.b * x.powi(2) - z + current,
329            1.0 - 5.0 * x.powi(2) - y,
330            self.r * (self.s * (x - self.x_rest) - z),
331        );
332        if derivative.0.is_finite() && derivative.1.is_finite() && derivative.2.is_finite() {
333            Some(derivative)
334        } else {
335            None
336        }
337    }
338    pub fn step(&mut self, current: f64) -> i32 {
339        if !self.valid_state() || !current.is_finite() {
340            return 0;
341        }
342        let x_prev = self.x;
343        let (x0, y0, z0) = (self.x, self.y, self.z);
344        let dt = self.dt;
345        let Some(k1) = self.derivatives(x0, y0, z0, current) else {
346            return 0;
347        };
348        let Some(k2) = self.derivatives(
349            x0 + 0.5 * dt * k1.0,
350            y0 + 0.5 * dt * k1.1,
351            z0 + 0.5 * dt * k1.2,
352            current,
353        ) else {
354            return 0;
355        };
356        let Some(k3) = self.derivatives(
357            x0 + 0.5 * dt * k2.0,
358            y0 + 0.5 * dt * k2.1,
359            z0 + 0.5 * dt * k2.2,
360            current,
361        ) else {
362            return 0;
363        };
364        let Some(k4) = self.derivatives(x0 + dt * k3.0, y0 + dt * k3.1, z0 + dt * k3.2, current)
365        else {
366            return 0;
367        };
368        let next_x = x0 + (dt / 6.0) * (k1.0 + 2.0 * k2.0 + 2.0 * k3.0 + k4.0);
369        let next_y = y0 + (dt / 6.0) * (k1.1 + 2.0 * k2.1 + 2.0 * k3.1 + k4.1);
370        let next_z = z0 + (dt / 6.0) * (k1.2 + 2.0 * k2.2 + 2.0 * k3.2 + k4.2);
371        if !(next_x.is_finite() && next_y.is_finite() && next_z.is_finite()) {
372            return 0;
373        }
374        self.x = next_x;
375        self.y = next_y;
376        self.z = next_z;
377        if self.x >= self.x_threshold && x_prev < self.x_threshold {
378            1
379        } else {
380            0
381        }
382    }
383    pub fn reset(&mut self) {
384        self.x = -1.6;
385        self.y = -10.0;
386        self.z = 2.0;
387    }
388
389    /// Run `n_steps` RK4 updates under a constant input, returning the `x` trace
390    /// and the upward-crossing spike count. Reuses `step` (RK4) so the trace is
391    /// bit-identical to the per-step path and — because the right-hand side is
392    /// exact arithmetic (`x.powi(3)` = `x*x*x`, `x.powi(2)` = `x*x`) — to the
393    /// Python reference, even though the bursting dynamics are chaotic. The
394    /// final state is left in `self.x` / `self.y` / `self.z`.
395    pub fn simulate(&mut self, n_steps: usize, current: f64) -> (Vec<f64>, i64) {
396        let mut trace = Vec::with_capacity(n_steps);
397        let mut spikes: i64 = 0;
398        for _ in 0..n_steps {
399            let spiked = self.step(current);
400            trace.push(self.x);
401            if spiked == 1 {
402                spikes += 1;
403            }
404        }
405        (trace, spikes)
406    }
407}
408impl Default for HindmarshRoseNeuron {
409    fn default() -> Self {
410        Self::new()
411    }
412}
413
414/// Resonate-and-Fire — damped oscillator with threshold. Izhikevich 2001.
415#[derive(Clone, Debug)]
416pub struct ResonateAndFireNeuron {
417    pub x: f64,
418    pub y: f64,
419    pub b: f64,
420    pub omega: f64,
421    pub threshold: f64,
422    pub dt: f64,
423}
424
425impl ResonateAndFireNeuron {
426    pub fn new() -> Self {
427        Self {
428            x: 0.0,
429            y: 0.0,
430            b: -0.1,
431            omega: 1.0,
432            threshold: 1.0,
433            dt: 0.05,
434        }
435    }
436    pub fn step(&mut self, current: f64) -> i32 {
437        // Izhikevich 2001: simultaneous Euler (both derivatives use old state)
438        let dx = (self.b * self.x - self.omega * self.y + current) * self.dt;
439        let dy = (self.omega * self.x + self.b * self.y) * self.dt;
440        self.x += dx;
441        self.y += dy;
442        let r = (self.x * self.x + self.y * self.y).sqrt();
443        if r >= self.threshold {
444            self.x = 0.0;
445            self.y = 0.0;
446            1
447        } else {
448            0
449        }
450    }
451    pub fn reset(&mut self) {
452        self.x = 0.0;
453        self.y = 0.0;
454    }
455}
456impl Default for ResonateAndFireNeuron {
457    fn default() -> Self {
458        Self::new()
459    }
460}
461
462/// Balanced Resonate-and-Fire — divergence-bound RF with smooth refractory
463/// reset. Higuchi, Kairat, Bohte, and Otte 2024, Algorithm 1.
464#[derive(Clone, Debug)]
465pub struct BalancedResonateAndFireNeuron {
466    pub x: f64,
467    pub y: f64,
468    pub q: f64,
469    pub omega: f64,
470    pub b_offset: f64,
471    pub threshold: f64,
472    pub gamma: f64,
473    pub dt: f64,
474}
475
476pub fn brf_sustain_oscillation_boundary(omega: f64, dt: f64) -> f64 {
477    let scaled = dt * omega;
478    (-1.0 + (1.0 - scaled * scaled).max(0.0).sqrt()) / dt
479}
480
481impl BalancedResonateAndFireNeuron {
482    pub fn new() -> Self {
483        Self {
484            x: 0.0,
485            y: 0.0,
486            q: 0.0,
487            omega: 10.0,
488            b_offset: 1.0,
489            threshold: 1.0,
490            gamma: 0.9,
491            dt: 0.01,
492        }
493    }
494
495    pub fn p_omega(&self) -> f64 {
496        brf_sustain_oscillation_boundary(self.omega, self.dt)
497    }
498
499    pub fn damping(&self) -> f64 {
500        self.p_omega() - self.b_offset - self.q
501    }
502
503    pub fn dynamic_threshold(&self) -> f64 {
504        self.threshold + self.q
505    }
506
507    pub fn step(&mut self, current: f64) -> i32 {
508        if !(self.dt.is_finite()
509            && self.omega.is_finite()
510            && self.dt > 0.0
511            && self.omega > 0.0
512            && self.dt * self.omega <= 1.0)
513        {
514            return 0;
515        }
516        let b_t = self.damping();
517        let theta_t = self.dynamic_threshold();
518        let x_prev = self.x;
519        let y_prev = self.y;
520        self.x = x_prev + self.dt * (b_t * x_prev - self.omega * y_prev + current);
521        self.y = y_prev + self.dt * (self.omega * x_prev + b_t * y_prev);
522        let spike = if self.x >= theta_t { 1 } else { 0 };
523        self.q = self.gamma * self.q + spike as f64;
524        spike
525    }
526
527    pub fn reset(&mut self) {
528        self.x = 0.0;
529        self.y = 0.0;
530        self.q = 0.0;
531    }
532}
533impl Default for BalancedResonateAndFireNeuron {
534    fn default() -> Self {
535        Self::new()
536    }
537}
538
539/// FitzHugh-Rinzel — 3D extension with slow bursting variable.
540#[derive(Clone, Debug)]
541pub struct FitzHughRinzelNeuron {
542    pub v: f64,
543    pub w: f64,
544    pub y: f64,
545    pub a: f64,
546    pub b: f64,
547    pub c: f64,
548    pub d: f64,
549    pub delta: f64,
550    pub mu: f64,
551    pub dt: f64,
552    pub v_threshold: f64,
553}
554
555impl FitzHughRinzelNeuron {
556    pub fn new() -> Self {
557        Self {
558            v: -1.0,
559            w: -0.5,
560            y: 0.0,
561            a: 0.7,
562            b: 0.8,
563            c: -0.775,
564            d: 1.0,
565            delta: 0.08,
566            mu: 0.0001,
567            dt: 0.1,
568            v_threshold: 1.0,
569        }
570    }
571
572    fn valid_numeric_contract(&self) -> bool {
573        self.v.is_finite()
574            && self.w.is_finite()
575            && self.y.is_finite()
576            && self.a.is_finite()
577            && self.b.is_finite()
578            && self.c.is_finite()
579            && self.d.is_finite()
580            && self.delta.is_finite()
581            && self.mu.is_finite()
582            && self.dt.is_finite()
583            && self.v_threshold.is_finite()
584            && self.b > 0.0
585            && self.d > 0.0
586            && self.delta > 0.0
587            && self.mu > 0.0
588            && self.dt > 0.0
589    }
590
591    fn derivatives(&self, v: f64, w: f64, y: f64, current: f64) -> Option<(f64, f64, f64)> {
592        if !(v.is_finite() && w.is_finite() && y.is_finite() && current.is_finite()) {
593            return None;
594        }
595        let dv = v - v.powi(3) / 3.0 - w + y + current;
596        let dw = self.delta * (self.a + v - self.b * w);
597        let dy = self.mu * (self.c - v - self.d * y);
598        if dv.is_finite() && dw.is_finite() && dy.is_finite() {
599            Some((dv, dw, dy))
600        } else {
601            None
602        }
603    }
604
605    fn rk4_candidate(&self, current: f64) -> Option<(f64, f64, f64)> {
606        let (v0, w0, y0, dt) = (self.v, self.w, self.y, self.dt);
607        let (k1v, k1w, k1y) = self.derivatives(v0, w0, y0, current)?;
608        let (k2v, k2w, k2y) = self.derivatives(
609            v0 + 0.5 * dt * k1v,
610            w0 + 0.5 * dt * k1w,
611            y0 + 0.5 * dt * k1y,
612            current,
613        )?;
614        let (k3v, k3w, k3y) = self.derivatives(
615            v0 + 0.5 * dt * k2v,
616            w0 + 0.5 * dt * k2w,
617            y0 + 0.5 * dt * k2y,
618            current,
619        )?;
620        let (k4v, k4w, k4y) =
621            self.derivatives(v0 + dt * k3v, w0 + dt * k3w, y0 + dt * k3y, current)?;
622        Some((
623            v0 + dt * (k1v + 2.0 * k2v + 2.0 * k3v + k4v) / 6.0,
624            w0 + dt * (k1w + 2.0 * k2w + 2.0 * k3w + k4w) / 6.0,
625            y0 + dt * (k1y + 2.0 * k2y + 2.0 * k3y + k4y) / 6.0,
626        ))
627    }
628
629    pub fn step(&mut self, current: f64) -> i32 {
630        if !self.valid_numeric_contract() || !current.is_finite() {
631            return 0;
632        }
633        let v_prev = self.v;
634        let Some((next_v, next_w, next_y)) = self.rk4_candidate(current) else {
635            return 0;
636        };
637        if !(next_v.is_finite() && next_w.is_finite() && next_y.is_finite()) {
638            return 0;
639        }
640        self.v = next_v;
641        self.w = next_w;
642        self.y = next_y;
643        if self.v >= self.v_threshold && v_prev < self.v_threshold {
644            1
645        } else {
646            0
647        }
648    }
649
650    pub fn reset(&mut self) {
651        self.v = -1.0;
652        self.w = -0.5;
653        self.y = 0.0;
654    }
655
656    /// Run `n_steps` RK4 updates under a constant input, returning the `v` trace
657    /// and the upward-crossing spike count. Reuses `step` (RK4) so the trace is
658    /// bit-identical to the per-step path and — because the right-hand side is
659    /// exact arithmetic (`v.powi(3)` = `v*v*v`) — to the Python reference. The
660    /// final state is left in `self.v` / `self.w` / `self.y`.
661    pub fn simulate(&mut self, n_steps: usize, current: f64) -> (Vec<f64>, i64) {
662        let mut trace = Vec::with_capacity(n_steps);
663        let mut spikes: i64 = 0;
664        for _ in 0..n_steps {
665            let spiked = self.step(current);
666            trace.push(self.v);
667            if spiked == 1 {
668                spikes += 1;
669            }
670        }
671        (trace, spikes)
672    }
673}
674impl Default for FitzHughRinzelNeuron {
675    fn default() -> Self {
676        Self::new()
677    }
678}
679
680/// McKean 1970 — piecewise-linear FHN variant.
681#[derive(Clone, Debug)]
682pub struct McKeanNeuron {
683    pub v: f64,
684    pub w: f64,
685    pub a: f64,
686    pub epsilon: f64,
687    pub gamma: f64,
688    pub dt: f64,
689    pub v_peak: f64,
690}
691
692impl McKeanNeuron {
693    pub fn new() -> Self {
694        Self {
695            v: 0.0,
696            w: 0.0,
697            a: 0.25,
698            epsilon: 0.01,
699            gamma: 0.5,
700            dt: 0.1,
701            v_peak: 0.8,
702        }
703    }
704    fn f_v(&self, v: f64) -> f64 {
705        let half_a = self.a / 2.0;
706        let mid = (1.0 + self.a) / 2.0;
707        if v < half_a {
708            -v
709        } else if v < mid {
710            v - self.a
711        } else {
712            1.0 - v
713        }
714    }
715    fn valid_numeric_contract(&self) -> bool {
716        self.v.is_finite()
717            && self.w.is_finite()
718            && self.a.is_finite()
719            && self.epsilon.is_finite()
720            && self.gamma.is_finite()
721            && self.dt.is_finite()
722            && self.v_peak.is_finite()
723            && self.a > 0.0
724            && self.a < 1.0
725            && self.epsilon > 0.0
726            && self.gamma > 0.0
727            && self.dt > 0.0
728    }
729    fn derivatives(&self, v: f64, w: f64, current: f64) -> Option<(f64, f64)> {
730        if !(v.is_finite() && w.is_finite() && current.is_finite()) {
731            return None;
732        }
733        let dv = self.f_v(v) - w + current;
734        let dw = self.epsilon * (v - self.gamma * w);
735        if dv.is_finite() && dw.is_finite() {
736            Some((dv, dw))
737        } else {
738            None
739        }
740    }
741    fn rk4_candidate(&self, current: f64) -> Option<(f64, f64)> {
742        let v0 = self.v;
743        let w0 = self.w;
744        let dt = self.dt;
745        let k1 = self.derivatives(v0, w0, current)?;
746        let k2 = self.derivatives(v0 + 0.5 * dt * k1.0, w0 + 0.5 * dt * k1.1, current)?;
747        let k3 = self.derivatives(v0 + 0.5 * dt * k2.0, w0 + 0.5 * dt * k2.1, current)?;
748        let k4 = self.derivatives(v0 + dt * k3.0, w0 + dt * k3.1, current)?;
749        let next_v = v0 + dt * (k1.0 + 2.0 * k2.0 + 2.0 * k3.0 + k4.0) / 6.0;
750        let next_w = w0 + dt * (k1.1 + 2.0 * k2.1 + 2.0 * k3.1 + k4.1) / 6.0;
751        if next_v.is_finite() && next_w.is_finite() {
752            Some((next_v, next_w))
753        } else {
754            None
755        }
756    }
757    pub fn step(&mut self, current: f64) -> i32 {
758        if !self.valid_numeric_contract() || !current.is_finite() {
759            return 0;
760        }
761        let v_prev = self.v;
762        let (next_v, next_w) = match self.rk4_candidate(current) {
763            Some(candidate) => candidate,
764            None => return 0,
765        };
766        self.v = next_v;
767        self.w = next_w;
768        if self.v >= self.v_peak && v_prev < self.v_peak {
769            1
770        } else {
771            0
772        }
773    }
774    /// Run `n_steps` RK4 updates under a constant input, returning the `v` trace
775    /// and the upward-`v_peak`-crossing spike count. Reuses `step`, so the trace
776    /// is bit-identical to the per-step path and to the Python reference (the
777    /// piecewise-linear RHS is exact arithmetic — no transcendental functions).
778    /// The final state is left in `self.v` / `self.w`.
779    pub fn simulate(&mut self, n_steps: usize, current: f64) -> (Vec<f64>, i64) {
780        let mut trace = Vec::with_capacity(n_steps);
781        let mut spikes: i64 = 0;
782        for _ in 0..n_steps {
783            let spiked = self.step(current);
784            trace.push(self.v);
785            spikes += spiked as i64;
786        }
787        (trace, spikes)
788    }
789    pub fn reset(&mut self) {
790        self.v = 0.0;
791        self.w = 0.0;
792    }
793}
794impl Default for McKeanNeuron {
795    fn default() -> Self {
796        Self::new()
797    }
798}
799
800/// Terman-Wang oscillator — segmentation by oscillatory correlation.
801#[derive(Clone, Debug)]
802pub struct TermanWangOscillator {
803    pub v: f64,
804    pub w: f64,
805    pub alpha: f64,
806    pub beta: f64,
807    pub epsilon: f64,
808    pub rho: f64,
809    pub dt: f64,
810    pub v_peak: f64,
811}
812
813impl TermanWangOscillator {
814    pub fn new() -> Self {
815        Self {
816            v: -1.5,
817            w: -0.5,
818            alpha: 3.0,
819            beta: 0.2,
820            epsilon: 0.02,
821            rho: 0.0,
822            dt: 0.05,
823            v_peak: 1.5,
824        }
825    }
826    fn valid_numeric_contract(&self) -> bool {
827        self.v.is_finite()
828            && self.w.is_finite()
829            && self.alpha.is_finite()
830            && self.beta.is_finite()
831            && self.epsilon.is_finite()
832            && self.rho.is_finite()
833            && self.dt.is_finite()
834            && self.v_peak.is_finite()
835            && self.beta > 0.0
836            && self.epsilon > 0.0
837            && self.dt > 0.0
838    }
839    fn derivatives(&self, v: f64, w: f64, current: f64) -> Option<(f64, f64)> {
840        if !(v.is_finite() && w.is_finite() && current.is_finite()) {
841            return None;
842        }
843        let f = 3.0 * v - v.powi(3) + 2.0;
844        let g = self.alpha * (1.0 + (v / self.beta).tanh());
845        let dv = f - w + current + self.rho;
846        let dw = self.epsilon * (g - w);
847        if dv.is_finite() && dw.is_finite() {
848            Some((dv, dw))
849        } else {
850            None
851        }
852    }
853    fn rk4_candidate(&self, current: f64) -> Option<(f64, f64)> {
854        let dt = self.dt;
855        let (k1v, k1w) = self.derivatives(self.v, self.w, current)?;
856        let (k2v, k2w) =
857            self.derivatives(self.v + 0.5 * dt * k1v, self.w + 0.5 * dt * k1w, current)?;
858        let (k3v, k3w) =
859            self.derivatives(self.v + 0.5 * dt * k2v, self.w + 0.5 * dt * k2w, current)?;
860        let (k4v, k4w) = self.derivatives(self.v + dt * k3v, self.w + dt * k3w, current)?;
861        let v = self.v + dt * (k1v + 2.0 * k2v + 2.0 * k3v + k4v) / 6.0;
862        let w = self.w + dt * (k1w + 2.0 * k2w + 2.0 * k3w + k4w) / 6.0;
863        if v.is_finite() && w.is_finite() {
864            Some((v, w))
865        } else {
866            None
867        }
868    }
869
870    pub fn step(&mut self, current: f64) -> i32 {
871        if !self.valid_numeric_contract() || !current.is_finite() {
872            return 0;
873        }
874        let v_prev = self.v;
875        let Some((next_v, next_w)) = self.rk4_candidate(current) else {
876            return 0;
877        };
878        self.v = next_v;
879        self.w = next_w;
880        if self.v >= self.v_peak && v_prev < self.v_peak {
881            1
882        } else {
883            0
884        }
885    }
886    /// Run `n_steps` RK4 updates under a constant input, returning the `v` trace
887    /// and the upward-`v_peak`-crossing spike count. Reuses `step`; the cubic uses
888    /// `v.powi(3)` = `v*v*v` (matching the Python `v*v*v`). The hyperbolic-tangent
889    /// gating resolves to the same glibc `tanh` as the Python reference on Linux,
890    /// so this backend is bit-identical there. The final state is left in
891    /// `self.v` / `self.w`.
892    pub fn simulate(&mut self, n_steps: usize, current: f64) -> (Vec<f64>, i64) {
893        let mut trace = Vec::with_capacity(n_steps);
894        let mut spikes: i64 = 0;
895        for _ in 0..n_steps {
896            let spiked = self.step(current);
897            trace.push(self.v);
898            spikes += spiked as i64;
899        }
900        (trace, spikes)
901    }
902    pub fn reset(&mut self) {
903        self.v = -1.5;
904        self.w = -0.5;
905    }
906}
907impl Default for TermanWangOscillator {
908    fn default() -> Self {
909        Self::new()
910    }
911}
912
913/// Benda-Herz 2003 — firing-rate adaptation via subtractive feedback.
914#[derive(Clone, Debug)]
915pub struct BendaHerzNeuron {
916    pub a: f64,
917    pub f_max: f64,
918    pub beta: f64,
919    pub i_half: f64,
920    pub tau_a: f64,
921    pub delta_a: f64,
922    pub dt: f64,
923    rng: Xoshiro256PlusPlus,
924}
925
926impl BendaHerzNeuron {
927    pub fn new(seed: u64) -> Self {
928        Self {
929            a: 0.0,
930            f_max: 200.0,
931            beta: 0.1,
932            i_half: 5.0,
933            tau_a: 100.0,
934            delta_a: 0.5,
935            dt: 1.0,
936            rng: Xoshiro256PlusPlus::seed_from_u64(seed),
937        }
938    }
939    pub fn step(&mut self, current: f64) -> i32 {
940        let x = current - self.a;
941        let rate = self.f_max / (1.0 + (-self.beta * (x - self.i_half)).exp());
942        self.a += (-self.a / self.tau_a + self.delta_a * rate) * self.dt;
943        let p = rate * self.dt / 1000.0;
944        if self.rng.random::<f64>() < p {
945            1
946        } else {
947            0
948        }
949    }
950    pub fn reset(&mut self) {
951        self.a = 0.0;
952    }
953}
954
955/// Alpha-synapse LIF — separate excitatory/inhibitory exponential synapses.
956#[derive(Clone, Debug)]
957pub struct AlphaNeuron {
958    pub v: f64,
959    pub i_exc: f64,
960    pub i_inh: f64,
961    pub v_rest: f64,
962    pub v_threshold: f64,
963    pub tau_v: f64,
964    pub tau_exc: f64,
965    pub tau_inh: f64,
966    pub dt: f64,
967}
968
969impl AlphaNeuron {
970    pub fn new() -> Self {
971        Self {
972            v: 0.0,
973            i_exc: 0.0,
974            i_inh: 0.0,
975            v_rest: 0.0,
976            v_threshold: 1.0,
977            tau_v: 20.0,
978            tau_exc: 5.0,
979            tau_inh: 10.0,
980            dt: 1.0,
981        }
982    }
983    pub fn step(&mut self, exc_current: f64, inh_current: f64) -> i32 {
984        self.i_exc += (-self.i_exc / self.tau_exc + exc_current) * self.dt;
985        self.i_inh += (-self.i_inh / self.tau_inh + inh_current) * self.dt;
986        self.v += (-(self.v - self.v_rest) + self.i_exc - self.i_inh) / self.tau_v * self.dt;
987        if self.v >= self.v_threshold {
988            self.v = self.v_rest;
989            1
990        } else {
991            0
992        }
993    }
994    pub fn reset(&mut self) {
995        self.v = self.v_rest;
996        self.i_exc = 0.0;
997        self.i_inh = 0.0;
998    }
999}
1000impl Default for AlphaNeuron {
1001    fn default() -> Self {
1002        Self::new()
1003    }
1004}
1005
1006/// Conductance-based LIF (COBA). Brette et al. 2007.
1007#[derive(Clone, Debug)]
1008pub struct COBALIFNeuron {
1009    pub v: f64,
1010    pub g_e: f64,
1011    pub g_i: f64,
1012    pub c_m: f64,
1013    pub g_l: f64,
1014    pub e_l: f64,
1015    pub e_e: f64,
1016    pub e_i: f64,
1017    pub tau_e: f64,
1018    pub tau_i: f64,
1019    pub v_threshold: f64,
1020    pub v_reset: f64,
1021    pub dt: f64,
1022}
1023
1024impl COBALIFNeuron {
1025    pub fn new() -> Self {
1026        Self {
1027            v: -65.0,
1028            g_e: 0.0,
1029            g_i: 0.0,
1030            c_m: 200.0,
1031            g_l: 10.0,
1032            e_l: -65.0,
1033            e_e: 0.0,
1034            e_i: -80.0,
1035            tau_e: 5.0,
1036            tau_i: 10.0,
1037            v_threshold: -50.0,
1038            v_reset: -65.0,
1039            dt: 0.1,
1040        }
1041    }
1042    pub fn step(&mut self, current: f64, delta_ge: f64, delta_gi: f64) -> i32 {
1043        self.g_e += delta_ge;
1044        self.g_i += delta_gi;
1045        let i_syn = self.g_e * (self.v - self.e_e) + self.g_i * (self.v - self.e_i);
1046        self.v += (-self.g_l * (self.v - self.e_l) - i_syn + current) / self.c_m * self.dt;
1047        self.g_e *= (-self.dt / self.tau_e).exp();
1048        self.g_i *= (-self.dt / self.tau_i).exp();
1049        if self.v >= self.v_threshold {
1050            self.v = self.v_reset;
1051            1
1052        } else {
1053            0
1054        }
1055    }
1056    pub fn reset(&mut self) {
1057        self.v = self.e_l;
1058        self.g_e = 0.0;
1059        self.g_i = 0.0;
1060    }
1061}
1062impl Default for COBALIFNeuron {
1063    fn default() -> Self {
1064        Self::new()
1065    }
1066}
1067
1068/// Gutkin-Ermentrout — reduced cortical neuron with Type-I excitability.
1069#[derive(Clone, Debug)]
1070pub struct GutkinErmentroutNeuron {
1071    pub v: f64,
1072    pub n: f64,
1073    pub g_na: f64,
1074    pub g_k: f64,
1075    pub g_l: f64,
1076    pub e_na: f64,
1077    pub e_k: f64,
1078    pub e_l: f64,
1079    pub dt: f64,
1080    pub v_threshold: f64,
1081}
1082
1083impl GutkinErmentroutNeuron {
1084    pub fn new() -> Self {
1085        Self {
1086            v: -65.0,
1087            n: 0.1,
1088            g_na: 20.0,
1089            g_k: 10.0,
1090            g_l: 8.0,
1091            e_na: 60.0,
1092            e_k: -90.0,
1093            e_l: -80.0,
1094            dt: 0.05,
1095            v_threshold: -20.0,
1096        }
1097    }
1098    fn valid_numeric_contract(&self) -> bool {
1099        self.v.is_finite()
1100            && self.n.is_finite()
1101            && (0.0..=1.0).contains(&self.n)
1102            && self.g_na.is_finite()
1103            && self.g_na >= 0.0
1104            && self.g_k.is_finite()
1105            && self.g_k >= 0.0
1106            && self.g_l.is_finite()
1107            && self.g_l >= 0.0
1108            && self.e_na.is_finite()
1109            && self.e_k.is_finite()
1110            && self.e_l.is_finite()
1111            && self.dt.is_finite()
1112            && self.dt > 0.0
1113            && self.v_threshold.is_finite()
1114    }
1115
1116    fn m_inf(v: f64) -> f64 {
1117        1.0 / (1.0 + (-(v + 20.0) / 15.0).exp())
1118    }
1119
1120    fn n_inf(v: f64) -> f64 {
1121        1.0 / (1.0 + (-(v + 25.0) / 5.0).exp())
1122    }
1123
1124    fn rhs(&self, v: f64, n_gate: f64, current: f64) -> Option<(f64, f64)> {
1125        if !(v.is_finite() && n_gate.is_finite() && current.is_finite()) {
1126            return None;
1127        }
1128        if !(0.0..=1.0).contains(&n_gate) {
1129            return None;
1130        }
1131        let m_inf = Self::m_inf(v);
1132        let n_inf = Self::n_inf(v);
1133        if !(m_inf.is_finite() && n_inf.is_finite()) {
1134            return None;
1135        }
1136        let i_na = self.g_na * m_inf * (v - self.e_na);
1137        let i_k = self.g_k * n_gate * (v - self.e_k);
1138        let i_l = self.g_l * (v - self.e_l);
1139        let dv = -i_na - i_k - i_l + current;
1140        let dn = n_inf - n_gate;
1141        if dv.is_finite() && dn.is_finite() {
1142            Some((dv, dn))
1143        } else {
1144            None
1145        }
1146    }
1147
1148    pub fn step(&mut self, current: f64) -> i32 {
1149        if !self.valid_numeric_contract() || !current.is_finite() {
1150            return 0;
1151        }
1152        let v_prev = self.v;
1153        let Some((k1_v, k1_n)) = self.rhs(self.v, self.n, current) else {
1154            return 0;
1155        };
1156        let Some((k2_v, k2_n)) = self.rhs(
1157            self.v + 0.5 * self.dt * k1_v,
1158            self.n + 0.5 * self.dt * k1_n,
1159            current,
1160        ) else {
1161            return 0;
1162        };
1163        let Some((k3_v, k3_n)) = self.rhs(
1164            self.v + 0.5 * self.dt * k2_v,
1165            self.n + 0.5 * self.dt * k2_n,
1166            current,
1167        ) else {
1168            return 0;
1169        };
1170        let Some((k4_v, k4_n)) =
1171            self.rhs(self.v + self.dt * k3_v, self.n + self.dt * k3_n, current)
1172        else {
1173            return 0;
1174        };
1175        let next_v = self.v + self.dt * (k1_v + 2.0 * k2_v + 2.0 * k3_v + k4_v) / 6.0;
1176        let next_n = self.n + self.dt * (k1_n + 2.0 * k2_n + 2.0 * k3_n + k4_n) / 6.0;
1177        if !(next_v.is_finite() && next_n.is_finite() && (0.0..=1.0).contains(&next_n)) {
1178            return 0;
1179        }
1180        self.v = next_v;
1181        self.n = next_n;
1182        if self.v >= self.v_threshold && v_prev < self.v_threshold {
1183            1
1184        } else {
1185            0
1186        }
1187    }
1188    pub fn reset(&mut self) {
1189        self.v = -65.0;
1190        self.n = 0.1;
1191    }
1192}
1193impl Default for GutkinErmentroutNeuron {
1194    fn default() -> Self {
1195        Self::new()
1196    }
1197}
1198
1199/// Wilson HR — polynomial cortical neuron. Wilson 1999.
1200#[derive(Clone, Debug)]
1201pub struct WilsonHRNeuron {
1202    pub v: f64,
1203    pub r: f64,
1204    pub tau_r: f64,
1205    pub v_peak: f64,
1206    pub dt: f64,
1207}
1208
1209impl WilsonHRNeuron {
1210    pub fn new() -> Self {
1211        Self {
1212            v: -0.7,
1213            r: 0.1,
1214            tau_r: 1.9,
1215            v_peak: 0.4,
1216            dt: 0.05,
1217        }
1218    }
1219    fn valid_numeric_contract(&self) -> bool {
1220        self.v.is_finite()
1221            && self.r.is_finite()
1222            && self.tau_r.is_finite()
1223            && self.tau_r > 0.0
1224            && self.v_peak.is_finite()
1225            && self.dt.is_finite()
1226            && self.dt > 0.0
1227    }
1228    fn poly(v: f64) -> f64 {
1229        -(17.81 + 47.71 * v + 32.63 * v * v) * (v - 0.55)
1230    }
1231    fn derivatives(&self, v: f64, r: f64, current: f64) -> Option<(f64, f64)> {
1232        if !(v.is_finite() && r.is_finite() && current.is_finite()) {
1233            return None;
1234        }
1235        let poly = Self::poly(v);
1236        let syn = -26.0 * r * (v + 0.92);
1237        let dv = poly + syn + current;
1238        let dr = (-r + 1.35 * v + 1.03) / self.tau_r;
1239        if poly.is_finite() && syn.is_finite() && dv.is_finite() && dr.is_finite() {
1240            Some((dv, dr))
1241        } else {
1242            None
1243        }
1244    }
1245    fn rk4_candidate(&self, current: f64) -> Option<(f64, f64)> {
1246        let v0 = self.v;
1247        let r0 = self.r;
1248        let dt = self.dt;
1249        let k1 = self.derivatives(v0, r0, current)?;
1250        let k2 = self.derivatives(v0 + 0.5 * dt * k1.0, r0 + 0.5 * dt * k1.1, current)?;
1251        let k3 = self.derivatives(v0 + 0.5 * dt * k2.0, r0 + 0.5 * dt * k2.1, current)?;
1252        let k4 = self.derivatives(v0 + dt * k3.0, r0 + dt * k3.1, current)?;
1253        let next_v = v0 + dt * (k1.0 + 2.0 * k2.0 + 2.0 * k3.0 + k4.0) / 6.0;
1254        let next_r = r0 + dt * (k1.1 + 2.0 * k2.1 + 2.0 * k3.1 + k4.1) / 6.0;
1255        if next_v.is_finite() && next_r.is_finite() {
1256            Some((next_v, next_r))
1257        } else {
1258            None
1259        }
1260    }
1261    pub fn step(&mut self, current: f64) -> i32 {
1262        if !self.valid_numeric_contract() || !current.is_finite() {
1263            return 0;
1264        }
1265        let (next_v, next_r) = match self.rk4_candidate(current) {
1266            Some(candidate) => candidate,
1267            None => return 0,
1268        };
1269        self.v = next_v;
1270        self.r = next_r;
1271        // Wilson 1999: hard reset on threshold (not crossing)
1272        if self.v >= self.v_peak {
1273            self.v = -0.7;
1274            1
1275        } else {
1276            0
1277        }
1278    }
1279    /// Run `n_steps` RK4 updates under a constant input, returning the `v` trace
1280    /// (already reset to `-0.7` on spiking steps) and the spike count. Reuses
1281    /// `step`, so the trace is bit-identical to the per-step path and to the
1282    /// Python reference (the right-hand side is exact polynomial arithmetic — no
1283    /// transcendental functions). The final state is left in `self.v` / `self.r`.
1284    pub fn simulate(&mut self, n_steps: usize, current: f64) -> (Vec<f64>, i64) {
1285        let mut trace = Vec::with_capacity(n_steps);
1286        let mut spikes: i64 = 0;
1287        for _ in 0..n_steps {
1288            let spiked = self.step(current);
1289            trace.push(self.v);
1290            spikes += spiked as i64;
1291        }
1292        (trace, spikes)
1293    }
1294    pub fn reset(&mut self) {
1295        self.v = -0.7;
1296        self.r = 0.1;
1297    }
1298}
1299impl Default for WilsonHRNeuron {
1300    fn default() -> Self {
1301        Self::new()
1302    }
1303}
1304
1305/// Chay 1985 — pancreatic beta cell with Ca-dependent K.
1306#[derive(Clone, Debug)]
1307pub struct ChayNeuron {
1308    pub v: f64,
1309    pub n: f64,
1310    pub ca: f64,
1311    pub g_ca: f64,
1312    pub g_k: f64,
1313    pub g_kca: f64,
1314    pub g_l: f64,
1315    pub e_ca: f64,
1316    pub e_k: f64,
1317    pub e_l: f64,
1318    pub rho: f64,
1319    pub alpha_ca: f64,
1320    pub k_ca: f64,
1321    pub dt: f64,
1322    pub v_threshold: f64,
1323}
1324
1325impl ChayNeuron {
1326    pub fn new() -> Self {
1327        Self {
1328            v: -50.0,
1329            n: 0.1,
1330            ca: 0.1,
1331            g_ca: 25.0,
1332            g_k: 1400.0,
1333            g_kca: 12.0,
1334            g_l: 7.0,
1335            e_ca: 100.0,
1336            e_k: -75.0,
1337            e_l: -40.0,
1338            rho: 0.00015,
1339            alpha_ca: 0.002,
1340            k_ca: 0.04,
1341            dt: 0.02,
1342            v_threshold: -20.0,
1343        }
1344    }
1345    pub fn step(&mut self, current: f64) -> i32 {
1346        if !current.is_finite() || !self.dt.is_finite() || self.dt <= 0.0 {
1347            return 0;
1348        }
1349
1350        let v_initial = self.v;
1351        let mut v = self.v;
1352        let mut n = self.n;
1353        let mut ca = self.ca;
1354        let substeps = (self.dt / 0.001_f64).ceil().max(1.0) as usize;
1355        let h = self.dt / substeps as f64;
1356        let mut crossed = false;
1357
1358        for _ in 0..substeps {
1359            let m_inf = 1.0 / (1.0 + (-(v + 25.0) / 8.0).clamp(-700.0, 700.0).exp());
1360            let n_inf = 1.0 / (1.0 + (-(v + 18.0) / 14.0).clamp(-700.0, 700.0).exp());
1361            let d = (v + 18.0).abs().max(0.01);
1362            let tau_n = 1.0 / (0.01 * d);
1363            let ca_denominator = ca + 1.0;
1364            if ca_denominator <= 0.0 {
1365                return 0;
1366            }
1367            let kca_act = ca / ca_denominator;
1368            let i_ca = self.g_ca * m_inf * (v - self.e_ca);
1369            let i_k = self.g_k * n * (v - self.e_k);
1370            let i_kca = self.g_kca * kca_act * (v - self.e_k);
1371            let i_l = self.g_l * (v - self.e_l);
1372
1373            let v_next = v + (-i_ca - i_k - i_kca - i_l + current) * h;
1374            let n_next = n + (n_inf - n) / tau_n.max(0.01) * h;
1375            let ca_next = ca + self.rho * (-self.alpha_ca * i_ca - self.k_ca * ca) * h;
1376            if !v_next.is_finite()
1377                || !n_next.is_finite()
1378                || !ca_next.is_finite()
1379                || !(-200.0..=200.0).contains(&v_next)
1380                || !(0.0..=1.0).contains(&n_next)
1381                || !(0.0..=100.0).contains(&ca_next)
1382            {
1383                return 0;
1384            }
1385            crossed = crossed || (v_next >= self.v_threshold && v < self.v_threshold);
1386            v = v_next;
1387            n = n_next;
1388            ca = ca_next;
1389        }
1390
1391        self.v = v;
1392        self.n = n;
1393        self.ca = ca;
1394        if crossed && v_initial < self.v_threshold {
1395            1
1396        } else {
1397            0
1398        }
1399    }
1400    pub fn reset(&mut self) {
1401        self.v = -50.0;
1402        self.n = 0.1;
1403        self.ca = 0.1;
1404    }
1405}
1406impl Default for ChayNeuron {
1407    fn default() -> Self {
1408        Self::new()
1409    }
1410}
1411
1412/// Chay-Keizer — modified beta cell with inactivating Ca current.
1413#[derive(Clone, Debug)]
1414pub struct ChayKeizerNeuron {
1415    pub v: f64,
1416    pub n: f64,
1417    pub ca: f64,
1418    pub g_ca: f64,
1419    pub g_k: f64,
1420    pub g_kca: f64,
1421    pub g_l: f64,
1422    pub e_ca: f64,
1423    pub e_k: f64,
1424    pub e_l: f64,
1425    pub k_d: f64,
1426    pub f_ca: f64,
1427    pub k_ca: f64,
1428    pub dt: f64,
1429    pub v_threshold: f64,
1430}
1431
1432impl ChayKeizerNeuron {
1433    pub fn new() -> Self {
1434        Self {
1435            v: -50.0,
1436            n: 0.01,
1437            ca: 0.1,
1438            g_ca: 20.0,
1439            g_k: 25.0,
1440            g_kca: 12.0,
1441            g_l: 0.1,
1442            e_ca: 100.0,
1443            e_k: -75.0,
1444            e_l: -40.0,
1445            k_d: 1.0,
1446            f_ca: 0.004,
1447            k_ca: 0.03,
1448            dt: 0.02,
1449            v_threshold: -20.0,
1450        }
1451    }
1452    pub fn step(&mut self, current: f64) -> i32 {
1453        let v_prev = self.v;
1454        let m_inf = 1.0 / (1.0 + (-(self.v + 25.0) / 8.0).exp());
1455        let n_inf = 1.0 / (1.0 + (-(self.v + 18.0) / 14.0).exp());
1456        let tau_n = (20.0 / (1.0 + ((self.v + 18.0) / 14.0).exp())).max(0.1);
1457        let q_kca = self.ca / (self.ca + self.k_d);
1458        let i_ca = self.g_ca * m_inf * (self.v - self.e_ca);
1459        let i_k = self.g_k * self.n * (self.v - self.e_k);
1460        let i_kca = self.g_kca * q_kca * (self.v - self.e_k);
1461        let i_l = self.g_l * (self.v - self.e_l);
1462        self.v += (-i_ca - i_k - i_kca - i_l + current) * self.dt;
1463        self.v = self.v.clamp(-200.0, 200.0);
1464        self.n += (n_inf - self.n) / tau_n * self.dt;
1465        self.ca = (self.ca + (-self.f_ca * i_ca - self.k_ca * self.ca) * self.dt).max(0.0);
1466        if self.v >= self.v_threshold && v_prev < self.v_threshold {
1467            1
1468        } else {
1469            0
1470        }
1471    }
1472    pub fn reset(&mut self) {
1473        self.v = -50.0;
1474        self.n = 0.01;
1475        self.ca = 0.1;
1476    }
1477}
1478impl Default for ChayKeizerNeuron {
1479    fn default() -> Self {
1480        Self::new()
1481    }
1482}
1483
1484/// Sherman-Rinzel-Keizer 1988 — pancreatic beta cell (reduced).
1485#[derive(Clone, Debug)]
1486pub struct ShermanRinzelKeizerNeuron {
1487    pub v: f64,
1488    pub n: f64,
1489    pub s: f64,
1490    pub g_ca: f64,
1491    pub g_k: f64,
1492    pub g_s: f64,
1493    pub e_ca: f64,
1494    pub e_k: f64,
1495    pub tau_s: f64,
1496    pub dt: f64,
1497    pub v_threshold: f64,
1498}
1499
1500impl ShermanRinzelKeizerNeuron {
1501    pub fn new() -> Self {
1502        Self {
1503            v: -50.0,
1504            n: 0.1,
1505            s: 0.1,
1506            g_ca: 3.6,
1507            g_k: 10.0,
1508            g_s: 4.0,
1509            e_ca: 25.0,
1510            e_k: -75.0,
1511            tau_s: 5000.0,
1512            dt: 0.5,
1513            v_threshold: -20.0,
1514        }
1515    }
1516    pub fn step(&mut self, current: f64) -> i32 {
1517        let v_prev = self.v;
1518        let m_inf = 1.0 / (1.0 + (-(self.v + 20.0) / 12.0).exp());
1519        let n_inf = 1.0 / (1.0 + (-(self.v + 16.0) / 5.0).exp());
1520        let s_inf = 1.0 / (1.0 + (-(self.v + 35.0) / 10.0).exp());
1521        let tau_n = 9.09;
1522        let i_ca = self.g_ca * m_inf * (self.v - self.e_ca);
1523        let i_k = self.g_k * self.n * (self.v - self.e_k);
1524        let i_s = self.g_s * self.s * (self.v - self.e_k);
1525        self.v += (-i_ca - i_k - i_s + current) * self.dt;
1526        self.n += (n_inf - self.n) / tau_n * self.dt;
1527        self.s += (s_inf - self.s) / self.tau_s * self.dt;
1528        if self.v >= self.v_threshold && v_prev < self.v_threshold {
1529            1
1530        } else {
1531            0
1532        }
1533    }
1534    pub fn reset(&mut self) {
1535        self.v = -50.0;
1536        self.n = 0.1;
1537        self.s = 0.1;
1538    }
1539}
1540impl Default for ShermanRinzelKeizerNeuron {
1541    fn default() -> Self {
1542        Self::new()
1543    }
1544}
1545
1546/// Butera pre-Bötzinger respiratory neuron. Butera et al. 1999.
1547#[derive(Clone, Debug)]
1548pub struct ButeraRespiratoryNeuron {
1549    pub v: f64,
1550    pub n: f64,
1551    pub h_nap: f64,
1552    pub g_na: f64,
1553    pub g_nap: f64,
1554    pub g_k: f64,
1555    pub g_l: f64,
1556    pub e_na: f64,
1557    pub e_k: f64,
1558    pub e_l: f64,
1559    pub tau_h: f64,
1560    pub dt: f64,
1561    pub v_threshold: f64,
1562}
1563
1564impl ButeraRespiratoryNeuron {
1565    pub fn new() -> Self {
1566        Self {
1567            v: -50.0,
1568            n: 0.01,
1569            h_nap: 0.5,
1570            g_na: 28.0,
1571            g_nap: 2.8,
1572            g_k: 11.2,
1573            g_l: 2.8,
1574            e_na: 50.0,
1575            e_k: -85.0,
1576            e_l: -65.0,
1577            tau_h: 10000.0,
1578            dt: 0.1,
1579            v_threshold: -20.0,
1580        }
1581    }
1582    fn butera_valid_state(v: f64, n: f64, h_nap: f64) -> bool {
1583        [v, n, h_nap].iter().all(|x| x.is_finite())
1584            && (-200.0..=100.0).contains(&v)
1585            && (-0.05..=1.05).contains(&n)
1586            && (-0.05..=1.05).contains(&h_nap)
1587    }
1588
1589    fn butera_valid_static(&self) -> bool {
1590        [
1591            self.g_na,
1592            self.g_nap,
1593            self.g_k,
1594            self.g_l,
1595            self.e_na,
1596            self.e_k,
1597            self.e_l,
1598            self.tau_h,
1599            self.dt,
1600            self.v_threshold,
1601        ]
1602        .iter()
1603        .all(|x| x.is_finite())
1604            && self.g_na >= 0.0
1605            && self.g_nap >= 0.0
1606            && self.g_k >= 0.0
1607            && self.g_l >= 0.0
1608            && self.tau_h > 0.0
1609            && self.dt > 0.0
1610    }
1611
1612    fn butera_derivatives(&self, state: (f64, f64, f64), current: f64) -> Option<(f64, f64, f64)> {
1613        let (mut v, mut n, mut h_nap) = state;
1614        if !(v.is_finite() && n.is_finite() && h_nap.is_finite() && current.is_finite()) {
1615            return None;
1616        }
1617        v = v.clamp(-200.0, 100.0);
1618        n = n.clamp(0.0, 1.0);
1619        h_nap = h_nap.clamp(0.0, 1.0);
1620        let m_na = 1.0 / (1.0 + (-(v + 34.0) / 5.0).exp());
1621        let n_inf = 1.0 / (1.0 + (-(v + 29.0) / 4.0).exp());
1622        let m_nap = 1.0 / (1.0 + (-(v + 40.0) / 6.0).exp());
1623        let h_nap_inf = 1.0 / (1.0 + ((v + 48.0) / 6.0).exp());
1624        let tau_n = (10.0 / ((v + 29.0) / 8.0).cosh().max(1e-12)).max(0.01);
1625        let tau_h_eff = (self.tau_h / ((v + 48.0) / 12.0).cosh().max(1e-12)).max(0.1);
1626        let i_na = self.g_na * m_na.powi(3) * (1.0 - n) * (v - self.e_na);
1627        let i_nap = self.g_nap * m_nap * h_nap * (v - self.e_na);
1628        let i_k = self.g_k * n.powi(4) * (v - self.e_k);
1629        let i_l = self.g_l * (v - self.e_l);
1630        let deriv = (
1631            -i_na - i_nap - i_k - i_l + current,
1632            (n_inf - n) / tau_n,
1633            (h_nap_inf - h_nap) / tau_h_eff,
1634        );
1635        [deriv.0, deriv.1, deriv.2]
1636            .iter()
1637            .all(|x| x.is_finite())
1638            .then_some(deriv)
1639    }
1640
1641    fn butera_rk4_candidate(&self, current: f64) -> Option<(f64, f64, f64)> {
1642        if !self.butera_valid_static()
1643            || !current.is_finite()
1644            || !Self::butera_valid_state(self.v, self.n, self.h_nap)
1645        {
1646            return None;
1647        }
1648        let state = (self.v, self.n, self.h_nap);
1649        let k1 = self.butera_derivatives(state, current)?;
1650        let k2_state = (
1651            state.0 + 0.5 * self.dt * k1.0,
1652            state.1 + 0.5 * self.dt * k1.1,
1653            state.2 + 0.5 * self.dt * k1.2,
1654        );
1655        let k2 = self.butera_derivatives(k2_state, current)?;
1656        let k3_state = (
1657            state.0 + 0.5 * self.dt * k2.0,
1658            state.1 + 0.5 * self.dt * k2.1,
1659            state.2 + 0.5 * self.dt * k2.2,
1660        );
1661        let k3 = self.butera_derivatives(k3_state, current)?;
1662        let k4_state = (
1663            state.0 + self.dt * k3.0,
1664            state.1 + self.dt * k3.1,
1665            state.2 + self.dt * k3.2,
1666        );
1667        let k4 = self.butera_derivatives(k4_state, current)?;
1668        let candidate = (
1669            state.0 + self.dt * (k1.0 + 2.0 * k2.0 + 2.0 * k3.0 + k4.0) / 6.0,
1670            state.1 + self.dt * (k1.1 + 2.0 * k2.1 + 2.0 * k3.1 + k4.1) / 6.0,
1671            state.2 + self.dt * (k1.2 + 2.0 * k2.2 + 2.0 * k3.2 + k4.2) / 6.0,
1672        );
1673        if candidate.0.is_finite() && candidate.1.is_finite() && candidate.2.is_finite() {
1674            Some((
1675                candidate.0.clamp(-200.0, 100.0),
1676                candidate.1.clamp(0.0, 1.0),
1677                candidate.2.clamp(0.0, 1.0),
1678            ))
1679        } else {
1680            None
1681        }
1682    }
1683
1684    pub fn step(&mut self, current: f64) -> i32 {
1685        let v_prev = self.v;
1686        let Some((v, n, h_nap)) = self.butera_rk4_candidate(current) else {
1687            return 0;
1688        };
1689        self.v = v;
1690        self.n = n;
1691        self.h_nap = h_nap;
1692        if self.v >= self.v_threshold && v_prev < self.v_threshold {
1693            1
1694        } else {
1695            0
1696        }
1697    }
1698    pub fn reset(&mut self) {
1699        self.v = -50.0;
1700        self.n = 0.01;
1701        self.h_nap = 0.5;
1702    }
1703}
1704impl Default for ButeraRespiratoryNeuron {
1705    fn default() -> Self {
1706        Self::new()
1707    }
1708}
1709
1710/// e-prop ALIF — adaptive LIF for eligibility-propagation learning. Bellec et al. 2020.
1711#[derive(Clone, Debug)]
1712pub struct EPropALIFNeuron {
1713    pub v: f64,
1714    pub a: f64,
1715    pub e_trace: f64,
1716    pub alpha_m: f64,
1717    pub alpha_a: f64,
1718    pub v_threshold_base: f64,
1719    pub beta: f64,
1720}
1721
1722impl EPropALIFNeuron {
1723    pub fn new(tau_m: f64, tau_a: f64, dt: f64) -> Self {
1724        Self {
1725            v: 0.0,
1726            a: 0.0,
1727            e_trace: 0.0,
1728            alpha_m: (-dt / tau_m).exp(),
1729            alpha_a: (-dt / tau_a).exp(),
1730            v_threshold_base: 1.0,
1731            beta: 0.07,
1732        }
1733    }
1734    pub fn step(&mut self, current: f64) -> i32 {
1735        self.v = self.alpha_m * self.v + current;
1736        let threshold = self.v_threshold_base + self.beta * self.a;
1737        let psi = ((1.0 - (self.v - threshold).abs()) * 0.3).max(0.0);
1738        self.e_trace = self.alpha_a * self.e_trace + psi;
1739        if self.v >= threshold {
1740            self.v = 0.0;
1741            self.a = self.alpha_a * self.a + 1.0;
1742            1
1743        } else {
1744            self.a *= self.alpha_a;
1745            0
1746        }
1747    }
1748    pub fn reset(&mut self) {
1749        self.v = 0.0;
1750        self.a = 0.0;
1751        self.e_trace = 0.0;
1752    }
1753}
1754
1755impl Default for EPropALIFNeuron {
1756    fn default() -> Self {
1757        Self::new(20.0, 200.0, 1.0)
1758    }
1759}
1760
1761/// SuperSpike — LIF with surrogate gradient trace. Zenke & Ganguli 2018.
1762#[derive(Clone, Debug)]
1763pub struct SuperSpikeNeuron {
1764    pub v: f64,
1765    pub trace: f64,
1766    pub alpha_m: f64,
1767    pub alpha_e: f64,
1768    pub v_threshold: f64,
1769    pub v_reset: f64,
1770    pub beta_sg: f64,
1771}
1772
1773impl SuperSpikeNeuron {
1774    pub fn new(tau_m: f64, tau_e: f64, dt: f64) -> Self {
1775        Self {
1776            v: 0.0,
1777            trace: 0.0,
1778            alpha_m: (-dt / tau_m).exp(),
1779            alpha_e: (-dt / tau_e).exp(),
1780            v_threshold: 1.0,
1781            v_reset: 0.0,
1782            beta_sg: 10.0,
1783        }
1784    }
1785    pub fn step(&mut self, current: f64) -> i32 {
1786        self.v = self.alpha_m * self.v + current;
1787        let sg = 1.0 / (self.beta_sg * (self.v - self.v_threshold).abs() + 1.0).powi(2);
1788        self.trace = self.alpha_e * self.trace + sg;
1789        if self.v >= self.v_threshold {
1790            self.v = self.v_reset;
1791            1
1792        } else {
1793            0
1794        }
1795    }
1796    pub fn reset(&mut self) {
1797        self.v = 0.0;
1798        self.trace = 0.0;
1799    }
1800}
1801
1802impl Default for SuperSpikeNeuron {
1803    fn default() -> Self {
1804        Self::new(10.0, 10.0, 1.0)
1805    }
1806}
1807
1808/// Learnable Neuron Model (LNM) — parameterised activation + decay.
1809#[derive(Clone, Debug)]
1810pub struct LearnableNeuronModel {
1811    pub v: f64,
1812    pub alpha: f64,
1813    pub beta: f64,
1814    pub gamma: f64,
1815    pub v_threshold: f64,
1816    pub f_slope: f64,
1817    pub f_shift: f64,
1818}
1819
1820impl LearnableNeuronModel {
1821    pub fn new() -> Self {
1822        Self {
1823            v: 0.0,
1824            alpha: 0.9,
1825            beta: 0.1,
1826            gamma: 0.05,
1827            v_threshold: 1.0,
1828            f_slope: 5.0,
1829            f_shift: 0.5,
1830        }
1831    }
1832    pub fn step(&mut self, current: f64) -> i32 {
1833        let f_v = 1.0 / (1.0 + (-(self.f_slope * (self.v - self.f_shift))).exp());
1834        self.v = self.alpha * self.v + self.beta * current + self.gamma * f_v;
1835        if self.v >= self.v_threshold {
1836            self.v = 0.0;
1837            1
1838        } else {
1839            0
1840        }
1841    }
1842    pub fn reset(&mut self) {
1843        self.v = 0.0;
1844    }
1845}
1846impl Default for LearnableNeuronModel {
1847    fn default() -> Self {
1848        Self::new()
1849    }
1850}
1851
1852/// Pernarowski 1994 — simplified beta cell burster (3 ODE).
1853#[derive(Clone, Debug)]
1854pub struct PernarowskiNeuron {
1855    pub v: f64,
1856    pub w: f64,
1857    pub z: f64,
1858    pub alpha: f64,
1859    pub beta: f64,
1860    pub eps1: f64,
1861    pub eps2: f64,
1862    pub gamma: f64,
1863    pub dt: f64,
1864    pub v_threshold: f64,
1865}
1866
1867impl PernarowskiNeuron {
1868    pub fn new() -> Self {
1869        Self {
1870            v: -1.0,
1871            w: 0.0,
1872            z: 0.0,
1873            alpha: 0.1,
1874            beta: 0.5,
1875            eps1: 0.1,
1876            eps2: 0.001,
1877            gamma: 0.5,
1878            dt: 0.1,
1879            v_threshold: 0.5,
1880        }
1881    }
1882    fn is_valid(&self) -> bool {
1883        self.v.is_finite()
1884            && self.w.is_finite()
1885            && self.z.is_finite()
1886            && self.alpha.is_finite()
1887            && self.beta.is_finite()
1888            && self.eps1.is_finite()
1889            && self.eps1 > 0.0
1890            && self.eps2.is_finite()
1891            && self.eps2 > 0.0
1892            && self.gamma.is_finite()
1893            && self.gamma > 0.0
1894            && self.dt.is_finite()
1895            && self.dt > 0.0
1896            && self.v_threshold.is_finite()
1897    }
1898    fn derivatives(&self, v: f64, w: f64, z: f64, current: f64) -> Option<(f64, f64, f64)> {
1899        if !(v.is_finite() && w.is_finite() && z.is_finite() && current.is_finite()) {
1900            return None;
1901        }
1902        let dv = v - v.powi(3) / 3.0 - w - z + current;
1903        let dw = self.eps1 * (v - self.gamma * w + self.alpha);
1904        let dz = self.eps2 * (self.beta * (v + 0.7) - z);
1905        if dv.is_finite() && dw.is_finite() && dz.is_finite() {
1906            Some((dv, dw, dz))
1907        } else {
1908            None
1909        }
1910    }
1911    fn rk4_candidate(&self, current: f64) -> Option<(f64, f64, f64)> {
1912        let dt = self.dt;
1913        let (k1v, k1w, k1z) = self.derivatives(self.v, self.w, self.z, current)?;
1914        let (k2v, k2w, k2z) = self.derivatives(
1915            self.v + 0.5 * dt * k1v,
1916            self.w + 0.5 * dt * k1w,
1917            self.z + 0.5 * dt * k1z,
1918            current,
1919        )?;
1920        let (k3v, k3w, k3z) = self.derivatives(
1921            self.v + 0.5 * dt * k2v,
1922            self.w + 0.5 * dt * k2w,
1923            self.z + 0.5 * dt * k2z,
1924            current,
1925        )?;
1926        let (k4v, k4w, k4z) = self.derivatives(
1927            self.v + dt * k3v,
1928            self.w + dt * k3w,
1929            self.z + dt * k3z,
1930            current,
1931        )?;
1932        let v = self.v + dt * (k1v + 2.0 * k2v + 2.0 * k3v + k4v) / 6.0;
1933        let w = self.w + dt * (k1w + 2.0 * k2w + 2.0 * k3w + k4w) / 6.0;
1934        let z = self.z + dt * (k1z + 2.0 * k2z + 2.0 * k3z + k4z) / 6.0;
1935        if v.is_finite() && w.is_finite() && z.is_finite() {
1936            Some((v, w, z))
1937        } else {
1938            None
1939        }
1940    }
1941    pub fn step(&mut self, current: f64) -> i32 {
1942        if !self.is_valid() || !current.is_finite() {
1943            return 0;
1944        }
1945        let v_prev = self.v;
1946        let Some((v, w, z)) = self.rk4_candidate(current) else {
1947            return 0;
1948        };
1949        self.v = v;
1950        self.w = w;
1951        self.z = z;
1952        if self.v >= self.v_threshold && v_prev < self.v_threshold {
1953            1
1954        } else {
1955            0
1956        }
1957    }
1958    /// Run `n_steps` RK4 updates under a constant input, returning the `v` trace
1959    /// and the upward-`v_threshold`-crossing spike count. Reuses `step`, so the
1960    /// trace is bit-identical to the per-step path and to the Python reference
1961    /// (the cubic uses `v.powi(3)` = `v*v*v`, matching the Python `v*v*v`; no
1962    /// transcendental functions). The final state is left in `self.v` / `self.w`
1963    /// / `self.z`.
1964    pub fn simulate(&mut self, n_steps: usize, current: f64) -> (Vec<f64>, i64) {
1965        let mut trace = Vec::with_capacity(n_steps);
1966        let mut spikes: i64 = 0;
1967        for _ in 0..n_steps {
1968            let spiked = self.step(current);
1969            trace.push(self.v);
1970            spikes += spiked as i64;
1971        }
1972        (trace, spikes)
1973    }
1974    pub fn reset(&mut self) {
1975        self.v = -1.0;
1976        self.w = 0.0;
1977        self.z = 0.0;
1978    }
1979}
1980impl Default for PernarowskiNeuron {
1981    fn default() -> Self {
1982        Self::new()
1983    }
1984}
1985
1986// ═══════════════════════════════════════════════════════════════════
1987// Brunel-Wang LIF with NMDA/AMPA/GABA
1988// ═══════════════════════════════════════════════════════════════════
1989
1990/// Brunel-Wang 2001 — LIF with NMDA (Mg²⁺ block), AMPA, and GABA synaptic
1991/// conductances for decision-making and working memory circuits.
1992///
1993/// Key feature: voltage-dependent NMDA conductance via Mg²⁺ block factor
1994/// `1 / (1 + [Mg²⁺]/3.57 · exp(-0.062·V))`. This creates positive feedback
1995/// that sustains persistent activity in recurrent circuits.
1996///
1997/// The single-current interface routes external input to i_ampa_ext; recurrent
1998/// AMPA/NMDA/GABA inputs are zero (use the multi-arg `step_full()` for
1999/// network simulations).
2000///
2001/// Brunel, N. & Wang, X.J., J Comput Neurosci 11:63, 2001.
2002#[derive(Clone, Debug)]
2003pub struct BrunelWangNeuron {
2004    pub v: f64,
2005    pub v_rest: f64,
2006    pub v_reset: f64,
2007    pub v_threshold: f64,
2008    pub tau_m: f64,
2009    pub tau_ref: f64,
2010    pub g_ampa_ext: f64,
2011    pub g_ampa_rec: f64,
2012    pub g_nmda: f64,
2013    pub g_gaba: f64,
2014    pub v_ampa: f64,
2015    pub v_nmda: f64,
2016    pub v_gaba: f64,
2017    pub c_m: f64,
2018    pub mg_conc: f64,
2019    pub dt: f64,
2020    pub ref_remaining: f64,
2021    pub gain: f64,
2022}
2023
2024impl BrunelWangNeuron {
2025    pub fn new() -> Self {
2026        Self {
2027            v: -70.0,
2028            v_rest: -70.0,
2029            v_reset: -55.0,
2030            v_threshold: -50.0,
2031            tau_m: 20.0,
2032            tau_ref: 2.0,
2033            g_ampa_ext: 2.1,
2034            g_ampa_rec: 0.05,
2035            g_nmda: 0.165,
2036            g_gaba: 1.3,
2037            v_ampa: 0.0,
2038            v_nmda: 0.0,
2039            v_gaba: -70.0,
2040            c_m: 0.5,
2041            mg_conc: 1.0,
2042            dt: 0.1,
2043            ref_remaining: 0.0,
2044            gain: 1.0,
2045        }
2046    }
2047
2048    /// Mg²⁺ block factor (Jahr & Stevens 1990).
2049    #[inline]
2050    fn nmda_mg_block(&self, v: f64) -> f64 {
2051        1.0 / (1.0 + self.mg_conc / 3.57 * (-0.062 * v).exp())
2052    }
2053
2054    /// Full step with all 4 synaptic inputs (for network simulations).
2055    pub fn step_full(
2056        &mut self,
2057        i_ampa_ext: f64,
2058        s_ampa_rec: f64,
2059        s_nmda_rec: f64,
2060        s_gaba: f64,
2061    ) -> i32 {
2062        if self.ref_remaining > 0.0 {
2063            self.ref_remaining -= self.dt;
2064            return 0;
2065        }
2066
2067        let v = self.v;
2068
2069        // Synaptic currents (conductance-based)
2070        let i_ampa = -self.g_ampa_ext * (v - self.v_ampa) * i_ampa_ext
2071            - self.g_ampa_rec * (v - self.v_ampa) * s_ampa_rec;
2072        let i_nmda = -self.g_nmda * self.nmda_mg_block(v) * (v - self.v_nmda) * s_nmda_rec;
2073        let i_gaba = -self.g_gaba * (v - self.v_gaba) * s_gaba;
2074
2075        // Membrane dynamics (LIF)
2076        let i_leak = -(v - self.v_rest) / self.tau_m;
2077        let dv = (i_leak + (i_ampa + i_nmda + i_gaba) / self.c_m) * self.dt;
2078        self.v += dv;
2079
2080        if self.v >= self.v_threshold {
2081            self.v = self.v_reset;
2082            self.ref_remaining = self.tau_ref;
2083            1
2084        } else {
2085            0
2086        }
2087    }
2088
2089    /// Single-current interface: routes input to external AMPA drive.
2090    pub fn step(&mut self, current: f64) -> i32 {
2091        self.step_full(self.gain * current, 0.0, 0.0, 0.0)
2092    }
2093
2094    pub fn reset(&mut self) {
2095        self.v = self.v_rest;
2096        self.ref_remaining = 0.0;
2097    }
2098}
2099
2100impl Default for BrunelWangNeuron {
2101    fn default() -> Self {
2102        Self::new()
2103    }
2104}
2105
2106#[cfg(test)]
2107mod tests {
2108    use super::*;
2109
2110    #[test]
2111    fn fhn_fires() {
2112        let mut n = FitzHughNagumoNeuron::new();
2113        let t: i32 = (0..2000).map(|_| n.step(1.0)).sum();
2114        assert!(t > 0);
2115    }
2116    #[test]
2117    fn morris_lecar_fires() {
2118        let mut n = MorrisLecarNeuron::new();
2119        let t: i32 = (0..2000).map(|_| n.step(100.0)).sum();
2120        assert!(t > 0);
2121    }
2122    #[test]
2123    fn hr_fires() {
2124        let mut n = HindmarshRoseNeuron::new();
2125        let t: i32 = (0..2000).map(|_| n.step(3.0)).sum();
2126        assert!(t > 0);
2127    }
2128    #[test]
2129    fn rnf_fires() {
2130        let mut n = ResonateAndFireNeuron::new();
2131        let t: i32 = (0..5000).map(|_| n.step(3.0)).sum();
2132        assert!(t > 0);
2133    }
2134    #[test]
2135    fn fhr_fires() {
2136        let mut n = FitzHughRinzelNeuron::new();
2137        let t: i32 = (0..2000).map(|_| n.step(1.0)).sum();
2138        assert!(t > 0);
2139    }
2140    #[test]
2141    fn mckean_fires() {
2142        let mut n = McKeanNeuron::new();
2143        let t: i32 = (0..2000).map(|_| n.step(0.5)).sum();
2144        assert!(t > 0);
2145    }
2146    #[test]
2147    fn tw_fires() {
2148        let mut n = TermanWangOscillator::new();
2149        let t: i32 = (0..2000).map(|_| n.step(0.5)).sum();
2150        assert!(t > 0);
2151    }
2152    #[test]
2153    fn benda_herz_fires() {
2154        let mut n = BendaHerzNeuron::new(42);
2155        let t: i32 = (0..10000).map(|_| n.step(20.0)).sum();
2156        assert!(t > 0);
2157    }
2158    #[test]
2159    fn alpha_fires() {
2160        let mut n = AlphaNeuron::new();
2161        let t: i32 = (0..100).map(|_| n.step(0.5, 0.0)).sum();
2162        assert!(t > 0);
2163    }
2164    #[test]
2165    fn coba_fires() {
2166        let mut n = COBALIFNeuron::new();
2167        let t: i32 = (0..2000).map(|_| n.step(500.0, 0.0, 0.0)).sum();
2168        assert!(t > 0);
2169    }
2170    #[test]
2171    fn gutkin_fires() {
2172        let mut n = GutkinErmentroutNeuron::new();
2173        let t: i32 = (0..2000).map(|_| n.step(15.0)).sum();
2174        assert!(t > 0);
2175    }
2176    #[test]
2177    fn wilson_hr_fires() {
2178        let mut n = WilsonHRNeuron::new();
2179        let t: i32 = (0..2000).map(|_| n.step(10.0)).sum();
2180        assert!(t > 0);
2181    }
2182    #[test]
2183    fn chay_drive_changes_state_without_leaving_physical_bounds() {
2184        let mut rest = ChayNeuron::new();
2185        let mut driven = ChayNeuron::new();
2186        for _ in 0..500 {
2187            rest.step(0.0);
2188            driven.step(5.0);
2189        }
2190        assert!(driven.v > rest.v);
2191        assert!((0.0..=1.0).contains(&driven.n));
2192        assert!(driven.ca >= 0.0);
2193    }
2194    #[test]
2195    fn chay_keizer_fires() {
2196        let mut n = ChayKeizerNeuron::new();
2197        let t: i32 = (0..5000).map(|_| n.step(10.0)).sum();
2198        assert!(t > 0);
2199    }
2200    #[test]
2201    fn srk_fires() {
2202        let mut n = ShermanRinzelKeizerNeuron::new();
2203        let t: i32 = (0..5000).map(|_| n.step(5.0)).sum();
2204        assert!(t > 0);
2205    }
2206    #[test]
2207    fn butera_fires() {
2208        let mut n = ButeraRespiratoryNeuron::new();
2209        let t: i32 = (0..20000).map(|_| n.step(50.0)).sum();
2210        assert!(t > 0);
2211    }
2212    #[test]
2213    fn eprop_fires() {
2214        let mut n = EPropALIFNeuron::default();
2215        let t: i32 = (0..50).map(|_| n.step(0.5)).sum();
2216        assert!(t > 0);
2217    }
2218    #[test]
2219    fn superspike_fires() {
2220        let mut n = SuperSpikeNeuron::default();
2221        let t: i32 = (0..50).map(|_| n.step(0.5)).sum();
2222        assert!(t > 0);
2223    }
2224    #[test]
2225    fn lnm_fires() {
2226        let mut n = LearnableNeuronModel::new();
2227        let t: i32 = (0..50).map(|_| n.step(2.0)).sum();
2228        assert!(t > 0);
2229    }
2230    #[test]
2231    fn pernarowski_fires() {
2232        let mut n = PernarowskiNeuron::new();
2233        let t: i32 = (0..2000).map(|_| n.step(1.0)).sum();
2234        assert!(t > 0);
2235    }
2236
2237    // ── Multi-angle tests for simple_spiking models ──
2238
2239    // -- FitzHughNagumo --
2240    #[test]
2241    fn fhn_silent_without_input() {
2242        let mut n = FitzHughNagumoNeuron::new();
2243        let t: i32 = (0..2000).map(|_| n.step(0.0)).sum();
2244        assert_eq!(t, 0);
2245    }
2246    #[test]
2247    fn fhn_reset_clears_state() {
2248        let mut n = FitzHughNagumoNeuron::new();
2249        for _ in 0..500 {
2250            n.step(1.0);
2251        }
2252        n.reset();
2253        assert!((n.v - (-1.0)).abs() < 1e-10);
2254        assert!((n.w - (-0.5)).abs() < 1e-10);
2255    }
2256    #[test]
2257    fn fhn_moderate_input_stable() {
2258        let mut n = FitzHughNagumoNeuron::new();
2259        for _ in 0..2000 {
2260            n.step(2.0);
2261        }
2262        assert!(n.v.is_finite());
2263    }
2264    #[test]
2265    fn fhn_recovery_variable() {
2266        let mut n = FitzHughNagumoNeuron::new();
2267        for _ in 0..2000 {
2268            n.step(1.0);
2269        }
2270        // w should have evolved from initial
2271        assert!((n.w - (-0.5)).abs() > 0.01, "recovery w should change");
2272    }
2273    #[test]
2274    fn fhn_invalid_input_preserves_state() {
2275        let mut n = FitzHughNagumoNeuron::new();
2276        let before = n.clone();
2277        assert_eq!(n.step(f64::NAN), -1);
2278        assert_eq!(n.v, before.v);
2279        assert_eq!(n.w, before.w);
2280    }
2281    #[test]
2282    fn fhn_overflow_candidate_preserves_state() {
2283        let mut n = FitzHughNagumoNeuron::new();
2284        n.v = 1.0e103;
2285        let before = n.clone();
2286        assert_eq!(n.step(0.0), -1);
2287        assert_eq!(n.v, before.v);
2288        assert_eq!(n.w, before.w);
2289    }
2290    #[test]
2291    fn fhn_negative_no_crash() {
2292        let mut n = FitzHughNagumoNeuron::new();
2293        for _ in 0..500 {
2294            n.step(-5.0);
2295        }
2296        assert!(n.v.is_finite());
2297    }
2298
2299    // -- MorrisLecar --
2300    #[test]
2301    fn ml_silent_without_input() {
2302        let mut n = MorrisLecarNeuron::new();
2303        let t: i32 = (0..500).map(|_| n.step(0.0)).sum();
2304        assert_eq!(t, 0);
2305    }
2306    #[test]
2307    fn ml_reset_clears_state() {
2308        let mut n = MorrisLecarNeuron::new();
2309        for _ in 0..500 {
2310            n.step(100.0);
2311        }
2312        n.reset();
2313        assert!((n.v - (-60.0)).abs() < 1e-10);
2314    }
2315    #[test]
2316    fn ml_moderate_input_stable() {
2317        let mut n = MorrisLecarNeuron::new();
2318        for _ in 0..2000 {
2319            n.step(200.0);
2320        }
2321        assert!(n.v.is_finite());
2322    }
2323    #[test]
2324    fn ml_rk4_separates_from_forward_euler() {
2325        let mut n = MorrisLecarNeuron::new();
2326        let v0 = n.v;
2327        let w0 = n.w;
2328        let current = 50.0;
2329        let m_inf = 0.5 * (1.0 + ((v0 - n.v1) / n.v2).tanh());
2330        let w_inf = 0.5 * (1.0 + ((v0 - n.v3) / n.v4).tanh());
2331        let lam = n.phi * ((v0 - n.v3) / (2.0 * n.v4)).cosh();
2332        let i_ca = n.g_ca * m_inf * (v0 - n.e_ca);
2333        let i_k = n.g_k * w0 * (v0 - n.e_k);
2334        let i_l = n.g_l * (v0 - n.e_l);
2335        let euler_v = v0 + (-i_ca - i_k - i_l + current) / n.c_m * n.dt;
2336        let euler_w = w0 + lam * (w_inf - w0) * n.dt;
2337
2338        assert_eq!(n.step(current), 0);
2339
2340        assert!((n.v - euler_v).abs() > 1e-6);
2341        assert!((n.w - euler_w).abs() > 1e-8);
2342        assert!(n.v.is_finite());
2343        assert!((0.0..=1.0).contains(&n.w));
2344    }
2345    #[test]
2346    fn ml_nan_no_panic() {
2347        let mut n = MorrisLecarNeuron::new();
2348        let before = (n.v, n.w);
2349        assert_eq!(n.step(f64::NAN), 0);
2350        assert_eq!((n.v, n.w), before);
2351    }
2352    #[test]
2353    fn ml_overflow_candidate_preserves_state() {
2354        let mut n = MorrisLecarNeuron {
2355            v: 1.0e6,
2356            w: 0.25,
2357            ..Default::default()
2358        };
2359        let before = (n.v, n.w);
2360        assert_eq!(n.step(0.0), 0);
2361        assert_eq!((n.v, n.w), before);
2362    }
2363    #[test]
2364    fn ml_negative_no_crash() {
2365        let mut n = MorrisLecarNeuron::new();
2366        for _ in 0..500 {
2367            n.step(-50.0);
2368        }
2369        assert!(n.v.is_finite());
2370    }
2371    #[test]
2372    fn ml_k_gating_bounded() {
2373        let mut n = MorrisLecarNeuron::new();
2374        for _ in 0..2000 {
2375            n.step(100.0);
2376        }
2377        assert!(n.w >= 0.0 && n.w <= 1.0, "w={}", n.w);
2378    }
2379
2380    // -- HindmarshRose --
2381    #[test]
2382    fn hr_reset_clears_state() {
2383        let mut n = HindmarshRoseNeuron::new();
2384        for _ in 0..500 {
2385            n.step(3.0);
2386        }
2387        n.reset();
2388        assert!((n.x - (-1.6)).abs() < 1e-10);
2389    }
2390    #[test]
2391    fn hr_moderate_input_stable() {
2392        let mut n = HindmarshRoseNeuron::new();
2393        for _ in 0..2000 {
2394            n.step(5.0);
2395        }
2396        assert!(n.x.is_finite());
2397    }
2398    #[test]
2399    fn hr_slow_z_evolves() {
2400        let mut n = HindmarshRoseNeuron::new();
2401        let z0 = n.z;
2402        for _ in 0..2000 {
2403            n.step(3.0);
2404        }
2405        assert!((n.z - z0).abs() > 0.001, "slow variable z should evolve");
2406    }
2407    #[test]
2408    fn hr_nan_no_panic() {
2409        HindmarshRoseNeuron::new().step(f64::NAN);
2410    }
2411    #[test]
2412    fn hr_negative_no_crash() {
2413        let mut n = HindmarshRoseNeuron::new();
2414        for _ in 0..500 {
2415            n.step(-1.0);
2416        }
2417        assert!(n.x.is_finite());
2418    }
2419
2420    // -- ResonateAndFire --
2421    #[test]
2422    fn rnf_reset_clears_state() {
2423        let mut n = ResonateAndFireNeuron::new();
2424        for _ in 0..500 {
2425            n.step(3.0);
2426        }
2427        n.reset();
2428        assert!((n.x - 0.0).abs() < 1e-10);
2429    }
2430    #[test]
2431    fn rnf_bounded() {
2432        let mut n = ResonateAndFireNeuron::new();
2433        for _ in 0..5000 {
2434            n.step(100.0);
2435        }
2436        assert!(n.x.is_finite());
2437    }
2438    #[test]
2439    fn rnf_nan_no_panic() {
2440        ResonateAndFireNeuron::new().step(f64::NAN);
2441    }
2442    #[test]
2443    fn rnf_negative_no_crash() {
2444        let mut n = ResonateAndFireNeuron::new();
2445        for _ in 0..500 {
2446            n.step(-5.0);
2447        }
2448        assert!(n.x.is_finite());
2449    }
2450    #[test]
2451    fn rnf_subthreshold_oscillation() {
2452        let mut n = ResonateAndFireNeuron::new();
2453        for _ in 0..100 {
2454            n.step(0.5);
2455        }
2456        assert!(n.x.abs() > 0.0 || n.y.abs() > 0.0);
2457    }
2458
2459    #[test]
2460    fn brf_boundary_matches_algorithm() {
2461        let p = brf_sustain_oscillation_boundary(10.0, 0.01);
2462        let expected = (-1.0 + (1.0_f64 - 0.1_f64 * 0.1_f64).sqrt()) / 0.01;
2463        assert!((p - expected).abs() < 1e-12);
2464    }
2465
2466    #[test]
2467    fn brf_step_matches_algorithm_one_step() {
2468        let mut n = BalancedResonateAndFireNeuron {
2469            x: 0.2,
2470            y: -0.1,
2471            q: 0.3,
2472            omega: 12.0,
2473            b_offset: 0.75,
2474            threshold: 1.0,
2475            gamma: 0.9,
2476            dt: 0.01,
2477        };
2478        let p_omega = brf_sustain_oscillation_boundary(12.0, 0.01);
2479        let b_t = p_omega - 0.75 - 0.3;
2480        let expected_x = 0.2 + 0.01 * (b_t * 0.2 - 12.0 * -0.1 + 2.0);
2481        let expected_y = -0.1 + 0.01 * (12.0 * 0.2 + b_t * -0.1);
2482        let expected_spike = if expected_x >= 1.3 { 1 } else { 0 };
2483        let spike = n.step(2.0);
2484        assert_eq!(spike, expected_spike);
2485        assert!((n.x - expected_x).abs() < 1e-12);
2486        assert!((n.y - expected_y).abs() < 1e-12);
2487        assert!((n.q - (0.9 * 0.3 + expected_spike as f64)).abs() < 1e-12);
2488    }
2489
2490    #[test]
2491    fn brf_reset_clears_membrane_and_refractory_state() {
2492        let mut n = BalancedResonateAndFireNeuron::new();
2493        assert_eq!(n.step(200.0), 1);
2494        n.reset();
2495        assert_eq!(n.x, 0.0);
2496        assert_eq!(n.y, 0.0);
2497        assert_eq!(n.q, 0.0);
2498    }
2499
2500    // -- FitzHughRinzel --
2501    #[test]
2502    fn fhr_reset_clears_state() {
2503        let mut n = FitzHughRinzelNeuron::new();
2504        for _ in 0..500 {
2505            n.step(1.0);
2506        }
2507        n.reset();
2508        assert!((n.v - (-1.0)).abs() < 1e-10);
2509    }
2510    #[test]
2511    fn fhr_bounded() {
2512        let mut n = FitzHughRinzelNeuron::new();
2513        for _ in 0..2000 {
2514            n.step(50.0);
2515        }
2516        assert!(n.v.is_finite());
2517    }
2518    #[test]
2519    fn fhr_invalid_input_preserves_state() {
2520        let mut n = FitzHughRinzelNeuron::new();
2521        let before = (n.v, n.w, n.y);
2522        assert_eq!(n.step(f64::NAN), 0);
2523        assert_eq!((n.v, n.w, n.y), before);
2524    }
2525    #[test]
2526    fn fhr_matches_rk4_candidate() {
2527        let mut n = FitzHughRinzelNeuron::new();
2528        n.v = -1.0;
2529        n.w = 0.2;
2530        n.y = 0.1;
2531        let expected = n.rk4_candidate(0.5).unwrap();
2532        assert_eq!(n.step(0.5), 0);
2533        assert!((n.v - expected.0).abs() < 1.0e-15);
2534        assert!((n.w - expected.1).abs() < 1.0e-15);
2535        assert!((n.y - expected.2).abs() < 1.0e-15);
2536    }
2537    #[test]
2538    fn fhr_overflow_candidate_preserves_state() {
2539        let mut n = FitzHughRinzelNeuron {
2540            v: 1.0e155,
2541            ..Default::default()
2542        };
2543        let before = (n.v, n.w, n.y);
2544        assert_eq!(n.step(0.5), 0);
2545        assert_eq!((n.v, n.w, n.y), before);
2546    }
2547    #[test]
2548    fn fhr_negative_no_crash() {
2549        let mut n = FitzHughRinzelNeuron::new();
2550        for _ in 0..500 {
2551            n.step(-5.0);
2552        }
2553        assert!(n.v.is_finite());
2554    }
2555    #[test]
2556    fn fhr_slow_y_variable() {
2557        let mut n = FitzHughRinzelNeuron::new();
2558        let y0 = n.y;
2559        for _ in 0..2000 {
2560            n.step(1.0);
2561        }
2562        assert!((n.y - y0).abs() > 1e-6, "slow y should evolve in 3D model");
2563    }
2564
2565    // -- McKean --
2566    #[test]
2567    fn mckean_reset_clears_state() {
2568        let mut n = McKeanNeuron::new();
2569        for _ in 0..500 {
2570            n.step(0.5);
2571        }
2572        n.reset();
2573        assert!((n.v - 0.0).abs() < 1e-10);
2574    }
2575    #[test]
2576    fn mckean_bounded() {
2577        let mut n = McKeanNeuron::new();
2578        for _ in 0..2000 {
2579            n.step(50.0);
2580        }
2581        assert!(n.v.is_finite());
2582    }
2583    #[test]
2584    fn mckean_matches_rk4_candidate() {
2585        fn f(v: f64, a: f64) -> f64 {
2586            let half_a = a / 2.0;
2587            let mid = (1.0 + a) / 2.0;
2588            if v < half_a {
2589                -v
2590            } else if v < mid {
2591                v - a
2592            } else {
2593                1.0 - v
2594            }
2595        }
2596        fn rhs(n: &McKeanNeuron, v: f64, w: f64, current: f64) -> (f64, f64) {
2597            (f(v, n.a) - w + current, n.epsilon * (v - n.gamma * w))
2598        }
2599
2600        let mut n = McKeanNeuron {
2601            v: 0.2,
2602            w: -0.1,
2603            ..Default::default()
2604        };
2605        let current = 0.5;
2606        let v0 = n.v;
2607        let w0 = n.w;
2608        let dt = n.dt;
2609        let k1 = rhs(&n, v0, w0, current);
2610        let k2 = rhs(&n, v0 + 0.5 * dt * k1.0, w0 + 0.5 * dt * k1.1, current);
2611        let k3 = rhs(&n, v0 + 0.5 * dt * k2.0, w0 + 0.5 * dt * k2.1, current);
2612        let k4 = rhs(&n, v0 + dt * k3.0, w0 + dt * k3.1, current);
2613        let expected_v = v0 + dt * (k1.0 + 2.0 * k2.0 + 2.0 * k3.0 + k4.0) / 6.0;
2614        let expected_w = w0 + dt * (k1.1 + 2.0 * k2.1 + 2.0 * k3.1 + k4.1) / 6.0;
2615
2616        assert_eq!(n.step(current), 0);
2617        assert!((n.v - expected_v).abs() < 1e-14);
2618        assert!((n.w - expected_w).abs() < 1e-14);
2619    }
2620    #[test]
2621    fn mckean_nan_no_panic() {
2622        let mut n = McKeanNeuron::new();
2623        let before = (n.v, n.w);
2624        assert_eq!(n.step(f64::NAN), 0);
2625        assert_eq!((n.v, n.w), before);
2626    }
2627    #[test]
2628    fn mckean_overflow_candidate_preserves_state() {
2629        let mut n = McKeanNeuron {
2630            v: 1.0e308,
2631            w: -1.7e308,
2632            ..Default::default()
2633        };
2634        let before = (n.v, n.w);
2635        assert_eq!(n.step(1.7e308), 0);
2636        assert_eq!((n.v, n.w), before);
2637    }
2638    #[test]
2639    fn mckean_negative_no_crash() {
2640        let mut n = McKeanNeuron::new();
2641        for _ in 0..500 {
2642            n.step(-5.0);
2643        }
2644        assert!(n.v.is_finite());
2645    }
2646
2647    // -- TermanWang --
2648    #[test]
2649    fn tw_reset_clears_state() {
2650        let mut n = TermanWangOscillator::new();
2651        for _ in 0..500 {
2652            n.step(0.5);
2653        }
2654        n.reset();
2655        assert!((n.v - (-1.5)).abs() < 1e-10);
2656    }
2657    #[test]
2658    fn tw_bounded() {
2659        let mut n = TermanWangOscillator::new();
2660        for _ in 0..2000 {
2661            n.step(50.0);
2662        }
2663        assert!(n.v.is_finite());
2664    }
2665    #[test]
2666    fn tw_matches_rk4_candidate() {
2667        let mut n = TermanWangOscillator::new();
2668        n.v = -1.2;
2669        n.w = -0.25;
2670        let current = 1.0;
2671        let dt = n.dt;
2672        let rhs = |v: f64, w: f64| {
2673            let f = 3.0 * v - v.powi(3) + 2.0;
2674            let g = n.alpha * (1.0 + (v / n.beta).tanh());
2675            (f - w + current + n.rho, n.epsilon * (g - w))
2676        };
2677        let (k1v, k1w) = rhs(n.v, n.w);
2678        let (k2v, k2w) = rhs(n.v + 0.5 * dt * k1v, n.w + 0.5 * dt * k1w);
2679        let (k3v, k3w) = rhs(n.v + 0.5 * dt * k2v, n.w + 0.5 * dt * k2w);
2680        let (k4v, k4w) = rhs(n.v + dt * k3v, n.w + dt * k3w);
2681        let expected = (
2682            n.v + dt * (k1v + 2.0 * k2v + 2.0 * k3v + k4v) / 6.0,
2683            n.w + dt * (k1w + 2.0 * k2w + 2.0 * k3w + k4w) / 6.0,
2684        );
2685
2686        assert_eq!(n.step(current), 0);
2687        assert!((n.v - expected.0).abs() < 1e-14);
2688        assert!((n.w - expected.1).abs() < 1e-14);
2689    }
2690    #[test]
2691    fn tw_nan_no_panic() {
2692        let mut n = TermanWangOscillator::new();
2693        let before = (n.v, n.w);
2694        assert_eq!(n.step(f64::NAN), 0);
2695        assert_eq!((n.v, n.w), before);
2696    }
2697    #[test]
2698    fn tw_overflow_candidate_preserves_state() {
2699        let mut n = TermanWangOscillator {
2700            v: 1.0e308,
2701            ..Default::default()
2702        };
2703        let before = (n.v, n.w);
2704        assert_eq!(n.step(1.0), 0);
2705        assert_eq!((n.v, n.w), before);
2706    }
2707    #[test]
2708    fn tw_negative_no_crash() {
2709        let mut n = TermanWangOscillator::new();
2710        for _ in 0..500 {
2711            n.step(-5.0);
2712        }
2713        assert!(n.v.is_finite());
2714    }
2715
2716    // -- BendaHerz --
2717    #[test]
2718    fn benda_herz_reset_clears_state() {
2719        let mut n = BendaHerzNeuron::new(42);
2720        for _ in 0..100 {
2721            n.step(20.0);
2722        }
2723        n.reset();
2724        assert!((n.a - 0.0).abs() < 1e-10);
2725    }
2726    #[test]
2727    fn benda_herz_bounded() {
2728        let mut n = BendaHerzNeuron::new(42);
2729        for _ in 0..10000 {
2730            n.step(1e4);
2731        }
2732        assert!(n.a.is_finite());
2733    }
2734    #[test]
2735    fn benda_herz_adaptation() {
2736        let mut n = BendaHerzNeuron::new(42);
2737        for _ in 0..10000 {
2738            n.step(20.0);
2739        }
2740        assert!(n.a > 0.0, "adaptation variable a should increase: {}", n.a);
2741    }
2742    #[test]
2743    fn benda_herz_nan_no_panic() {
2744        BendaHerzNeuron::new(42).step(f64::NAN);
2745    }
2746    #[test]
2747    fn benda_herz_negative_no_crash() {
2748        let mut n = BendaHerzNeuron::new(42);
2749        for _ in 0..500 {
2750            n.step(-10.0);
2751        }
2752        assert!(n.a.is_finite());
2753    }
2754
2755    // -- Alpha --
2756    #[test]
2757    fn alpha_reset_clears_state() {
2758        let mut n = AlphaNeuron::new();
2759        for _ in 0..50 {
2760            n.step(0.5, 0.0);
2761        }
2762        n.reset();
2763        assert!((n.v - 0.0).abs() < 1e-10);
2764    }
2765    #[test]
2766    fn alpha_bounded() {
2767        let mut n = AlphaNeuron::new();
2768        for _ in 0..1000 {
2769            n.step(100.0, 0.0);
2770        }
2771        assert!(n.v.is_finite());
2772    }
2773    #[test]
2774    fn alpha_spike_input_drives() {
2775        let mut n = AlphaNeuron::new();
2776        for _ in 0..100 {
2777            n.step(0.0, 1.0);
2778        }
2779        // Spike input should contribute to synaptic current
2780        assert!(n.v.is_finite());
2781    }
2782    #[test]
2783    fn alpha_nan_no_panic() {
2784        AlphaNeuron::new().step(f64::NAN, 0.0);
2785    }
2786    #[test]
2787    fn alpha_negative_no_crash() {
2788        let mut n = AlphaNeuron::new();
2789        for _ in 0..100 {
2790            n.step(-5.0, 0.0);
2791        }
2792        assert!(n.v.is_finite());
2793    }
2794
2795    // -- COBALIF --
2796    #[test]
2797    fn coba_reset_clears_state() {
2798        let mut n = COBALIFNeuron::new();
2799        for _ in 0..100 {
2800            n.step(500.0, 0.0, 0.0);
2801        }
2802        n.reset();
2803        assert!((n.v - n.e_l).abs() < 1e-10);
2804    }
2805    #[test]
2806    fn coba_bounded() {
2807        let mut n = COBALIFNeuron::new();
2808        for _ in 0..2000 {
2809            n.step(1e5, 0.0, 0.0);
2810        }
2811        assert!(n.v.is_finite());
2812    }
2813    #[test]
2814    fn coba_inhibition_suppresses() {
2815        let mut n_exc = COBALIFNeuron::new();
2816        let mut n_inh = COBALIFNeuron::new();
2817        let t_exc: i32 = (0..2000).map(|_| n_exc.step(500.0, 0.0, 0.0)).sum();
2818        let t_inh: i32 = (0..2000).map(|_| n_inh.step(500.0, 0.0, 5.0)).sum();
2819        assert!(t_inh <= t_exc, "inhibition should reduce spiking");
2820    }
2821    #[test]
2822    fn coba_nan_no_panic() {
2823        COBALIFNeuron::new().step(f64::NAN, 0.0, 0.0);
2824    }
2825    #[test]
2826    fn coba_negative_no_crash() {
2827        let mut n = COBALIFNeuron::new();
2828        for _ in 0..500 {
2829            n.step(-100.0, 0.0, 0.0);
2830        }
2831        assert!(n.v.is_finite());
2832    }
2833
2834    // -- GutkinErmentrout --
2835    #[test]
2836    fn gutkin_reset_clears_state() {
2837        let mut n = GutkinErmentroutNeuron::new();
2838        for _ in 0..500 {
2839            n.step(15.0);
2840        }
2841        n.reset();
2842        assert!((n.v - (-65.0)).abs() < 1e-10);
2843    }
2844    #[test]
2845    fn gutkin_bounded() {
2846        let mut n = GutkinErmentroutNeuron::new();
2847        for _ in 0..2000 {
2848            n.step(1e4);
2849        }
2850        assert!(n.v.is_finite());
2851    }
2852    #[test]
2853    fn gutkin_nan_no_panic() {
2854        GutkinErmentroutNeuron::new().step(f64::NAN);
2855    }
2856    #[test]
2857    fn gutkin_matches_rk4_candidate() {
2858        fn rhs(n: &GutkinErmentroutNeuron, v: f64, n_gate: f64, current: f64) -> (f64, f64) {
2859            let m_inf = 1.0 / (1.0 + (-(v + 20.0) / 15.0).exp());
2860            let n_inf = 1.0 / (1.0 + (-(v + 25.0) / 5.0).exp());
2861            let i_na = n.g_na * m_inf * (v - n.e_na);
2862            let i_k = n.g_k * n_gate * (v - n.e_k);
2863            let i_l = n.g_l * (v - n.e_l);
2864            (-i_na - i_k - i_l + current, n_inf - n_gate)
2865        }
2866
2867        let mut n = GutkinErmentroutNeuron::new();
2868        let current = 5.0;
2869        let v0 = n.v;
2870        let n0 = n.n;
2871        let dt = n.dt;
2872        let (k1_v, k1_n) = rhs(&n, v0, n0, current);
2873        let (k2_v, k2_n) = rhs(&n, v0 + 0.5 * dt * k1_v, n0 + 0.5 * dt * k1_n, current);
2874        let (k3_v, k3_n) = rhs(&n, v0 + 0.5 * dt * k2_v, n0 + 0.5 * dt * k2_n, current);
2875        let (k4_v, k4_n) = rhs(&n, v0 + dt * k3_v, n0 + dt * k3_n, current);
2876        let expected_v = v0 + dt * (k1_v + 2.0 * k2_v + 2.0 * k3_v + k4_v) / 6.0;
2877        let expected_n = n0 + dt * (k1_n + 2.0 * k2_n + 2.0 * k3_n + k4_n) / 6.0;
2878
2879        n.step(current);
2880
2881        assert!((n.v - expected_v).abs() < 1e-12);
2882        assert!((n.n - expected_n).abs() < 1e-12);
2883    }
2884    #[test]
2885    fn gutkin_invalid_candidate_preserves_state() {
2886        let mut n = GutkinErmentroutNeuron {
2887            dt: 100.0,
2888            ..Default::default()
2889        };
2890        let v0 = n.v;
2891        let n0 = n.n;
2892        assert_eq!(n.step(1.0e9), 0);
2893        assert_eq!(n.v, v0);
2894        assert_eq!(n.n, n0);
2895    }
2896    #[test]
2897    fn gutkin_negative_no_crash() {
2898        let mut n = GutkinErmentroutNeuron::new();
2899        for _ in 0..500 {
2900            n.step(-10.0);
2901        }
2902        assert!(n.v.is_finite());
2903    }
2904
2905    // -- WilsonHR --
2906    #[test]
2907    fn wilson_hr_reset_clears_state() {
2908        let mut n = WilsonHRNeuron::new();
2909        for _ in 0..500 {
2910            n.step(0.5);
2911        }
2912        n.reset();
2913        assert!((n.v - (-0.7)).abs() < 1e-10);
2914    }
2915    #[test]
2916    fn wilson_hr_moderate_stable() {
2917        let mut n = WilsonHRNeuron::new();
2918        for _ in 0..2000 {
2919            n.step(1.0);
2920        }
2921        assert!(n.v.is_finite());
2922    }
2923    #[test]
2924    fn wilson_hr_matches_rk4_candidate() {
2925        fn rhs(n: &WilsonHRNeuron, v: f64, r: f64, current: f64) -> (f64, f64) {
2926            (
2927                WilsonHRNeuron::poly(v) - 26.0 * r * (v + 0.92) + current,
2928                (-r + 1.35 * v + 1.03) / n.tau_r,
2929            )
2930        }
2931
2932        let mut n = WilsonHRNeuron {
2933            v: -0.4,
2934            r: 0.08,
2935            ..Default::default()
2936        };
2937        let current = 0.3;
2938        let v0 = n.v;
2939        let r0 = n.r;
2940        let dt = n.dt;
2941        let k1 = rhs(&n, v0, r0, current);
2942        let k2 = rhs(&n, v0 + 0.5 * dt * k1.0, r0 + 0.5 * dt * k1.1, current);
2943        let k3 = rhs(&n, v0 + 0.5 * dt * k2.0, r0 + 0.5 * dt * k2.1, current);
2944        let k4 = rhs(&n, v0 + dt * k3.0, r0 + dt * k3.1, current);
2945        let expected_v = v0 + dt * (k1.0 + 2.0 * k2.0 + 2.0 * k3.0 + k4.0) / 6.0;
2946        let expected_r = r0 + dt * (k1.1 + 2.0 * k2.1 + 2.0 * k3.1 + k4.1) / 6.0;
2947
2948        assert_eq!(n.step(current), 0);
2949        assert!((n.v - expected_v).abs() < 1e-14);
2950        assert!((n.r - expected_r).abs() < 1e-14);
2951    }
2952    #[test]
2953    fn wilson_hr_nan_no_panic() {
2954        let mut n = WilsonHRNeuron::new();
2955        let before = (n.v, n.r);
2956        assert_eq!(n.step(f64::NAN), 0);
2957        assert_eq!((n.v, n.r), before);
2958    }
2959    #[test]
2960    fn wilson_hr_overflow_candidate_preserves_state() {
2961        let mut n = WilsonHRNeuron {
2962            v: 1.0e308,
2963            ..Default::default()
2964        };
2965        let before = (n.v, n.r);
2966        assert_eq!(n.step(0.3), 0);
2967        assert_eq!((n.v, n.r), before);
2968    }
2969    #[test]
2970    fn wilson_hr_negative_no_crash() {
2971        let mut n = WilsonHRNeuron::new();
2972        for _ in 0..500 {
2973            n.step(-5.0);
2974        }
2975        assert!(n.v.is_finite());
2976    }
2977
2978    // -- Chay --
2979    #[test]
2980    fn chay_reset_clears_state() {
2981        let mut n = ChayNeuron::new();
2982        for _ in 0..1000 {
2983            n.step(20.0);
2984        }
2985        n.reset();
2986        assert!((n.v - (-50.0)).abs() < 1e-10);
2987    }
2988    #[test]
2989    fn chay_bounded() {
2990        let mut n = ChayNeuron::new();
2991        for _ in 0..5000 {
2992            n.step(200.0);
2993        }
2994        assert!(n.v.is_finite());
2995    }
2996    #[test]
2997    fn chay_ca_nonneg() {
2998        let mut n = ChayNeuron::new();
2999        for _ in 0..5000 {
3000            n.step(20.0);
3001        }
3002        assert!(n.ca >= 0.0, "Ca²⁺ must be non-negative");
3003    }
3004    #[test]
3005    fn chay_nan_no_panic() {
3006        ChayNeuron::new().step(f64::NAN);
3007    }
3008    #[test]
3009    fn chay_negative_no_crash() {
3010        let mut n = ChayNeuron::new();
3011        for _ in 0..500 {
3012            n.step(-10.0);
3013        }
3014        assert!(n.v.is_finite());
3015    }
3016
3017    // -- ChayKeizer --
3018    #[test]
3019    fn chay_keizer_reset_clears_state() {
3020        let mut n = ChayKeizerNeuron::new();
3021        for _ in 0..1000 {
3022            n.step(10.0);
3023        }
3024        n.reset();
3025        assert!((n.v - (-50.0)).abs() < 1e-10);
3026    }
3027    #[test]
3028    fn chay_keizer_bounded() {
3029        let mut n = ChayKeizerNeuron::new();
3030        for _ in 0..5000 {
3031            n.step(100.0);
3032        }
3033        assert!(n.v.is_finite());
3034    }
3035    #[test]
3036    fn chay_keizer_nan_no_panic() {
3037        ChayKeizerNeuron::new().step(f64::NAN);
3038    }
3039    #[test]
3040    fn chay_keizer_negative_no_crash() {
3041        let mut n = ChayKeizerNeuron::new();
3042        for _ in 0..500 {
3043            n.step(-10.0);
3044        }
3045        assert!(n.v.is_finite());
3046    }
3047
3048    // -- ShermanRinzelKeizer --
3049    #[test]
3050    fn srk_reset_clears_state() {
3051        let mut n = ShermanRinzelKeizerNeuron::new();
3052        for _ in 0..1000 {
3053            n.step(5.0);
3054        }
3055        n.reset();
3056        assert!((n.v - (-50.0)).abs() < 1e-10);
3057    }
3058    #[test]
3059    fn srk_bounded() {
3060        let mut n = ShermanRinzelKeizerNeuron::new();
3061        for _ in 0..5000 {
3062            n.step(100.0);
3063        }
3064        assert!(n.v.is_finite());
3065    }
3066    #[test]
3067    fn srk_nan_no_panic() {
3068        ShermanRinzelKeizerNeuron::new().step(f64::NAN);
3069    }
3070    #[test]
3071    fn srk_negative_no_crash() {
3072        let mut n = ShermanRinzelKeizerNeuron::new();
3073        for _ in 0..500 {
3074            n.step(-5.0);
3075        }
3076        assert!(n.v.is_finite());
3077    }
3078
3079    // -- ButeraRespiratory --
3080    #[test]
3081    fn butera_reset_clears_state() {
3082        let mut n = ButeraRespiratoryNeuron::new();
3083        for _ in 0..1000 {
3084            n.step(50.0);
3085        }
3086        n.reset();
3087        assert!((n.v - (-50.0)).abs() < 1e-10);
3088    }
3089    #[test]
3090    fn butera_bounded() {
3091        let mut n = ButeraRespiratoryNeuron::new();
3092        for _ in 0..5000 {
3093            n.step(500.0);
3094        }
3095        assert!(n.v.is_finite());
3096    }
3097    #[test]
3098    fn butera_nan_no_panic() {
3099        ButeraRespiratoryNeuron::new().step(f64::NAN);
3100    }
3101    #[test]
3102    fn butera_nan_preserves_state() {
3103        let mut n = ButeraRespiratoryNeuron::new();
3104        let before = (n.v, n.n, n.h_nap);
3105        assert_eq!(n.step(f64::NAN), 0);
3106        assert_eq!((n.v, n.n, n.h_nap), before);
3107    }
3108    #[test]
3109    fn butera_negative_no_crash() {
3110        let mut n = ButeraRespiratoryNeuron::new();
3111        for _ in 0..500 {
3112            n.step(-20.0);
3113        }
3114        assert!(n.v.is_finite());
3115    }
3116
3117    // -- EPropALIF --
3118    #[test]
3119    fn eprop_reset_clears_state() {
3120        let mut n = EPropALIFNeuron::default();
3121        for _ in 0..50 {
3122            n.step(0.5);
3123        }
3124        n.reset();
3125        assert!((n.v - 0.0).abs() < 1e-10);
3126    }
3127    #[test]
3128    fn eprop_bounded() {
3129        let mut n = EPropALIFNeuron::default();
3130        for _ in 0..1000 {
3131            n.step(100.0);
3132        }
3133        assert!(n.v.is_finite());
3134    }
3135    #[test]
3136    fn eprop_adaptation() {
3137        let mut n = EPropALIFNeuron::default();
3138        for _ in 0..50 {
3139            n.step(0.5);
3140        }
3141        // a (adaptation) should have increased after spikes
3142        assert!(n.a.is_finite());
3143    }
3144    #[test]
3145    fn eprop_nan_no_panic() {
3146        EPropALIFNeuron::default().step(f64::NAN);
3147    }
3148
3149    // -- SuperSpike --
3150    #[test]
3151    fn superspike_reset_clears_state() {
3152        let mut n = SuperSpikeNeuron::default();
3153        for _ in 0..50 {
3154            n.step(0.5);
3155        }
3156        n.reset();
3157        assert!((n.v - 0.0).abs() < 1e-10);
3158    }
3159    #[test]
3160    fn superspike_bounded() {
3161        let mut n = SuperSpikeNeuron::default();
3162        for _ in 0..1000 {
3163            n.step(100.0);
3164        }
3165        assert!(n.v.is_finite());
3166    }
3167    #[test]
3168    fn superspike_trace_evolves() {
3169        let mut n = SuperSpikeNeuron::default();
3170        for _ in 0..50 {
3171            n.step(0.5);
3172        }
3173        assert!(n.trace.is_finite());
3174    }
3175    #[test]
3176    fn superspike_nan_no_panic() {
3177        SuperSpikeNeuron::default().step(f64::NAN);
3178    }
3179
3180    // -- LearnableNeuron --
3181    #[test]
3182    fn lnm_reset_clears_state() {
3183        let mut n = LearnableNeuronModel::new();
3184        for _ in 0..50 {
3185            n.step(2.0);
3186        }
3187        n.reset();
3188        assert!((n.v - 0.0).abs() < 1e-10);
3189    }
3190    #[test]
3191    fn lnm_bounded() {
3192        let mut n = LearnableNeuronModel::new();
3193        for _ in 0..1000 {
3194            n.step(100.0);
3195        }
3196        assert!(n.v.is_finite());
3197    }
3198    #[test]
3199    fn lnm_nan_no_panic() {
3200        LearnableNeuronModel::new().step(f64::NAN);
3201    }
3202
3203    // -- Pernarowski --
3204    #[test]
3205    fn pernarowski_reset_clears_state() {
3206        let mut n = PernarowskiNeuron::new();
3207        for _ in 0..500 {
3208            n.step(1.0);
3209        }
3210        n.reset();
3211        assert!((n.v - (-1.0)).abs() < 1e-10);
3212    }
3213    #[test]
3214    fn pernarowski_bounded() {
3215        let mut n = PernarowskiNeuron::new();
3216        for _ in 0..2000 {
3217            n.step(50.0);
3218        }
3219        assert!(n.v.is_finite());
3220    }
3221    #[test]
3222    fn pernarowski_slow_z() {
3223        let mut n = PernarowskiNeuron::new();
3224        let z0 = n.z;
3225        for _ in 0..2000 {
3226            n.step(1.0);
3227        }
3228        assert!((n.z - z0).abs() > 1e-6, "slow z should evolve");
3229    }
3230    #[test]
3231    fn pernarowski_matches_rk4_candidate() {
3232        let mut n = PernarowskiNeuron::new();
3233        n.v = -0.8;
3234        n.w = 0.2;
3235        n.z = -0.1;
3236        let current = 0.5;
3237        let dt = n.dt;
3238        let rhs = |v: f64, w: f64, z: f64| {
3239            (
3240                v - v.powi(3) / 3.0 - w - z + current,
3241                n.eps1 * (v - n.gamma * w + n.alpha),
3242                n.eps2 * (n.beta * (v + 0.7) - z),
3243            )
3244        };
3245        let (k1v, k1w, k1z) = rhs(n.v, n.w, n.z);
3246        let (k2v, k2w, k2z) = rhs(
3247            n.v + 0.5 * dt * k1v,
3248            n.w + 0.5 * dt * k1w,
3249            n.z + 0.5 * dt * k1z,
3250        );
3251        let (k3v, k3w, k3z) = rhs(
3252            n.v + 0.5 * dt * k2v,
3253            n.w + 0.5 * dt * k2w,
3254            n.z + 0.5 * dt * k2z,
3255        );
3256        let (k4v, k4w, k4z) = rhs(n.v + dt * k3v, n.w + dt * k3w, n.z + dt * k3z);
3257        let expected = (
3258            n.v + dt * (k1v + 2.0 * k2v + 2.0 * k3v + k4v) / 6.0,
3259            n.w + dt * (k1w + 2.0 * k2w + 2.0 * k3w + k4w) / 6.0,
3260            n.z + dt * (k1z + 2.0 * k2z + 2.0 * k3z + k4z) / 6.0,
3261        );
3262
3263        assert_eq!(n.step(current), 0);
3264        assert!((n.v - expected.0).abs() < 1e-14);
3265        assert!((n.w - expected.1).abs() < 1e-14);
3266        assert!((n.z - expected.2).abs() < 1e-14);
3267    }
3268    #[test]
3269    fn pernarowski_invalid_input_preserves_state() {
3270        let mut n = PernarowskiNeuron::new();
3271        let before = (n.v, n.w, n.z);
3272        assert_eq!(n.step(f64::NAN), 0);
3273        assert_eq!((n.v, n.w, n.z), before);
3274    }
3275    #[test]
3276    fn pernarowski_overflow_candidate_preserves_state() {
3277        let mut n = PernarowskiNeuron::new();
3278        n.v = 1.0e160;
3279        let before = (n.v, n.w, n.z);
3280        assert_eq!(n.step(0.5), 0);
3281        assert_eq!((n.v, n.w, n.z), before);
3282    }
3283    #[test]
3284    fn pernarowski_nan_no_panic() {
3285        PernarowskiNeuron::new().step(f64::NAN);
3286    }
3287    #[test]
3288    fn pernarowski_negative_no_crash() {
3289        let mut n = PernarowskiNeuron::new();
3290        for _ in 0..500 {
3291            n.step(-5.0);
3292        }
3293        assert!(n.v.is_finite());
3294    }
3295
3296    // -- BrunelWang tests --
3297
3298    #[test]
3299    fn brunel_wang_fires_with_ampa_ext() {
3300        let mut n = BrunelWangNeuron::new();
3301        let mut spikes = 0;
3302        for _ in 0..5000 {
3303            spikes += n.step(5.0);
3304        }
3305        assert!(
3306            spikes > 0,
3307            "Must fire with external AMPA drive, got {spikes}"
3308        );
3309    }
3310
3311    #[test]
3312    fn brunel_wang_silent_without_input() {
3313        let mut n = BrunelWangNeuron::new();
3314        let spikes: i32 = (0..10_000).map(|_| n.step(0.0)).sum();
3315        assert_eq!(spikes, 0, "Must be silent without input");
3316    }
3317
3318    #[test]
3319    fn brunel_wang_nmda_mg_block() {
3320        let n = BrunelWangNeuron::new();
3321        // At resting potential (-70 mV), Mg²⁺ block should be strong
3322        let block_rest = n.nmda_mg_block(-70.0);
3323        // At depolarised potential (0 mV), block should be weak
3324        let block_depol = n.nmda_mg_block(0.0);
3325        assert!(
3326            block_depol > block_rest,
3327            "Mg²⁺ block should weaken with depolarisation: rest={block_rest:.3} depol={block_depol:.3}"
3328        );
3329        // At -70 mV, block factor should be small (< 0.1)
3330        assert!(
3331            block_rest < 0.1,
3332            "Block at -70 mV should be < 0.1, got {block_rest:.3}"
3333        );
3334        // At 0 mV, block factor should be close to 1
3335        assert!(
3336            block_depol > 0.5,
3337            "Block at 0 mV should be > 0.5, got {block_depol:.3}"
3338        );
3339    }
3340
3341    #[test]
3342    fn brunel_wang_full_step_nmda_drive() {
3343        // NMDA recurrent input should drive firing via positive feedback
3344        let mut n = BrunelWangNeuron::new();
3345        n.v = -55.0; // near threshold, Mg²⁺ block partially relieved
3346        let mut spikes = 0;
3347        for _ in 0..1000 {
3348            spikes += n.step_full(0.0, 0.0, 1.0, 0.0); // pure NMDA drive
3349        }
3350        assert!(
3351            spikes > 0,
3352            "NMDA drive at depolarised V should cause spikes"
3353        );
3354    }
3355
3356    #[test]
3357    fn brunel_wang_gaba_suppresses() {
3358        let mut with_gaba = BrunelWangNeuron::new();
3359        let mut no_gaba = BrunelWangNeuron::new();
3360        let mut spikes_gaba = 0;
3361        let mut spikes_no = 0;
3362        for _ in 0..5000 {
3363            spikes_gaba += with_gaba.step_full(3.0, 0.0, 0.0, 1.0); // GABA on
3364            spikes_no += no_gaba.step_full(3.0, 0.0, 0.0, 0.0);
3365        }
3366        assert!(
3367            spikes_no >= spikes_gaba,
3368            "GABA should suppress: no_gaba={spikes_no}, with_gaba={spikes_gaba}"
3369        );
3370    }
3371
3372    #[test]
3373    fn brunel_wang_refractory() {
3374        let mut n = BrunelWangNeuron::new();
3375        // Drive to spike
3376        while n.step(10.0) == 0 {}
3377        // Immediately after spike, should be in refractory
3378        assert!(n.ref_remaining > 0.0, "Should be refractory after spike");
3379        // Should not spike during refractory
3380        assert_eq!(
3381            n.step(100.0),
3382            0,
3383            "Should not spike during refractory period"
3384        );
3385    }
3386
3387    #[test]
3388    fn brunel_wang_reset() {
3389        let mut n = BrunelWangNeuron::new();
3390        for _ in 0..1000 {
3391            n.step(5.0);
3392        }
3393        n.reset();
3394        assert_eq!(n.v, n.v_rest);
3395        assert_eq!(n.ref_remaining, 0.0);
3396    }
3397
3398    #[test]
3399    fn brunel_wang_voltage_bounded() {
3400        let mut n = BrunelWangNeuron::new();
3401        for _ in 0..10_000 {
3402            n.step(100.0);
3403        }
3404        assert!(n.v.is_finite(), "V must stay finite");
3405        // V should stay near v_reset during sustained spiking (refractory resets)
3406        assert!(
3407            n.v <= n.v_threshold,
3408            "V should be at or below threshold (reset clamp)"
3409        );
3410    }
3411
3412    #[test]
3413    fn brunel_wang_nan_input() {
3414        let mut n = BrunelWangNeuron::new();
3415        n.step(f64::NAN);
3416        // NaN input → V becomes NaN. Check we don't panic.
3417    }
3418
3419    #[test]
3420    fn brunel_wang_performance() {
3421        let start = std::time::Instant::now();
3422        let mut n = BrunelWangNeuron::new();
3423        for _ in 0..10_000 {
3424            std::hint::black_box(n.step(3.0));
3425        }
3426        let elapsed = start.elapsed();
3427        assert!(
3428            elapsed.as_millis() < 50,
3429            "10k steps in <50ms, took {}ms",
3430            elapsed.as_millis()
3431        );
3432    }
3433}