Skip to main content

sc_neurocore_engine/neurons/
biophysical.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 — Hodgkin-Huxley-type conductance-based neuron models
8
9//! Hodgkin-Huxley-type conductance-based neuron models.
10
11use rand::{RngExt, SeedableRng};
12use rand_xoshiro::Xoshiro256PlusPlus;
13
14// ── Helper: safe alpha/beta kinetics avoiding division-by-zero ──
15
16pub fn safe_rate(
17    number_factor: f64,
18    v_offset: f64,
19    v: f64,
20    denom_scale: f64,
21    fallback: f64,
22) -> f64 {
23    let d = v + v_offset;
24    if d.abs() < 1e-7 {
25        fallback
26    } else {
27        number_factor * d / (1.0 - (-d / denom_scale).exp())
28    }
29}
30
31/// Hodgkin-Huxley 1952 — 4-ODE ion channel model.
32#[derive(Clone, Debug)]
33pub struct HodgkinHuxleyNeuron {
34    pub v: f64,
35    pub m: f64,
36    pub h: f64,
37    pub n: f64,
38    pub c_m: f64,
39    pub g_na: f64,
40    pub g_k: f64,
41    pub g_l: f64,
42    pub e_na: f64,
43    pub e_k: f64,
44    pub e_l: f64,
45    pub dt: f64,
46    pub v_threshold: f64,
47}
48
49impl HodgkinHuxleyNeuron {
50    pub fn new() -> Self {
51        Self {
52            v: -65.0,
53            m: 0.05,
54            h: 0.6,
55            n: 0.32,
56            c_m: 1.0,
57            g_na: 120.0,
58            g_k: 36.0,
59            g_l: 0.3,
60            e_na: 50.0,
61            e_k: -77.0,
62            e_l: -54.4,
63            dt: 0.01,
64            v_threshold: 0.0,
65        }
66    }
67    pub fn step(&mut self, current: f64) -> i32 {
68        let v_prev = self.v;
69        let steps = (1.0 / self.dt) as usize;
70        for _ in 0..steps {
71            let am = safe_rate(0.1, 40.0, self.v, 10.0, 1.0);
72            let bm = 4.0 * (-(self.v + 65.0) / 18.0).exp();
73            let ah = 0.07 * (-(self.v + 65.0) / 20.0).exp();
74            let bh = 1.0 / (1.0 + (-(self.v + 35.0) / 10.0).exp());
75            let an = safe_rate(0.01, 55.0, self.v, 10.0, 0.1);
76            let bn = 0.125 * (-(self.v + 65.0) / 80.0).exp();
77            self.m += (am * (1.0 - self.m) - bm * self.m) * self.dt;
78            self.h += (ah * (1.0 - self.h) - bh * self.h) * self.dt;
79            self.n += (an * (1.0 - self.n) - bn * self.n) * self.dt;
80            let i_na = self.g_na * self.m.powi(3) * self.h * (self.v - self.e_na);
81            let i_k = self.g_k * self.n.powi(4) * (self.v - self.e_k);
82            let i_l = self.g_l * (self.v - self.e_l);
83            self.v += (-i_na - i_k - i_l + current) / self.c_m * self.dt;
84        }
85        if self.v >= self.v_threshold && v_prev < self.v_threshold {
86            1
87        } else {
88            0
89        }
90    }
91    pub fn reset(&mut self) {
92        self.v = -65.0;
93        self.m = 0.05;
94        self.h = 0.6;
95        self.n = 0.32;
96    }
97}
98impl Default for HodgkinHuxleyNeuron {
99    fn default() -> Self {
100        Self::new()
101    }
102}
103
104/// Traub-Miles — hippocampal CA3 pyramidal neuron with M-current.
105///
106/// Full Traub et al. 1991 model with Na, K_dr, leak, plus
107/// M-current (Kv7/KCNQ) for spike frequency adaptation.
108/// The M-current is the slow K⁺ conductance responsible for:
109/// - Spike frequency adaptation (SFA)
110/// - Theta-frequency resonance (~4-8 Hz)
111/// - Subthreshold membrane potential oscillations
112///
113/// IM = g_M * w * (V - E_K)
114/// dw/dt = (w_inf - w) / tau_w
115/// w_inf = 1 / (1 + exp(-(V + 35) / 10))
116/// tau_w = 100 / (3.3 * exp((V+35)/20) + exp(-(V+35)/20))
117///
118/// Traub et al., J Neurophysiol 66:635, 1991.
119/// Yamada et al., Meth Neuronal Model (Koch & Segev), 1989 (M-current kinetics).
120#[derive(Clone, Debug)]
121pub struct TraubMilesNeuron {
122    pub v: f64,
123    pub m: f64,
124    pub h: f64,
125    pub n: f64,
126    pub g_na: f64,
127    pub g_k: f64,
128    pub g_l: f64,
129    pub e_na: f64,
130    pub e_k: f64,
131    pub e_l: f64,
132    pub dt: f64,
133    pub v_threshold: f64,
134}
135
136impl TraubMilesNeuron {
137    pub fn new() -> Self {
138        Self {
139            v: -67.0,
140            m: 0.05,
141            h: 0.6,
142            n: 0.3,
143            g_na: 100.0,
144            g_k: 80.0,
145            g_l: 0.1,
146            e_na: 50.0,
147            e_k: -100.0,
148            e_l: -67.0,
149            dt: 0.01,
150            v_threshold: -20.0,
151        }
152    }
153    pub fn step(&mut self, current: f64) -> i32 {
154        let v_prev = self.v;
155        for _ in 0..10 {
156            let am = safe_rate(0.32, 54.0, self.v, 4.0, 8.0);
157            let bm = safe_rate(-0.28, 27.0, self.v, -5.0, 5.6);
158            let ah = 0.128 * (-(self.v + 50.0) / 18.0).exp();
159            let bh = 4.0 / (1.0 + (-(self.v + 27.0) / 5.0).exp());
160            let an = safe_rate(0.032, 52.0, self.v, 5.0, 0.32);
161            let bn = 0.5 * (-(self.v + 57.0) / 40.0).exp();
162
163            self.m += (am * (1.0 - self.m) - bm * self.m) * self.dt;
164            self.h += (ah * (1.0 - self.h) - bh * self.h) * self.dt;
165            self.n += (an * (1.0 - self.n) - bn * self.n) * self.dt;
166
167            let i_na = self.g_na * self.m.powi(3) * self.h * (self.v - self.e_na);
168            let i_k = self.g_k * self.n.powi(4) * (self.v - self.e_k);
169            let i_l = self.g_l * (self.v - self.e_l);
170            self.v += (-i_na - i_k - i_l + current) * self.dt;
171        }
172        if self.v >= self.v_threshold && v_prev < self.v_threshold {
173            1
174        } else {
175            0
176        }
177    }
178    pub fn reset(&mut self) {
179        *self = Self::new();
180    }
181}
182impl Default for TraubMilesNeuron {
183    fn default() -> Self {
184        Self::new()
185    }
186}
187
188/// Wang-Buzsaki — fast-spiking GABAergic interneuron. Wang & Buzsáki 1996.
189#[derive(Clone, Debug)]
190pub struct WangBuzsakiNeuron {
191    pub v: f64,
192    pub h: f64,
193    pub n: f64,
194    pub g_na: f64,
195    pub g_k: f64,
196    pub g_l: f64,
197    pub e_na: f64,
198    pub e_k: f64,
199    pub e_l: f64,
200    pub c_m: f64,
201    pub phi: f64,
202    pub dt: f64,
203    pub v_threshold: f64,
204}
205
206impl WangBuzsakiNeuron {
207    pub fn new() -> Self {
208        Self {
209            v: -65.0,
210            h: 0.8,
211            n: 0.1,
212            g_na: 35.0,
213            g_k: 9.0,
214            g_l: 0.1,
215            e_na: 55.0,
216            e_k: -90.0,
217            e_l: -65.0,
218            c_m: 1.0,
219            phi: 5.0,
220            dt: 0.01,
221            v_threshold: -20.0,
222        }
223    }
224    pub fn step(&mut self, current: f64) -> i32 {
225        let v_prev = self.v;
226        let n_sub = (0.5 / self.dt.max(0.001)) as usize;
227        for _ in 0..n_sub {
228            let am = safe_rate(0.1, 35.0, self.v, 10.0, 1.0);
229            let bm = 4.0 * (-(self.v + 60.0) / 18.0).exp();
230            let m_inf = am / (am + bm);
231            let ah = 0.07 * (-(self.v + 58.0) / 20.0).exp();
232            let bh = 1.0 / (1.0 + (-(self.v + 28.0) / 10.0).exp());
233            let an = safe_rate(0.01, 34.0, self.v, 10.0, 0.1);
234            let bn = 0.125 * (-(self.v + 44.0) / 80.0).exp();
235            self.h += self.phi * (ah * (1.0 - self.h) - bh * self.h) * self.dt;
236            self.n += self.phi * (an * (1.0 - self.n) - bn * self.n) * self.dt;
237            let i_na = self.g_na * m_inf.powi(3) * self.h * (self.v - self.e_na);
238            let i_k = self.g_k * self.n.powi(4) * (self.v - self.e_k);
239            let i_l = self.g_l * (self.v - self.e_l);
240            self.v += (-i_na - i_k - i_l + current) / self.c_m * self.dt;
241        }
242        if self.v >= self.v_threshold && v_prev < self.v_threshold {
243            1
244        } else {
245            0
246        }
247    }
248    pub fn reset(&mut self) {
249        self.v = -65.0;
250        self.h = 0.8;
251        self.n = 0.1;
252    }
253}
254impl Default for WangBuzsakiNeuron {
255    fn default() -> Self {
256        Self::new()
257    }
258}
259
260/// Connor-Stevens — A-type K current for delay tuning. Connor et al. 1977.
261#[derive(Clone, Debug)]
262pub struct ConnorStevensNeuron {
263    pub v: f64,
264    pub m: f64,
265    pub h: f64,
266    pub n: f64,
267    pub a: f64,
268    pub b: f64,
269    pub g_na: f64,
270    pub g_k: f64,
271    pub g_a: f64,
272    pub g_l: f64,
273    pub e_na: f64,
274    pub e_k: f64,
275    pub e_a: f64,
276    pub e_l: f64,
277    pub c_m: f64,
278    pub dt: f64,
279    pub v_threshold: f64,
280}
281
282impl ConnorStevensNeuron {
283    pub fn new() -> Self {
284        Self {
285            v: -68.0,
286            m: 0.01,
287            h: 0.99,
288            n: 0.1,
289            a: 0.5,
290            b: 0.1,
291            g_na: 120.0,
292            g_k: 20.0,
293            g_a: 47.7,
294            g_l: 0.3,
295            e_na: 55.0,
296            e_k: -72.0,
297            e_a: -75.0,
298            e_l: -17.0,
299            c_m: 1.0,
300            dt: 0.01,
301            v_threshold: 0.0,
302        }
303    }
304    pub fn step(&mut self, current: f64) -> i32 {
305        let v_prev = self.v;
306        // Connor, Walter & McKown 1977: modified HH rates for Type-I excitability
307        for _ in 0..100 {
308            let am = safe_rate(0.38, 29.7, self.v, 10.0, 3.8);
309            let bm = 15.2 * (-(self.v + 54.7) / 18.0).exp();
310            let ah = 0.266 * (-(self.v + 48.0) / 20.0).exp();
311            let bh = 3.8 / (1.0 + (-(self.v + 18.0) / 10.0).exp());
312            let an = safe_rate(0.02, 45.7, self.v, 10.0, 0.2);
313            let bn = 0.25 * (-(self.v + 55.7) / 80.0).exp();
314            let a_inf = (0.0761 * ((self.v + 94.22) / 31.84).exp()
315                / (1.0 + ((self.v + 1.17) / 28.93).exp()))
316            .powf(1.0 / 3.0);
317            let b_inf = 1.0 / (1.0 + ((self.v + 53.3) / 14.54).exp()).powf(4.0);
318            let tau_a = 0.3632 + 1.158 / (1.0 + ((self.v + 55.96) / 20.12).exp());
319            let tau_b = 1.24 + 2.678 / (1.0 + ((self.v + 50.0) / 16.027).exp());
320            self.m += (am * (1.0 - self.m) - bm * self.m) * self.dt;
321            self.h += (ah * (1.0 - self.h) - bh * self.h) * self.dt;
322            self.n += (an * (1.0 - self.n) - bn * self.n) * self.dt;
323            self.a += (a_inf - self.a) / tau_a * self.dt;
324            self.b += (b_inf - self.b) / tau_b * self.dt;
325            let i_na = self.g_na * self.m.powi(3) * self.h * (self.v - self.e_na);
326            let i_k = self.g_k * self.n.powi(4) * (self.v - self.e_k);
327            let i_a = self.g_a * self.a.powi(3) * self.b * (self.v - self.e_a);
328            let i_l = self.g_l * (self.v - self.e_l);
329            self.v += (-i_na - i_k - i_a - i_l + current) / self.c_m * self.dt;
330        }
331        if self.v >= self.v_threshold && v_prev < self.v_threshold {
332            1
333        } else {
334            0
335        }
336    }
337    pub fn reset(&mut self) {
338        self.v = -68.0;
339        self.m = 0.01;
340        self.h = 0.99;
341        self.n = 0.1;
342        self.a = 0.5;
343        self.b = 0.1;
344    }
345}
346impl Default for ConnorStevensNeuron {
347    fn default() -> Self {
348        Self::new()
349    }
350}
351
352/// Destexhe thalamocortical relay neuron with T-current. Destexhe et al. 1993.
353#[derive(Clone, Debug)]
354pub struct DestexheThalamicNeuron {
355    pub v: f64,
356    pub h_na: f64,
357    pub n_k: f64,
358    pub m_t: f64,
359    pub h_t: f64,
360    pub g_na: f64,
361    pub g_k: f64,
362    pub g_t: f64,
363    pub g_l: f64,
364    pub e_na: f64,
365    pub e_k: f64,
366    pub e_ca: f64,
367    pub e_l: f64,
368    pub dt: f64,
369    pub v_threshold: f64,
370}
371
372impl DestexheThalamicNeuron {
373    pub fn new() -> Self {
374        Self {
375            v: -65.0,
376            h_na: 0.6,
377            n_k: 0.3,
378            m_t: 0.0,
379            h_t: 1.0,
380            g_na: 100.0,
381            g_k: 10.0,
382            g_t: 2.0,
383            g_l: 0.05,
384            e_na: 50.0,
385            e_k: -90.0,
386            e_ca: 120.0,
387            e_l: -70.0,
388            dt: 0.02,
389            v_threshold: -20.0,
390        }
391    }
392    pub fn step(&mut self, current: f64) -> i32 {
393        let v_prev = self.v;
394        for _ in 0..5 {
395            let m_na = 1.0 / (1.0 + (-(self.v + 37.0) / 7.0).exp());
396            let h_na_inf = 1.0 / (1.0 + ((self.v + 41.0) / 4.0).exp());
397            let n_inf = 1.0 / (1.0 + (-(self.v + 25.0) / 12.0).exp());
398            let m_t_inf = 1.0 / (1.0 + (-(self.v + 57.0) / 6.5).exp());
399            let h_t_inf = 1.0 / (1.0 + ((self.v + 81.0) / 4.0).exp());
400            // Voltage-dependent time constants (Destexhe 1993)
401            let tau_h_na = (1.0
402                / (0.128 * (-(self.v + 46.0) / 18.0).exp()
403                    + 4.0 / (1.0 + (-(self.v + 23.0) / 5.0).exp())))
404            .max(0.1);
405            let tau_n_k = (1.0 / (0.032 * 5.0 + 0.5 * (-(self.v + 40.0) / 40.0).exp())).max(0.1);
406            let tau_h_t = if self.v < -81.0 {
407                (30.8
408                    + 211.4 * ((self.v + 115.2) / 5.0).exp()
409                        / (1.0 + ((self.v + 86.0) / 3.2).exp()))
410                .max(0.1)
411            } else {
412                10.0
413            };
414            self.h_na += (h_na_inf - self.h_na) / tau_h_na * self.dt;
415            self.n_k += (n_inf - self.n_k) / tau_n_k * self.dt;
416            self.m_t = m_t_inf; // instantaneous (no ODE)
417            self.h_t += (h_t_inf - self.h_t) / tau_h_t * self.dt;
418            let i_na = self.g_na * m_na.powi(3) * self.h_na * (self.v - self.e_na);
419            let i_k = self.g_k * self.n_k.powi(4) * (self.v - self.e_k);
420            let i_t = self.g_t * self.m_t.powi(2) * self.h_t * (self.v - self.e_ca);
421            let i_l = self.g_l * (self.v - self.e_l);
422            self.v += (-i_na - i_k - i_t - i_l + current) * self.dt;
423        }
424        if self.v >= self.v_threshold && v_prev < self.v_threshold {
425            1
426        } else {
427            0
428        }
429    }
430    pub fn reset(&mut self) {
431        self.v = -65.0;
432        self.h_na = 0.6;
433        self.n_k = 0.3;
434        self.m_t = 0.0;
435        self.h_t = 1.0;
436    }
437}
438impl Default for DestexheThalamicNeuron {
439    fn default() -> Self {
440        Self::new()
441    }
442}
443
444/// Huber-Braun — temperature-sensitive cold receptor. Braun et al. 1998.
445#[derive(Clone, Debug)]
446pub struct HuberBraunNeuron {
447    pub v: f64,
448    pub a_sd: f64,
449    pub a_sr: f64,
450    pub g_sd: f64,
451    pub g_sr: f64,
452    pub g_l: f64,
453    pub e_sd: f64,
454    pub e_sr: f64,
455    pub e_l: f64,
456    pub tau_sd: f64,
457    pub tau_sr: f64,
458    pub eta: f64,
459    pub dt: f64,
460    pub v_threshold: f64,
461}
462
463impl HuberBraunNeuron {
464    pub fn new() -> Self {
465        Self {
466            v: -50.0,
467            a_sd: 0.0,
468            a_sr: 0.0,
469            g_sd: 1.5,
470            g_sr: 0.4,
471            g_l: 0.1,
472            e_sd: 50.0,
473            e_sr: -90.0,
474            e_l: -60.0,
475            tau_sd: 10.0,
476            tau_sr: 20.0,
477            eta: 0.012,
478            dt: 0.1,
479            v_threshold: -20.0,
480        }
481    }
482    pub fn step(&mut self, current: f64) -> i32 {
483        let v_prev = self.v;
484        // Braun, Huber et al. 1998: SD V1/2 = -40 mV, slope = 6
485        let sd_inf = 1.0 / (1.0 + (-(self.v + 40.0) / 6.0).exp());
486        let sr_inf = 1.0 / (1.0 + ((self.v + 40.0) / 6.0).exp());
487        self.a_sd += (sd_inf - self.a_sd) / self.tau_sd * self.dt;
488        self.a_sr += (sr_inf - self.a_sr) / self.tau_sr * self.dt;
489        let i_sd = self.g_sd * self.a_sd * (self.v - self.e_sd);
490        let i_sr = self.g_sr * self.a_sr * (self.v - self.e_sr);
491        let i_l = self.g_l * (self.v - self.e_l);
492        self.v += (-i_sd - i_sr - i_l + current) * self.dt;
493        if self.v >= self.v_threshold && v_prev < self.v_threshold {
494            1
495        } else {
496            0
497        }
498    }
499    pub fn reset(&mut self) {
500        self.v = -50.0;
501        self.a_sd = 0.0;
502        self.a_sr = 0.0;
503    }
504}
505impl Default for HuberBraunNeuron {
506    fn default() -> Self {
507        Self::new()
508    }
509}
510
511/// Golomb fast-spiking interneuron with Kv3. Golomb et al. 2007.
512#[derive(Clone, Debug)]
513pub struct GolombFSNeuron {
514    pub v: f64,
515    pub h: f64,
516    pub n: f64,
517    pub p: f64,
518    pub g_na: f64,
519    pub g_k: f64,
520    pub g_kv3: f64,
521    pub g_l: f64,
522    pub e_na: f64,
523    pub e_k: f64,
524    pub e_l: f64,
525    pub dt: f64,
526    pub v_threshold: f64,
527}
528
529impl GolombFSNeuron {
530    pub fn new() -> Self {
531        Self {
532            v: -65.0,
533            h: 0.9,
534            n: 0.1,
535            p: 0.0,
536            g_na: 112.5,
537            g_k: 225.0,
538            g_kv3: 150.0,
539            g_l: 0.25,
540            e_na: 50.0,
541            e_k: -90.0,
542            e_l: -70.0,
543            dt: 0.01,
544            v_threshold: -20.0,
545        }
546    }
547    pub fn step(&mut self, current: f64) -> i32 {
548        let v_prev = self.v;
549        for _ in 0..10 {
550            let m_inf = 1.0 / (1.0 + (-(self.v + 24.0) / 11.5).exp());
551            let h_inf = 1.0 / (1.0 + ((self.v + 58.3) / 6.7).exp());
552            let n_inf = 1.0 / (1.0 + (-(self.v + 12.4) / 6.8).exp());
553            // Golomb et al. 2007: p_inf slope = 8.0
554            let p_inf = 1.0 / (1.0 + (-(self.v + 3.0) / 8.0).exp());
555            // Golomb et al. 2007: voltage-dependent time constants
556            let tau_h = 0.5 + 14.0 / (1.0 + ((self.v + 60.0) / 12.0).exp());
557            let tau_n = 0.087 + 11.4 / (1.0 + ((self.v + 14.6) / 8.6).exp());
558            let tau_p = 0.1 + 4.0 / (1.0 + ((self.v + 25.0) / 10.0).exp());
559            self.h += (h_inf - self.h) / tau_h * self.dt;
560            self.n += (n_inf - self.n) / tau_n * self.dt;
561            self.p += (p_inf - self.p) / tau_p * self.dt;
562            let i_na = self.g_na * m_inf.powi(3) * self.h * (self.v - self.e_na);
563            // Golomb et al. 2007: I_Kd uses n^4, I_Kv3 uses p^2
564            let i_k = self.g_k * self.n.powi(4) * (self.v - self.e_k);
565            let i_kv3 = self.g_kv3 * self.p.powi(2) * (self.v - self.e_k);
566            let i_l = self.g_l * (self.v - self.e_l);
567            self.v += (-i_na - i_k - i_kv3 - i_l + current) * self.dt;
568        }
569        if self.v >= self.v_threshold && v_prev < self.v_threshold {
570            1
571        } else {
572            0
573        }
574    }
575    pub fn reset(&mut self) {
576        self.v = -65.0;
577        self.h = 0.9;
578        self.n = 0.1;
579        self.p = 0.0;
580    }
581}
582impl Default for GolombFSNeuron {
583    fn default() -> Self {
584        Self::new()
585    }
586}
587
588/// Pospischil — minimal HH for 5 cortical cell types. Pospischil et al. 2008.
589#[derive(Clone, Debug)]
590pub struct PospischilNeuron {
591    pub v: f64,
592    pub m: f64,
593    pub h: f64,
594    pub n: f64,
595    pub p: f64,
596    pub g_na: f64,
597    pub g_k: f64,
598    pub g_m: f64,
599    pub g_l: f64,
600    pub e_na: f64,
601    pub e_k: f64,
602    pub e_l: f64,
603    pub c_m: f64,
604    pub vt: f64,
605    pub dt: f64,
606    pub v_threshold: f64,
607}
608
609impl PospischilNeuron {
610    pub fn new() -> Self {
611        Self {
612            v: -70.0,
613            m: 0.05,
614            h: 0.6,
615            n: 0.3,
616            p: 0.0,
617            g_na: 50.0,
618            g_k: 5.0,
619            g_m: 0.07,
620            g_l: 0.1,
621            e_na: 50.0,
622            e_k: -90.0,
623            e_l: -70.0,
624            c_m: 1.0,
625            vt: -56.2,
626            dt: 0.025,
627            v_threshold: -20.0,
628        }
629    }
630    pub fn step(&mut self, current: f64) -> i32 {
631        let v_prev = self.v;
632        for _ in 0..4 {
633            let dv = self.v - self.vt;
634            let x_m = dv - 13.0;
635            let am = if x_m.abs() < 1e-6 {
636                -0.32 * -4.0
637            } else {
638                -0.32 * x_m / ((-(x_m) / 4.0).exp() - 1.0)
639            };
640            let x_bm = dv - 40.0;
641            let bm = if x_bm.abs() < 1e-6 {
642                0.28 * 5.0
643            } else {
644                0.28 * x_bm / ((x_bm / 5.0).exp() - 1.0)
645            };
646            let ah = 0.128 * (-(dv - 17.0) / 18.0).exp();
647            let bh = 4.0 / (1.0 + (-(dv - 40.0) / 5.0).exp());
648            let x_n = dv - 15.0;
649            let an = if x_n.abs() < 1e-6 {
650                -0.032 * -5.0
651            } else {
652                -0.032 * x_n / ((-(x_n) / 5.0).exp() - 1.0)
653            };
654            let bn = 0.5 * (-(dv - 10.0) / 40.0).exp();
655            let p_inf = 1.0 / (1.0 + (-(self.v + 35.0) / 10.0).exp());
656            self.m += (am * (1.0 - self.m) - bm * self.m) * self.dt;
657            self.h += (ah * (1.0 - self.h) - bh * self.h) * self.dt;
658            self.n += (an * (1.0 - self.n) - bn * self.n) * self.dt;
659            let tau_p =
660                608.0 / (3.3 * ((self.v + 35.0) / 20.0).exp() + (-(self.v + 35.0) / 20.0).exp());
661            self.p += (p_inf - self.p) / tau_p * self.dt;
662            let i_na = self.g_na * self.m.powi(3) * self.h * (self.v - self.e_na);
663            let i_k = self.g_k * self.n.powi(4) * (self.v - self.e_k);
664            let i_m = self.g_m * self.p * (self.v - self.e_k);
665            let i_l = self.g_l * (self.v - self.e_l);
666            self.v += (-i_na - i_k - i_m - i_l + current) / self.c_m * self.dt;
667        }
668        if self.v >= self.v_threshold && v_prev < self.v_threshold {
669            1
670        } else {
671            0
672        }
673    }
674    pub fn reset(&mut self) {
675        self.v = -70.0;
676        self.m = 0.05;
677        self.h = 0.6;
678        self.n = 0.3;
679        self.p = 0.0;
680    }
681}
682#[allow(clippy::derivable_impls)]
683impl Default for PospischilNeuron {
684    fn default() -> Self {
685        Self::new()
686    }
687}
688
689/// Mainen-Sejnowski — two-compartment (soma + axon). Mainen & Sejnowski 1996.
690#[derive(Clone, Debug)]
691pub struct MainenSejnowskiNeuron {
692    pub vs: f64,
693    pub va: f64,
694    pub m: f64,
695    pub h: f64,
696    pub n: f64,
697    pub kappa: f64,
698    pub g_na: f64,
699    pub g_k: f64,
700    pub g_l: f64,
701    pub e_na: f64,
702    pub e_k: f64,
703    pub e_l: f64,
704    pub c_s: f64,
705    pub c_a: f64,
706    pub dt: f64,
707    pub v_threshold: f64,
708}
709
710impl MainenSejnowskiNeuron {
711    pub fn new() -> Self {
712        Self {
713            vs: -65.0,
714            va: -65.0,
715            m: 0.05,
716            h: 0.6,
717            n: 0.3,
718            kappa: 10.0,
719            g_na: 3000.0,
720            g_k: 1500.0,
721            g_l: 1.0,
722            e_na: 50.0,
723            e_k: -90.0,
724            e_l: -70.0,
725            c_s: 1.0,
726            c_a: 0.1,
727            dt: 0.005,
728            v_threshold: -20.0,
729        }
730    }
731    pub fn step(&mut self, current: f64) -> i32 {
732        let v_prev = self.vs;
733        for _ in 0..20 {
734            // Mainen & Sejnowski 1996 axonal rate functions
735            let x_am = self.va + 25.0;
736            let am = if x_am.abs() < 1e-6 {
737                0.182 * 9.0
738            } else {
739                0.182 * x_am / (1.0 - (-(x_am) / 9.0).exp() + 1e-12)
740            };
741            let bm = if x_am.abs() < 1e-6 {
742                0.124 * 9.0
743            } else {
744                -0.124 * x_am / (1.0 - ((x_am) / 9.0).exp() + 1e-12)
745            };
746            let x_ah = self.va + 40.0;
747            let ah = if x_ah.abs() < 1e-6 {
748                0.024 * 5.0
749            } else {
750                0.024 * x_ah / (1.0 - (-(x_ah) / 5.0).exp() + 1e-12)
751            };
752            let x_bh = self.va + 65.0;
753            let bh = if x_bh.abs() < 1e-6 {
754                0.0091 * 5.0
755            } else {
756                -0.0091 * x_bh / (1.0 - ((x_bh) / 5.0).exp() + 1e-12)
757            };
758            let x_an = self.va - 20.0;
759            let an = if x_an.abs() < 1e-6 {
760                0.02 * 9.0
761            } else {
762                0.02 * x_an / (1.0 - (-(x_an) / 9.0).exp() + 1e-12)
763            };
764            let bn = if x_an.abs() < 1e-6 {
765                0.002 * 9.0
766            } else {
767                -0.002 * x_an / (1.0 - ((x_an) / 9.0).exp() + 1e-12)
768            };
769            self.m = (self.m + (am * (1.0 - self.m) - bm * self.m) * self.dt).clamp(0.0, 1.0);
770            self.h = (self.h + (ah * (1.0 - self.h) - bh * self.h) * self.dt).clamp(0.0, 1.0);
771            self.n = (self.n + (an * (1.0 - self.n) - bn * self.n) * self.dt).clamp(0.0, 1.0);
772            let i_na = self.g_na * self.m.powi(3) * self.h * (self.va - self.e_na);
773            let i_k = self.g_k * self.n * (self.va - self.e_k);
774            let i_l_s = self.g_l * (self.vs - self.e_l);
775            self.vs = (self.vs
776                + (-i_l_s + self.kappa * (self.va - self.vs) + current) / self.c_s * self.dt)
777                .clamp(-200.0, 200.0);
778            self.va = (self.va
779                + (-i_na - i_k + self.kappa * (self.vs - self.va)) / self.c_a * self.dt)
780                .clamp(-200.0, 200.0);
781        }
782        if self.vs >= self.v_threshold && v_prev < self.v_threshold {
783            1
784        } else {
785            0
786        }
787    }
788    pub fn reset(&mut self) {
789        self.vs = -65.0;
790        self.va = -65.0;
791        self.m = 0.05;
792        self.h = 0.6;
793        self.n = 0.3;
794    }
795}
796impl Default for MainenSejnowskiNeuron {
797    fn default() -> Self {
798        Self::new()
799    }
800}
801
802/// De Schutter-Bower Purkinje cell — Ca-dependent K. De Schutter & Bower 1994.
803#[derive(Clone, Debug)]
804pub struct DeSchutterPurkinjeNeuron {
805    pub v: f64,
806    pub h_na: f64,
807    pub n_k: f64,
808    pub m_cap: f64,
809    pub h_cap: f64,
810    pub q_kca: f64,
811    pub ca: f64,
812    pub g_na: f64,
813    pub g_k: f64,
814    pub g_cap: f64,
815    pub g_kca: f64,
816    pub g_l: f64,
817    pub e_na: f64,
818    pub e_k: f64,
819    pub e_ca: f64,
820    pub e_l: f64,
821    pub ca_decay: f64,
822    pub f_ca: f64,
823    pub dt: f64,
824    pub v_threshold: f64,
825}
826
827impl DeSchutterPurkinjeNeuron {
828    pub fn new() -> Self {
829        Self {
830            v: -68.0,
831            h_na: 0.8,
832            n_k: 0.1,
833            m_cap: 0.0,
834            h_cap: 0.9,
835            q_kca: 0.0,
836            ca: 0.0001,
837            g_na: 125.0,
838            g_k: 10.0,
839            g_cap: 45.0,
840            g_kca: 35.0,
841            g_l: 0.5,
842            e_na: 45.0,
843            e_k: -85.0,
844            e_ca: 135.0,
845            e_l: -68.0,
846            ca_decay: 0.02,
847            f_ca: 0.00024,
848            dt: 0.01,
849            v_threshold: -20.0,
850        }
851    }
852    pub fn step(&mut self, current: f64) -> i32 {
853        let v_prev = self.v;
854        for _ in 0..5 {
855            let m_na = 1.0 / (1.0 + (-(self.v + 35.0) / 7.5).exp());
856            let h_na_inf = 1.0 / (1.0 + ((self.v + 55.0) / 7.0).exp());
857            let n_inf = 1.0 / (1.0 + (-(self.v + 30.0) / 15.0).exp());
858            let m_cap_inf = 1.0 / (1.0 + (-(self.v + 19.0) / 5.5).exp());
859            let h_cap_inf = 1.0 / (1.0 + ((self.v + 48.0) / 7.0).exp());
860            let q_inf = self.ca / (self.ca + 0.0002);
861            let tau_h_na = 0.5 + 14.0 / (1.0 + ((self.v + 40.0) / 12.0).exp());
862            let tau_n_k = 1.0 + 11.0 / (1.0 + ((self.v + 15.0) / 8.0).exp());
863            self.h_na += (h_na_inf - self.h_na) / tau_h_na * self.dt;
864            self.n_k += (n_inf - self.n_k) / tau_n_k * self.dt;
865            self.m_cap += (m_cap_inf - self.m_cap) / 0.3 * self.dt;
866            self.h_cap += (h_cap_inf - self.h_cap) / 45.0 * self.dt;
867            self.q_kca += (q_inf - self.q_kca) / 1.0 * self.dt;
868            let i_na = self.g_na * m_na.powi(3) * self.h_na * (self.v - self.e_na);
869            let i_k = self.g_k * self.n_k.powi(4) * (self.v - self.e_k);
870            let i_cap = self.g_cap * self.m_cap.powi(2) * self.h_cap * (self.v - self.e_ca);
871            let i_kca = self.g_kca * self.q_kca * (self.v - self.e_k);
872            let i_l = self.g_l * (self.v - self.e_l);
873            self.v += (-i_na - i_k - i_cap - i_kca - i_l + current) * self.dt;
874            self.ca = (self.ca + (-self.f_ca * i_cap - self.ca_decay * self.ca) * self.dt).max(0.0);
875        }
876        if self.v >= self.v_threshold && v_prev < self.v_threshold {
877            1
878        } else {
879            0
880        }
881    }
882    pub fn reset(&mut self) {
883        self.v = -68.0;
884        self.h_na = 0.8;
885        self.n_k = 0.1;
886        self.m_cap = 0.0;
887        self.h_cap = 0.9;
888        self.q_kca = 0.0;
889        self.ca = 0.0001;
890    }
891}
892impl Default for DeSchutterPurkinjeNeuron {
893    fn default() -> Self {
894        Self::new()
895    }
896}
897
898/// Plant R15 — Aplysia parabolic burster. Plant & Kim 1976.
899#[derive(Clone, Debug)]
900pub struct PlantR15Neuron {
901    pub v: f64,
902    pub m: f64,
903    pub h: f64,
904    pub n: f64,
905    pub ca: f64,
906    pub g_na: f64,
907    pub g_k: f64,
908    pub g_ca: f64,
909    pub g_l: f64,
910    pub g_kca: f64,
911    pub e_na: f64,
912    pub e_k: f64,
913    pub e_ca: f64,
914    pub e_l: f64,
915    pub c_m: f64,
916    pub k_ca: f64,
917    pub tau_ca: f64,
918    pub dt: f64,
919    pub v_threshold: f64,
920}
921
922impl PlantR15Neuron {
923    pub fn new() -> Self {
924        Self {
925            v: -50.0,
926            m: 0.05,
927            h: 0.6,
928            n: 0.3,
929            ca: 0.1,
930            g_na: 4.0,
931            g_k: 0.3,
932            g_ca: 0.004,
933            g_l: 0.003,
934            g_kca: 0.03,
935            e_na: 30.0,
936            e_k: -75.0,
937            e_ca: 140.0,
938            e_l: -40.0,
939            c_m: 1.0,
940            k_ca: 0.0085,
941            tau_ca: 500.0,
942            dt: 0.05,
943            v_threshold: -10.0,
944        }
945    }
946    pub fn step(&mut self, current: f64) -> i32 {
947        let v_prev = self.v;
948        for _ in 0..5 {
949            let am = safe_rate(0.1, 50.0, self.v, 10.0, 1.0);
950            let bm = 4.0 * (-(self.v + 75.0) / 18.0).exp();
951            let ah = 0.07 * (-(self.v + 50.0) / 20.0).exp();
952            let bh = 1.0 / (1.0 + (-(self.v + 20.0) / 10.0).exp());
953            let an = safe_rate(0.01, 55.0, self.v, 10.0, 0.1);
954            let bn = 0.125 * (-(self.v + 65.0) / 80.0).exp();
955            self.m += (am * (1.0 - self.m) - bm * self.m) * self.dt;
956            self.h += (ah * (1.0 - self.h) - bh * self.h) * self.dt;
957            self.n += (an * (1.0 - self.n) - bn * self.n) * self.dt;
958            let m_ca = 1.0 / (1.0 + (-(self.v + 25.0) / 5.0).exp());
959            let kca_act = self.ca / (0.5 + self.ca);
960            let i_na = self.g_na * self.m.powi(3) * self.h * (self.v - self.e_na);
961            let i_k = self.g_k * self.n.powi(4) * (self.v - self.e_k);
962            let i_ca = self.g_ca * m_ca.powi(2) * (self.v - self.e_ca);
963            let i_kca = self.g_kca * kca_act * (self.v - self.e_k);
964            let i_l = self.g_l * (self.v - self.e_l);
965            self.v += (-i_na - i_k - i_ca - i_kca - i_l + current) / self.c_m * self.dt;
966            self.ca = (self.ca + (-self.k_ca * i_ca - self.ca / self.tau_ca) * self.dt).max(0.0);
967        }
968        if self.v >= self.v_threshold && v_prev < self.v_threshold {
969            1
970        } else {
971            0
972        }
973    }
974    pub fn reset(&mut self) {
975        self.v = -50.0;
976        self.m = 0.05;
977        self.h = 0.6;
978        self.n = 0.3;
979        self.ca = 0.1;
980    }
981}
982impl Default for PlantR15Neuron {
983    fn default() -> Self {
984        Self::new()
985    }
986}
987
988/// Prescott 2008 — Type I/II/III excitability tuning via M-current.
989#[derive(Clone, Debug)]
990pub struct PrescottNeuron {
991    pub v: f64,
992    pub w: f64,
993    pub g_fast: f64,
994    pub g_slow: f64,
995    pub g_l: f64,
996    pub e_fast: f64,
997    pub e_slow: f64,
998    pub e_l: f64,
999    pub beta_w: f64,
1000    pub gamma_w: f64,
1001    pub tau_w: f64,
1002    pub phi: f64,
1003    pub dt: f64,
1004    pub v_threshold: f64,
1005}
1006
1007impl PrescottNeuron {
1008    pub fn new() -> Self {
1009        Self {
1010            v: -65.0,
1011            w: 0.0,
1012            g_fast: 20.0,
1013            g_slow: 20.0,
1014            g_l: 2.0,
1015            e_fast: 50.0,
1016            e_slow: -100.0,
1017            e_l: -70.0,
1018            beta_w: -21.0,
1019            gamma_w: 15.0,
1020            tau_w: 100.0,
1021            phi: 0.15,
1022            dt: 0.1,
1023            v_threshold: -20.0,
1024        }
1025    }
1026    pub fn step(&mut self, current: f64) -> i32 {
1027        let v_prev = self.v;
1028        let m_inf = 1.0 / (1.0 + (-(self.v + 20.0) / 15.0).exp());
1029        let w_inf = 1.0 / (1.0 + (-(self.v - self.beta_w) / self.gamma_w).exp());
1030        let i_fast = self.g_fast * m_inf * (self.v - self.e_fast);
1031        let i_slow = self.g_slow * self.w * (self.v - self.e_slow);
1032        let i_l = self.g_l * (self.v - self.e_l);
1033        self.v += (-i_fast - i_slow - i_l + current) * self.dt;
1034        self.w += self.phi * (w_inf - self.w) / self.tau_w * self.dt;
1035        if self.v >= self.v_threshold && v_prev < self.v_threshold {
1036            1
1037        } else {
1038            0
1039        }
1040    }
1041    pub fn reset(&mut self) {
1042        self.v = -65.0;
1043        self.w = 0.0;
1044    }
1045}
1046impl Default for PrescottNeuron {
1047    fn default() -> Self {
1048        Self::new()
1049    }
1050}
1051
1052/// Mihalas-Niebur 2009 — generalized IF capturing 20 spike patterns.
1053#[derive(Clone, Debug)]
1054pub struct MihalasNieburNeuron {
1055    pub v: f64,
1056    pub theta: f64,
1057    pub i1: f64,
1058    pub i2: f64,
1059    pub v_rest: f64,
1060    pub v_reset: f64,
1061    pub theta_reset: f64,
1062    pub theta_inf: f64,
1063    pub tau_v: f64,
1064    pub tau_theta: f64,
1065    pub tau_1: f64,
1066    pub tau_2: f64,
1067    pub a: f64,
1068    pub b: f64,
1069    pub r1: f64,
1070    pub r2: f64,
1071    pub dt: f64,
1072}
1073
1074impl MihalasNieburNeuron {
1075    pub fn new() -> Self {
1076        Self {
1077            v: 0.0,
1078            theta: 1.0,
1079            i1: 0.0,
1080            i2: 0.0,
1081            v_rest: 0.0,
1082            v_reset: 0.0,
1083            theta_reset: 1.0,
1084            theta_inf: 1.0,
1085            tau_v: 10.0,
1086            tau_theta: 100.0,
1087            tau_1: 10.0,
1088            tau_2: 200.0,
1089            a: 0.0,
1090            b: 0.0,
1091            r1: 0.0,
1092            r2: 0.0,
1093            dt: 1.0,
1094        }
1095    }
1096    pub fn step(&mut self, current: f64) -> i32 {
1097        self.v += (-(self.v - self.v_rest) + self.i1 + self.i2 + current) / self.tau_v * self.dt;
1098        self.theta += self.a * (self.v - self.v_rest)
1099            + (self.theta_inf - self.theta) / self.tau_theta * self.dt;
1100        self.i1 += -self.i1 / self.tau_1 * self.dt;
1101        self.i2 += -self.i2 / self.tau_2 * self.dt;
1102        if self.v >= self.theta {
1103            self.v = self.v_reset + self.b * (self.v - self.v_rest);
1104            self.theta = self.theta_reset.max(self.theta);
1105            self.i1 += self.r1;
1106            self.i2 += self.r2;
1107            1
1108        } else {
1109            0
1110        }
1111    }
1112    pub fn reset(&mut self) {
1113        self.v = self.v_rest;
1114        self.theta = self.theta_reset;
1115        self.i1 = 0.0;
1116        self.i2 = 0.0;
1117    }
1118}
1119impl Default for MihalasNieburNeuron {
1120    fn default() -> Self {
1121        Self::new()
1122    }
1123}
1124
1125/// Allen GLIF5 — LIF + threshold adaptation + after-spike currents.
1126#[derive(Clone, Debug)]
1127pub struct GLIFNeuron {
1128    pub v: f64,
1129    pub theta: f64,
1130    pub theta_inf: f64,
1131    pub i_asc1: f64,
1132    pub i_asc2: f64,
1133    pub v_rest: f64,
1134    pub v_reset: f64,
1135    pub tau_m: f64,
1136    pub tau_theta: f64,
1137    pub tau_asc1: f64,
1138    pub tau_asc2: f64,
1139    pub a_theta: f64,
1140    pub delta_theta: f64,
1141    pub r_asc1: f64,
1142    pub r_asc2: f64,
1143    pub resistance: f64,
1144    pub dt: f64,
1145}
1146
1147impl GLIFNeuron {
1148    pub fn new() -> Self {
1149        Self {
1150            v: -70.0,
1151            theta: -50.0,
1152            theta_inf: -50.0,
1153            i_asc1: 0.0,
1154            i_asc2: 0.0,
1155            v_rest: -70.0,
1156            v_reset: -70.0,
1157            tau_m: 10.0,
1158            tau_theta: 100.0,
1159            tau_asc1: 10.0,
1160            tau_asc2: 200.0,
1161            a_theta: 0.01,
1162            delta_theta: 2.0,
1163            r_asc1: 1.0,
1164            r_asc2: 0.5,
1165            resistance: 1.0,
1166            dt: 1.0,
1167        }
1168    }
1169    pub fn step(&mut self, current: f64) -> i32 {
1170        self.v += (-(self.v - self.v_rest) + self.resistance * current + self.i_asc1 + self.i_asc2)
1171            / self.tau_m
1172            * self.dt;
1173        self.theta += self.a_theta * (self.v - self.v_rest)
1174            + (self.theta_inf - self.theta) / self.tau_theta * self.dt;
1175        self.i_asc1 *= (-self.dt / self.tau_asc1).exp();
1176        self.i_asc2 *= (-self.dt / self.tau_asc2).exp();
1177        if self.v >= self.theta {
1178            self.v = self.v_reset;
1179            self.theta += self.delta_theta;
1180            self.i_asc1 += self.r_asc1;
1181            self.i_asc2 += self.r_asc2;
1182            1
1183        } else {
1184            0
1185        }
1186    }
1187    pub fn reset(&mut self) {
1188        self.v = self.v_rest;
1189        self.theta = self.theta_inf;
1190        self.i_asc1 = 0.0;
1191        self.i_asc2 = 0.0;
1192    }
1193}
1194impl Default for GLIFNeuron {
1195    fn default() -> Self {
1196        Self::new()
1197    }
1198}
1199
1200/// GIF population — escape-rate generalized IF. Mensi et al. 2012.
1201#[derive(Clone, Debug)]
1202pub struct GIFPopulationNeuron {
1203    pub v: f64,
1204    pub theta: f64,
1205    pub eta: f64,
1206    pub tau_m: f64,
1207    pub tau_eta: f64,
1208    pub delta_v: f64,
1209    pub lambda_0: f64,
1210    pub eta_increment: f64,
1211    pub v_rest: f64,
1212    pub v_reset: f64,
1213    pub dt: f64,
1214    rng: Xoshiro256PlusPlus,
1215}
1216
1217impl GIFPopulationNeuron {
1218    pub fn new(seed: u64) -> Self {
1219        Self {
1220            v: -65.0,
1221            theta: -50.0,
1222            eta: 0.0,
1223            tau_m: 20.0,
1224            tau_eta: 100.0,
1225            delta_v: 2.0,
1226            lambda_0: 0.001,
1227            eta_increment: 5.0,
1228            v_rest: -65.0,
1229            v_reset: -65.0,
1230            dt: 0.5,
1231            rng: Xoshiro256PlusPlus::seed_from_u64(seed),
1232        }
1233    }
1234    pub fn step(&mut self, current: f64) -> i32 {
1235        // Mensi et al. 2012 Eq. 1: C dV/dt = -g_L(V - E_L) - eta + I_ext
1236        self.v += (-(self.v - self.v_rest) - self.eta + current) / self.tau_m * self.dt;
1237        // Eq. 2: eta decays exponentially between spikes
1238        self.eta *= (-self.dt / self.tau_eta).exp();
1239        // Eq. 3: escape-rate hazard lambda = lambda_0 * exp((V - theta) / delta_V)
1240        let exponent = ((self.v - self.theta) / self.delta_v).min(20.0);
1241        let hazard = self.lambda_0 * exponent.exp();
1242        // Eq. 4: P(spike) = 1 - exp(-lambda * dt)
1243        let p_spike = 1.0 - (-hazard * self.dt).exp();
1244        if self.rng.random::<f64>() < p_spike {
1245            self.v = self.v_reset;
1246            self.eta += self.eta_increment;
1247            1
1248        } else {
1249            0
1250        }
1251    }
1252    pub fn reset(&mut self) {
1253        self.v = -65.0;
1254        self.eta = 0.0;
1255    }
1256}
1257
1258/// Av-Ron cardiac ganglion — Type III burster. Av-Ron et al. 1991.
1259#[derive(Clone, Debug)]
1260pub struct AvRonCardiacNeuron {
1261    pub v: f64,
1262    pub h: f64,
1263    pub n: f64,
1264    pub s: f64,
1265    pub g_na: f64,
1266    pub g_k: f64,
1267    pub g_s: f64,
1268    pub g_l: f64,
1269    pub e_na: f64,
1270    pub e_k: f64,
1271    pub e_s: f64,
1272    pub e_l: f64,
1273    pub dt: f64,
1274    pub v_threshold: f64,
1275}
1276
1277impl AvRonCardiacNeuron {
1278    pub fn new() -> Self {
1279        Self {
1280            v: -60.0,
1281            h: 0.6,
1282            n: 0.3,
1283            s: 0.5,
1284            g_na: 80.0,
1285            g_k: 40.0,
1286            g_s: 20.0,
1287            g_l: 0.1,
1288            e_na: 40.0,
1289            e_k: -80.0,
1290            e_s: -25.0,
1291            e_l: -60.0,
1292            dt: 0.02,
1293            v_threshold: -20.0,
1294        }
1295    }
1296    pub fn step(&mut self, current: f64) -> i32 {
1297        let v_prev = self.v;
1298        let m_inf = 1.0 / (1.0 + (-(self.v + 40.0) / 7.0).exp());
1299        let h_inf = 1.0 / (1.0 + ((self.v + 45.0) / 5.0).exp());
1300        let n_inf = 1.0 / (1.0 + (-(self.v + 40.0) / 15.0).exp());
1301        let s_inf = 1.0 / (1.0 + ((self.v + 35.0) / 3.0).exp());
1302        let tau_h = 1.0 + 12.0 / (1.0 + ((self.v + 50.0) / 8.0).exp());
1303        let tau_n = 1.0 + 8.0 / (1.0 + ((self.v + 35.0) / 8.0).exp());
1304        let tau_s = 200.0 + 1000.0 / (1.0 + ((self.v + 30.0) / 5.0).exp());
1305        self.h += (h_inf - self.h) / tau_h * self.dt;
1306        self.n += (n_inf - self.n) / tau_n * self.dt;
1307        self.s += (s_inf - self.s) / tau_s * self.dt;
1308        let i_na = self.g_na * m_inf.powi(3) * self.h * (self.v - self.e_na);
1309        let i_k = self.g_k * self.n.powi(4) * (self.v - self.e_k);
1310        let i_s = self.g_s * self.s * (self.v - self.e_s);
1311        let i_l = self.g_l * (self.v - self.e_l);
1312        self.v += (-i_na - i_k - i_s - i_l + current) * self.dt;
1313        if self.v >= self.v_threshold && v_prev < self.v_threshold {
1314            1
1315        } else {
1316            0
1317        }
1318    }
1319    pub fn reset(&mut self) {
1320        self.v = -60.0;
1321        self.h = 0.6;
1322        self.n = 0.3;
1323        self.s = 0.5;
1324    }
1325}
1326impl Default for AvRonCardiacNeuron {
1327    fn default() -> Self {
1328        Self::new()
1329    }
1330}
1331
1332/// Durstewitz PFC neuron with D1 dopamine modulation. Durstewitz et al. 2000.
1333#[derive(Clone, Debug)]
1334pub struct DurstewitzDopamineNeuron {
1335    pub v: f64,
1336    pub h_na: f64,
1337    pub n_k: f64,
1338    pub g_na: f64,
1339    pub g_k: f64,
1340    pub g_nmda: f64,
1341    pub g_l: f64,
1342    pub e_na: f64,
1343    pub e_k: f64,
1344    pub e_nmda: f64,
1345    pub e_l: f64,
1346    pub mg: f64,
1347    pub d1_level: f64,
1348    pub g_nmda_scale: f64,
1349    pub g_k_scale: f64,
1350    pub v_shift_na: f64,
1351    pub dt: f64,
1352    pub v_threshold: f64,
1353}
1354
1355impl DurstewitzDopamineNeuron {
1356    pub fn new() -> Self {
1357        Self {
1358            v: -65.0,
1359            h_na: 0.7,
1360            n_k: 0.2,
1361            g_na: 45.0,
1362            g_k: 18.0,
1363            g_nmda: 0.5,
1364            g_l: 0.02,
1365            e_na: 55.0,
1366            e_k: -80.0,
1367            e_nmda: 0.0,
1368            e_l: -65.0,
1369            mg: 1.0,
1370            d1_level: 0.0,
1371            g_nmda_scale: 2.5,
1372            g_k_scale: 1.5,
1373            v_shift_na: -5.0,
1374            dt: 0.05,
1375            v_threshold: -20.0,
1376        }
1377    }
1378    pub fn step(&mut self, current: f64) -> i32 {
1379        let v_prev = self.v;
1380        let shift = self.d1_level * self.v_shift_na;
1381        let m_inf = 1.0 / (1.0 + (-(self.v + 30.0 + shift) / 9.5).exp());
1382        let h_inf = 1.0 / (1.0 + ((self.v + 53.0) / 7.0).exp());
1383        let n_inf = 1.0 / (1.0 + (-(self.v + 30.0) / 10.0).exp());
1384        let tau_h = 0.5 + 14.0 / (1.0 + ((self.v + 50.0) / 12.0).exp());
1385        let tau_n = 1.0 + 11.0 / (1.0 + ((self.v + 40.0) / 10.0).exp());
1386        self.h_na += (h_inf - self.h_na) / tau_h * self.dt;
1387        self.n_k += (n_inf - self.n_k) / tau_n * self.dt;
1388        let g_eff_nmda = self.g_nmda * (1.0 + self.d1_level * (self.g_nmda_scale - 1.0));
1389        let g_eff_k = self.g_k * (1.0 + self.d1_level * (self.g_k_scale - 1.0));
1390        let mg_block = 1.0 / (1.0 + self.mg * (-0.062 * self.v).exp() / 3.57);
1391        let i_na = self.g_na * m_inf.powi(3) * self.h_na * (self.v - self.e_na);
1392        let i_k = g_eff_k * self.n_k.powi(4) * (self.v - self.e_k);
1393        let i_nmda = g_eff_nmda * mg_block * (self.v - self.e_nmda);
1394        let i_l = self.g_l * (self.v - self.e_l);
1395        self.v += (-i_na - i_k - i_nmda - i_l + current) * self.dt;
1396        if self.v >= self.v_threshold && v_prev < self.v_threshold {
1397            1
1398        } else {
1399            0
1400        }
1401    }
1402    pub fn reset(&mut self) {
1403        self.v = -65.0;
1404        self.h_na = 0.7;
1405        self.n_k = 0.2;
1406    }
1407}
1408impl Default for DurstewitzDopamineNeuron {
1409    fn default() -> Self {
1410        Self::new()
1411    }
1412}
1413
1414/// Hill-Tononi 2005 — thalamocortical sleep/wake with Na-dependent K.
1415#[derive(Clone, Debug)]
1416pub struct HillTononiNeuron {
1417    pub v: f64,
1418    pub h_na: f64,
1419    pub n_k: f64,
1420    pub m_h: f64,
1421    pub h_t: f64,
1422    pub na_i: f64,
1423    pub g_na: f64,
1424    pub g_k: f64,
1425    pub g_h: f64,
1426    pub g_t: f64,
1427    pub g_kna: f64,
1428    pub g_l: f64,
1429    pub e_na: f64,
1430    pub e_k: f64,
1431    pub e_h: f64,
1432    pub e_ca: f64,
1433    pub e_l: f64,
1434    pub na_pump_max: f64,
1435    pub na_eq: f64,
1436    pub dt: f64,
1437    pub v_threshold: f64,
1438}
1439
1440impl HillTononiNeuron {
1441    pub fn new() -> Self {
1442        Self {
1443            v: -65.0,
1444            h_na: 0.6,
1445            n_k: 0.3,
1446            m_h: 0.0,
1447            h_t: 0.9,
1448            na_i: 5.0,
1449            g_na: 50.0,
1450            g_k: 5.0,
1451            g_h: 1.0,
1452            g_t: 3.0,
1453            g_kna: 1.33,
1454            g_l: 0.02,
1455            e_na: 50.0,
1456            e_k: -90.0,
1457            e_h: -43.0,
1458            e_ca: 120.0,
1459            e_l: -70.0,
1460            na_pump_max: 20.0,
1461            na_eq: 9.5,
1462            dt: 0.05,
1463            v_threshold: -20.0,
1464        }
1465    }
1466    pub fn step(&mut self, current: f64) -> i32 {
1467        let v_prev = self.v;
1468        let m_na_inf = 1.0 / (1.0 + (-(self.v + 38.0) / 10.0).exp());
1469        let h_na_inf = 1.0 / (1.0 + ((self.v + 43.0) / 6.0).exp());
1470        let n_k_inf = 1.0 / (1.0 + (-(self.v + 27.0) / 11.5).exp());
1471        let m_h_inf = 1.0 / (1.0 + ((self.v + 75.0) / 5.5).exp());
1472        let m_t_inf = 1.0 / (1.0 + (-(self.v + 59.0) / 6.2).exp());
1473        let h_t_inf = 1.0 / (1.0 + ((self.v + 83.0) / 4.0).exp());
1474        let w_kna = 0.37 / (1.0 + (38.7 / self.na_i.max(0.01)).powf(3.5));
1475        let tau_h_na = (1.0 + 10.0 / (1.0 + ((self.v + 40.0) / 10.0).exp())).max(0.1);
1476        // Hill & Tononi 2005: tau_n_k = 5 + 47 * exp(-((V+50)/25)^2)
1477        let tau_n_k = (5.0 + 47.0 * (-(((self.v + 50.0) / 25.0).powi(2))).exp()).max(0.1);
1478        let tau_m_h = (20.0
1479            + 1000.0 / (((self.v + 71.5) / 14.2).exp() + (-(self.v + 89.0) / 11.6).exp()))
1480        .max(1.0);
1481        let tau_h_t = if self.v < -81.0 {
1482            (30.8 + 211.4 * ((self.v + 115.2) / 5.0).exp() / (1.0 + ((self.v + 86.0) / 3.2).exp()))
1483                .max(0.1)
1484        } else {
1485            10.0
1486        };
1487        self.h_na += (h_na_inf - self.h_na) / tau_h_na * self.dt;
1488        self.n_k += (n_k_inf - self.n_k) / tau_n_k * self.dt;
1489        self.m_h += (m_h_inf - self.m_h) / tau_m_h * self.dt;
1490        self.h_t += (h_t_inf - self.h_t) / tau_h_t * self.dt;
1491        let i_na = self.g_na * m_na_inf.powi(3) * self.h_na * (self.v - self.e_na);
1492        let i_k = self.g_k * self.n_k.powi(4) * (self.v - self.e_k);
1493        let i_h = self.g_h * self.m_h * (self.v - self.e_h);
1494        let i_t = self.g_t * m_t_inf.powi(2) * self.h_t * (self.v - self.e_ca);
1495        let i_kna = self.g_kna * w_kna * (self.v - self.e_k);
1496        let i_l = self.g_l * (self.v - self.e_l);
1497        self.v += (-i_na - i_k - i_h - i_t - i_kna - i_l + current) * self.dt;
1498        self.na_i = (self.na_i
1499            + (-0.001 * i_na - self.na_pump_max * (self.na_i / (self.na_i + self.na_eq)))
1500                * self.dt)
1501            .max(0.0);
1502        if self.v >= self.v_threshold && v_prev < self.v_threshold {
1503            1
1504        } else {
1505            0
1506        }
1507    }
1508    pub fn reset(&mut self) {
1509        self.v = -65.0;
1510        self.h_na = 0.6;
1511        self.n_k = 0.3;
1512        self.m_h = 0.0;
1513        self.h_t = 0.9;
1514        self.na_i = 5.0;
1515    }
1516}
1517impl Default for HillTononiNeuron {
1518    fn default() -> Self {
1519        Self::new()
1520    }
1521}
1522
1523/// Bertram phantom burster — dual slow K for phantom bursting. Bertram et al. 2000.
1524#[derive(Clone, Debug)]
1525pub struct BertramPhantomBurster {
1526    pub v: f64,
1527    pub s1: f64,
1528    pub s2: f64,
1529    pub g_ca: f64,
1530    pub g_k: f64,
1531    pub g_s1: f64,
1532    pub g_s2: f64,
1533    pub g_l: f64,
1534    pub e_ca: f64,
1535    pub e_k: f64,
1536    pub e_l: f64,
1537    pub c_m: f64,
1538    pub v_m: f64,
1539    pub s_m: f64,
1540    pub v_n: f64,
1541    pub s_n: f64,
1542    pub v_s1: f64,
1543    pub s_s1: f64,
1544    pub v_s2: f64,
1545    pub s_s2: f64,
1546    pub tau_s1: f64,
1547    pub tau_s2: f64,
1548    pub dt: f64,
1549    pub v_threshold: f64,
1550}
1551
1552impl BertramPhantomBurster {
1553    pub fn new() -> Self {
1554        Self {
1555            v: -50.0,
1556            s1: 0.1,
1557            s2: 0.1,
1558            g_ca: 3.6,
1559            g_k: 10.0,
1560            g_s1: 4.0,
1561            g_s2: 4.0,
1562            g_l: 0.2,
1563            e_ca: 25.0,
1564            e_k: -75.0,
1565            e_l: -40.0,
1566            c_m: 5.3,
1567            v_m: -20.0,
1568            s_m: 12.0,
1569            v_n: -16.0,
1570            s_n: 5.6,
1571            v_s1: -40.0,
1572            s_s1: 10.0,
1573            v_s2: -42.0,
1574            s_s2: 0.4,
1575            tau_s1: 20000.0,
1576            tau_s2: 100000.0,
1577            dt: 0.5,
1578            v_threshold: -20.0,
1579        }
1580    }
1581    pub fn step(&mut self, current: f64) -> i32 {
1582        let v_prev = self.v;
1583        let m_inf = 1.0 / (1.0 + (-(self.v - self.v_m) / self.s_m).exp());
1584        let n_inf = 1.0 / (1.0 + (-(self.v - self.v_n) / self.s_n).exp());
1585        let s1_inf = 1.0 / (1.0 + (-(self.v - self.v_s1) / self.s_s1).exp());
1586        let s2_inf = 1.0 / (1.0 + (-(self.v - self.v_s2) / self.s_s2).exp());
1587        let i_ca = self.g_ca * m_inf * (self.v - self.e_ca);
1588        let i_k = self.g_k * n_inf * (self.v - self.e_k);
1589        let i_s1 = self.g_s1 * self.s1 * (self.v - self.e_k);
1590        let i_s2 = self.g_s2 * self.s2 * (self.v - self.e_k);
1591        let i_l = self.g_l * (self.v - self.e_l);
1592        self.v += (-i_ca - i_k - i_s1 - i_s2 - i_l + current) / self.c_m * self.dt;
1593        self.s1 += (s1_inf - self.s1) / self.tau_s1 * self.dt;
1594        self.s2 += (s2_inf - self.s2) / self.tau_s2 * self.dt;
1595        if self.v >= self.v_threshold && v_prev < self.v_threshold {
1596            1
1597        } else {
1598            0
1599        }
1600    }
1601    pub fn reset(&mut self) {
1602        self.v = -50.0;
1603        self.s1 = 0.1;
1604        self.s2 = 0.1;
1605    }
1606}
1607impl Default for BertramPhantomBurster {
1608    fn default() -> Self {
1609        Self::new()
1610    }
1611}
1612
1613/// Yamada 1989 — subcritical Hopf burster.
1614#[derive(Clone, Debug)]
1615pub struct YamadaNeuron {
1616    pub v: f64,
1617    pub n: f64,
1618    pub q: f64,
1619    pub g_na: f64,
1620    pub g_k: f64,
1621    pub g_q: f64,
1622    pub g_l: f64,
1623    pub e_na: f64,
1624    pub e_k: f64,
1625    pub e_q: f64,
1626    pub e_l: f64,
1627    pub tau_q: f64,
1628    pub dt: f64,
1629    pub v_threshold: f64,
1630}
1631
1632impl YamadaNeuron {
1633    pub fn new() -> Self {
1634        Self {
1635            v: -60.0,
1636            n: 0.1,
1637            q: 0.0,
1638            g_na: 20.0,
1639            g_k: 10.0,
1640            g_q: 5.0,
1641            g_l: 0.5,
1642            e_na: 60.0,
1643            e_k: -80.0,
1644            e_q: -80.0,
1645            e_l: -60.0,
1646            tau_q: 300.0,
1647            dt: 0.05,
1648            v_threshold: -20.0,
1649        }
1650    }
1651    pub fn step(&mut self, current: f64) -> i32 {
1652        let v_prev = self.v;
1653        let m_inf = 1.0 / (1.0 + (-(self.v + 30.0) / 9.5).exp());
1654        let n_inf = 1.0 / (1.0 + (-(self.v + 30.0) / 10.0).exp());
1655        let q_inf = 1.0 / (1.0 + (-(self.v + 50.0) / 10.0).exp());
1656        let tau_n = 1.0 + 7.5 / (1.0 + ((self.v + 40.0) / 12.0).exp());
1657        let i_na = self.g_na * m_inf.powi(3) * (1.0 - self.n) * (self.v - self.e_na);
1658        let i_k = self.g_k * self.n.powi(4) * (self.v - self.e_k);
1659        let i_q = self.g_q * self.q * (self.v - self.e_q);
1660        let i_l = self.g_l * (self.v - self.e_l);
1661        self.v += (-i_na - i_k - i_q - i_l + current) * self.dt;
1662        self.n += (n_inf - self.n) / tau_n * self.dt;
1663        self.q += (q_inf - self.q) / self.tau_q * self.dt;
1664        if self.v >= self.v_threshold && v_prev < self.v_threshold {
1665            1
1666        } else {
1667            0
1668        }
1669    }
1670    pub fn reset(&mut self) {
1671        self.v = -60.0;
1672        self.n = 0.1;
1673        self.q = 0.0;
1674    }
1675}
1676impl Default for YamadaNeuron {
1677    fn default() -> Self {
1678        Self::new()
1679    }
1680}
1681
1682#[cfg(test)]
1683mod tests {
1684    use super::*;
1685
1686    #[test]
1687    fn hh_fires() {
1688        let mut n = HodgkinHuxleyNeuron::new();
1689        let t: i32 = (0..100).map(|_| n.step(10.0)).sum();
1690        assert!(t > 0);
1691    }
1692    #[test]
1693    fn traub_fires() {
1694        let mut n = TraubMilesNeuron::new();
1695        let t: i32 = (0..200).map(|_| n.step(5.0)).sum();
1696        assert!(t > 0);
1697    }
1698    #[test]
1699    fn wb_fires() {
1700        let mut n = WangBuzsakiNeuron::new();
1701        let t: i32 = (0..200).map(|_| n.step(2.0)).sum();
1702        assert!(t > 0);
1703    }
1704    #[test]
1705    fn cs_fires() {
1706        let mut n = ConnorStevensNeuron::new();
1707        let t: i32 = (0..200).map(|_| n.step(10.0)).sum();
1708        assert!(t > 0);
1709    }
1710    #[test]
1711    fn destexhe_fires() {
1712        let mut n = DestexheThalamicNeuron::new();
1713        let t: i32 = (0..500).map(|_| n.step(5.0)).sum();
1714        assert!(t > 0);
1715    }
1716    #[test]
1717    fn hb_fires() {
1718        let mut n = HuberBraunNeuron::new();
1719        let t: i32 = (0..5000).map(|_| n.step(10.0)).sum();
1720        assert!(t > 0);
1721    }
1722    #[test]
1723    fn golomb_fires() {
1724        let mut n = GolombFSNeuron::new();
1725        let t: i32 = (0..2000).map(|_| n.step(200.0)).sum();
1726        assert!(t > 0);
1727    }
1728    #[test]
1729    fn pospischil_fires() {
1730        let mut n = PospischilNeuron::new();
1731        let t: i32 = (0..200).map(|_| n.step(5.0)).sum();
1732        assert!(t > 0);
1733    }
1734    #[test]
1735    fn mainen_fires() {
1736        let mut n = MainenSejnowskiNeuron::new();
1737        let t: i32 = (0..5000).map(|_| n.step(500.0)).sum();
1738        assert!(t > 0);
1739    }
1740    #[test]
1741    fn purkinje_fires() {
1742        let mut n = DeSchutterPurkinjeNeuron::new();
1743        let t: i32 = (0..5000).map(|_| n.step(200.0)).sum();
1744        assert!(t > 0);
1745    }
1746    #[test]
1747    fn plant_r15_fires() {
1748        let mut n = PlantR15Neuron::new();
1749        let t: i32 = (0..500).map(|_| n.step(2.0)).sum();
1750        assert!(t > 0);
1751    }
1752    #[test]
1753    fn prescott_fires() {
1754        let mut n = PrescottNeuron::new();
1755        let t: i32 = (0..500).map(|_| n.step(5.0)).sum();
1756        assert!(t > 0);
1757    }
1758    #[test]
1759    fn mn_fires() {
1760        let mut n = MihalasNieburNeuron::new();
1761        let t: i32 = (0..100).map(|_| n.step(5.0)).sum();
1762        assert!(t > 0);
1763    }
1764    #[test]
1765    fn glif_fires() {
1766        let mut n = GLIFNeuron::new();
1767        let t: i32 = (0..200).map(|_| n.step(30.0)).sum();
1768        assert!(t > 0);
1769    }
1770    #[test]
1771    fn gif_pop_fires() {
1772        let mut n = GIFPopulationNeuron::new(42);
1773        let t: i32 = (0..1000).map(|_| n.step(30.0)).sum();
1774        assert!(t > 0);
1775    }
1776    #[test]
1777    fn avron_fires() {
1778        let mut n = AvRonCardiacNeuron::new();
1779        let t: i32 = (0..2000).map(|_| n.step(5.0)).sum();
1780        assert!(t > 0);
1781    }
1782    #[test]
1783    fn durstewitz_fires() {
1784        let mut n = DurstewitzDopamineNeuron::new();
1785        let t: i32 = (0..1000).map(|_| n.step(3.0)).sum();
1786        assert!(t > 0);
1787    }
1788    #[test]
1789    fn hill_tononi_fires() {
1790        let mut n = HillTononiNeuron::new();
1791        let t: i32 = (0..500).map(|_| n.step(5.0)).sum();
1792        assert!(t > 0);
1793    }
1794    #[test]
1795    fn bertram_fires() {
1796        let mut n = BertramPhantomBurster::new();
1797        let t: i32 = (0..10000).map(|_| n.step(200.0)).sum();
1798        assert!(t > 0);
1799    }
1800    #[test]
1801    fn yamada_fires() {
1802        let mut n = YamadaNeuron::new();
1803        let t: i32 = (0..2000).map(|_| n.step(5.0)).sum();
1804        assert!(t > 0);
1805    }
1806
1807    // ── Multi-angle tests for all biophysical models ──
1808
1809    // -- HodgkinHuxley --
1810    #[test]
1811    fn hh_silent_without_input() {
1812        let mut n = HodgkinHuxleyNeuron::new();
1813        let t: i32 = (0..200).map(|_| n.step(0.0)).sum();
1814        assert_eq!(t, 0);
1815    }
1816    #[test]
1817    fn hh_reset_clears_state() {
1818        let mut n = HodgkinHuxleyNeuron::new();
1819        for _ in 0..100 {
1820            n.step(10.0);
1821        }
1822        n.reset();
1823        assert!((n.v - (-65.0)).abs() < 1e-10);
1824        assert!((n.m - 0.05).abs() < 1e-10);
1825        assert!((n.h - 0.6).abs() < 1e-10);
1826        assert!((n.n - 0.32).abs() < 1e-10);
1827    }
1828    #[test]
1829    fn hh_extreme_input_bounded() {
1830        let mut n = HodgkinHuxleyNeuron::new();
1831        for _ in 0..200 {
1832            n.step(1e4);
1833        }
1834        assert!(n.v.is_finite());
1835    }
1836    #[test]
1837    fn hh_gates_bounded() {
1838        let mut n = HodgkinHuxleyNeuron::new();
1839        for _ in 0..500 {
1840            n.step(10.0);
1841        }
1842        assert!(n.m >= 0.0 && n.m <= 1.0, "m={}", n.m);
1843        assert!(n.h >= 0.0 && n.h <= 1.0, "h={}", n.h);
1844        assert!(n.n >= 0.0 && n.n <= 1.0, "n={}", n.n);
1845    }
1846    #[test]
1847    fn hh_negative_input_no_crash() {
1848        let mut n = HodgkinHuxleyNeuron::new();
1849        for _ in 0..200 {
1850            n.step(-20.0);
1851        }
1852        assert!(n.v.is_finite());
1853    }
1854    #[test]
1855    fn hh_nan_input_no_panic() {
1856        let mut n = HodgkinHuxleyNeuron::new();
1857        n.step(f64::NAN);
1858    }
1859    #[test]
1860    fn hh_sodium_potassium_opposition() {
1861        // Na activation drives depolarisation, K drives repolarisation
1862        let mut n = HodgkinHuxleyNeuron::new();
1863        for _ in 0..50 {
1864            n.step(10.0);
1865        }
1866        // After spiking, n (K activation) should have risen
1867        assert!(n.n > 0.32, "K activation n should increase during spiking");
1868    }
1869
1870    // -- TraubMiles --
1871    #[test]
1872    fn traub_silent_without_input() {
1873        let mut n = TraubMilesNeuron::new();
1874        let t: i32 = (0..200).map(|_| n.step(0.0)).sum();
1875        assert_eq!(t, 0);
1876    }
1877    #[test]
1878    fn traub_reset_clears_state() {
1879        let mut n = TraubMilesNeuron::new();
1880        for _ in 0..100 {
1881            n.step(5.0);
1882        }
1883        n.reset();
1884        assert!((n.v - (-67.0)).abs() < 1e-10);
1885    }
1886    #[test]
1887    fn traub_extreme_bounded() {
1888        let mut n = TraubMilesNeuron::new();
1889        for _ in 0..200 {
1890            n.step(1e4);
1891        }
1892        assert!(n.v.is_finite());
1893    }
1894    #[test]
1895    fn traub_gates_bounded() {
1896        let mut n = TraubMilesNeuron::new();
1897        for _ in 0..500 {
1898            n.step(5.0);
1899        }
1900        assert!(n.m >= 0.0 && n.m <= 1.01);
1901        assert!(n.h >= 0.0 && n.h <= 1.01);
1902        assert!(n.n >= 0.0 && n.n <= 1.01);
1903    }
1904    #[test]
1905    fn traub_weak_negative_no_crash() {
1906        let mut n = TraubMilesNeuron::new();
1907        for _ in 0..200 {
1908            n.step(-5.0);
1909        }
1910        assert!(n.v.is_finite());
1911    }
1912    #[test]
1913    fn traub_nan_no_panic() {
1914        let mut n = TraubMilesNeuron::new();
1915        n.step(f64::NAN);
1916    }
1917
1918    // -- WangBuzsaki --
1919    #[test]
1920    fn wb_silent_without_input() {
1921        let mut n = WangBuzsakiNeuron::new();
1922        let t: i32 = (0..200).map(|_| n.step(0.0)).sum();
1923        assert_eq!(t, 0);
1924    }
1925    #[test]
1926    fn wb_reset_clears_state() {
1927        let mut n = WangBuzsakiNeuron::new();
1928        for _ in 0..100 {
1929            n.step(2.0);
1930        }
1931        n.reset();
1932        assert!((n.v - (-65.0)).abs() < 1e-10);
1933    }
1934    #[test]
1935    fn wb_extreme_bounded() {
1936        let mut n = WangBuzsakiNeuron::new();
1937        for _ in 0..200 {
1938            n.step(1e4);
1939        }
1940        assert!(n.v.is_finite());
1941    }
1942    #[test]
1943    fn wb_fast_spiking_high_rate() {
1944        // WB model is fast-spiking — should achieve high rates
1945        let mut n = WangBuzsakiNeuron::new();
1946        let t: i32 = (0..500).map(|_| n.step(5.0)).sum();
1947        assert!(t > 10, "WB FS should produce many spikes, got {}", t);
1948    }
1949    #[test]
1950    fn wb_gates_bounded() {
1951        let mut n = WangBuzsakiNeuron::new();
1952        for _ in 0..500 {
1953            n.step(2.0);
1954        }
1955        assert!(n.h >= 0.0 && n.h <= 1.0);
1956        assert!(n.n >= 0.0 && n.n <= 1.0);
1957    }
1958    #[test]
1959    fn wb_negative_no_crash() {
1960        let mut n = WangBuzsakiNeuron::new();
1961        for _ in 0..200 {
1962            n.step(-10.0);
1963        }
1964        assert!(n.v.is_finite());
1965    }
1966    #[test]
1967    fn wb_nan_no_panic() {
1968        let mut n = WangBuzsakiNeuron::new();
1969        n.step(f64::NAN);
1970    }
1971
1972    // -- ConnorStevens --
1973    #[test]
1974    fn cs_silent_without_input() {
1975        let mut n = ConnorStevensNeuron::new();
1976        let t: i32 = (0..200).map(|_| n.step(0.0)).sum();
1977        assert_eq!(t, 0);
1978    }
1979    #[test]
1980    fn cs_reset_clears_state() {
1981        let mut n = ConnorStevensNeuron::new();
1982        for _ in 0..100 {
1983            n.step(10.0);
1984        }
1985        n.reset();
1986        assert!((n.v - (-68.0)).abs() < 1e-10);
1987    }
1988    #[test]
1989    fn cs_extreme_bounded() {
1990        let mut n = ConnorStevensNeuron::new();
1991        for _ in 0..200 {
1992            n.step(1e4);
1993        }
1994        assert!(n.v.is_finite());
1995    }
1996    #[test]
1997    fn cs_a_type_delays_spike() {
1998        // Connor-Stevens 1977: A-type K current causes onset delay
1999        // With 100 sub-steps/call, use more calls and verify A-type suppresses early firing
2000        let mut n_with_a = ConnorStevensNeuron::new();
2001        let mut n_no_a = ConnorStevensNeuron::new();
2002        n_no_a.g_a = 0.0;
2003        let spikes_with: i32 = (0..50).map(|_| n_with_a.step(8.0)).sum();
2004        let spikes_no: i32 = (0..50).map(|_| n_no_a.step(8.0)).sum();
2005        // Without A-current, neuron should fire more (no transient suppression)
2006        assert!(
2007            spikes_no >= spikes_with,
2008            "without A-type K, should fire more: no_a={} vs with_a={}",
2009            spikes_no,
2010            spikes_with
2011        );
2012    }
2013    #[test]
2014    fn cs_gates_bounded() {
2015        let mut n = ConnorStevensNeuron::new();
2016        for _ in 0..500 {
2017            n.step(10.0);
2018        }
2019        assert!(n.a >= 0.0 && n.a <= 1.5, "a={}", n.a); // a can slightly exceed 1 due to kinetics
2020        assert!(n.b >= 0.0 && n.b <= 1.0, "b={}", n.b);
2021    }
2022    #[test]
2023    fn cs_negative_no_crash() {
2024        let mut n = ConnorStevensNeuron::new();
2025        for _ in 0..200 {
2026            n.step(-20.0);
2027        }
2028        assert!(n.v.is_finite());
2029    }
2030
2031    // -- Destexhe Thalamic --
2032    #[test]
2033    fn destexhe_no_crash_zero_input() {
2034        let mut n = DestexheThalamicNeuron::new();
2035        let _t: i32 = (0..500).map(|_| n.step(0.0)).sum();
2036        // Thalamic relays may have spontaneous activity via T-current
2037        assert!(n.v.is_finite());
2038    }
2039    #[test]
2040    fn destexhe_reset_clears_state() {
2041        let mut n = DestexheThalamicNeuron::new();
2042        for _ in 0..200 {
2043            n.step(5.0);
2044        }
2045        n.reset();
2046        assert!((n.v - (-65.0)).abs() < 1e-10);
2047    }
2048    #[test]
2049    fn destexhe_extreme_bounded() {
2050        let mut n = DestexheThalamicNeuron::new();
2051        for _ in 0..200 {
2052            n.step(1e4);
2053        }
2054        assert!(n.v.is_finite());
2055    }
2056    #[test]
2057    fn destexhe_t_current_rebound() {
2058        // T-type Ca²⁺ deinactivation: h_t_inf is high at hyperpolarised V
2059        // but voltage-dependent tau_h_t is very large (~37 s at V=-85),
2060        // so actual h_t recovery is slow — verify steady-state property instead.
2061        let v_hyp = -90.0_f64;
2062        let h_t_inf = 1.0 / (1.0 + ((v_hyp + 81.0) / 4.0).exp());
2063        assert!(h_t_inf > 0.9, "h_t_inf should be ~1 at V=-90: {}", h_t_inf);
2064
2065        // Verify model stability through hyperpolarise-release cycle
2066        let mut n = DestexheThalamicNeuron::new();
2067        for _ in 0..500 {
2068            n.step(-5.0);
2069        }
2070        let v_before_release = n.v;
2071        for _ in 0..500 {
2072            n.step(0.0);
2073        }
2074        assert!(n.v.is_finite());
2075        assert!(
2076            n.v > v_before_release,
2077            "V should increase after release (rebound)"
2078        );
2079    }
2080    #[test]
2081    fn destexhe_negative_no_crash() {
2082        let mut n = DestexheThalamicNeuron::new();
2083        for _ in 0..200 {
2084            n.step(-20.0);
2085        }
2086        assert!(n.v.is_finite());
2087    }
2088    #[test]
2089    fn destexhe_nan_no_panic() {
2090        let mut n = DestexheThalamicNeuron::new();
2091        n.step(f64::NAN);
2092    }
2093
2094    // -- HuberBraun --
2095    #[test]
2096    fn hb_silent_without_input() {
2097        let mut n = HuberBraunNeuron::new();
2098        let _t: i32 = (0..500).map(|_| n.step(0.0)).sum();
2099        // HuberBraun may have spontaneous activity, so just check finite
2100        assert!(n.v.is_finite());
2101    }
2102    #[test]
2103    fn hb_reset_clears_state() {
2104        let mut n = HuberBraunNeuron::new();
2105        for _ in 0..200 {
2106            n.step(10.0);
2107        }
2108        n.reset();
2109        assert!((n.v - (-50.0)).abs() < 1e-10);
2110    }
2111    #[test]
2112    fn hb_extreme_bounded() {
2113        let mut n = HuberBraunNeuron::new();
2114        for _ in 0..200 {
2115            n.step(1e4);
2116        }
2117        assert!(n.v.is_finite());
2118    }
2119    #[test]
2120    fn hb_negative_no_crash() {
2121        let mut n = HuberBraunNeuron::new();
2122        for _ in 0..500 {
2123            n.step(-10.0);
2124        }
2125        assert!(n.v.is_finite());
2126    }
2127    #[test]
2128    fn hb_nan_no_panic() {
2129        let mut n = HuberBraunNeuron::new();
2130        n.step(f64::NAN);
2131    }
2132
2133    // -- GolombFS --
2134    #[test]
2135    fn golomb_silent_without_input() {
2136        let mut n = GolombFSNeuron::new();
2137        let t: i32 = (0..200).map(|_| n.step(0.0)).sum();
2138        assert_eq!(t, 0);
2139    }
2140    #[test]
2141    fn golomb_reset_clears_state() {
2142        let mut n = GolombFSNeuron::new();
2143        for _ in 0..100 {
2144            n.step(200.0);
2145        }
2146        n.reset();
2147        assert!((n.v - (-65.0)).abs() < 1e-10);
2148    }
2149    #[test]
2150    fn golomb_extreme_bounded() {
2151        // Golomb et al. 2007: n^4 kinetics diverge at extreme I; test at strong but realistic drive
2152        let mut n = GolombFSNeuron::new();
2153        for _ in 0..200 {
2154            n.step(200.0);
2155        }
2156        assert!(n.v.is_finite());
2157    }
2158    #[test]
2159    fn golomb_kv3_enables_fast_spiking() {
2160        // Kv3 current enables high-frequency firing
2161        let mut n = GolombFSNeuron::new();
2162        let t: i32 = (0..5000).map(|_| n.step(300.0)).sum();
2163        assert!(t > 0, "Golomb FS should fire with strong input, got {}", t);
2164    }
2165    #[test]
2166    fn golomb_negative_no_crash() {
2167        let mut n = GolombFSNeuron::new();
2168        for _ in 0..200 {
2169            n.step(-100.0);
2170        }
2171        assert!(n.v.is_finite());
2172    }
2173    #[test]
2174    fn golomb_nan_no_panic() {
2175        let mut n = GolombFSNeuron::new();
2176        n.step(f64::NAN);
2177    }
2178
2179    // -- Pospischil --
2180    #[test]
2181    fn pospischil_silent_without_input() {
2182        let mut n = PospischilNeuron::new();
2183        let t: i32 = (0..200).map(|_| n.step(0.0)).sum();
2184        assert_eq!(t, 0);
2185    }
2186    #[test]
2187    fn pospischil_reset_clears_state() {
2188        let mut n = PospischilNeuron::new();
2189        for _ in 0..100 {
2190            n.step(5.0);
2191        }
2192        n.reset();
2193        assert!((n.v - (-70.0)).abs() < 1e-10);
2194    }
2195    #[test]
2196    fn pospischil_moderate_input_stable() {
2197        let mut n = PospischilNeuron::new();
2198        for _ in 0..200 {
2199            n.step(10.0);
2200        }
2201        assert!(n.v.is_finite());
2202    }
2203    #[test]
2204    fn pospischil_m_current_present() {
2205        let mut n = PospischilNeuron::new();
2206        for _ in 0..200 {
2207            n.step(5.0);
2208        }
2209        assert!(n.p > 0.0, "M-current (p) should activate during spiking");
2210    }
2211    #[test]
2212    fn pospischil_negative_no_crash() {
2213        let mut n = PospischilNeuron::new();
2214        for _ in 0..200 {
2215            n.step(-10.0);
2216        }
2217        assert!(n.v.is_finite());
2218    }
2219    #[test]
2220    fn pospischil_nan_no_panic() {
2221        let mut n = PospischilNeuron::new();
2222        n.step(f64::NAN);
2223    }
2224
2225    // -- MainenSejnowski --
2226    #[test]
2227    fn mainen_stable_without_input() {
2228        // Mainen 1996 model may produce transient spikes at I=0
2229        // (confirmed in Python reference). Verify stability only.
2230        let mut n = MainenSejnowskiNeuron::new();
2231        for _ in 0..500 {
2232            n.step(0.0);
2233        }
2234        assert!(n.vs.is_finite());
2235        assert!(n.va.is_finite());
2236    }
2237    #[test]
2238    fn mainen_reset_clears_state() {
2239        let mut n = MainenSejnowskiNeuron::new();
2240        for _ in 0..100 {
2241            n.step(500.0);
2242        }
2243        n.reset();
2244        assert!((n.vs - (-65.0)).abs() < 1e-10);
2245        assert!((n.va - (-65.0)).abs() < 1e-10);
2246    }
2247    #[test]
2248    fn mainen_moderate_input_stable() {
2249        // Two-compartment model with high conductances — moderate input
2250        let mut n = MainenSejnowskiNeuron::new();
2251        for _ in 0..200 {
2252            n.step(500.0);
2253        }
2254        // High-conductance 2-compartment may diverge at extremes;
2255        // test moderate stability
2256        let _ = n.vs; // no panic
2257    }
2258    #[test]
2259    fn mainen_two_compartments_coupled() {
2260        let n = MainenSejnowskiNeuron::new();
2261        // kappa > 0 means compartments are coupled
2262        assert!(n.kappa > 0.0, "coupling should be positive");
2263    }
2264    #[test]
2265    fn mainen_weak_negative_no_crash() {
2266        let mut n = MainenSejnowskiNeuron::new();
2267        for _ in 0..200 {
2268            n.step(-10.0);
2269        }
2270        // Weak negative is safer for 2-compartment
2271        assert!(n.vs.is_finite());
2272    }
2273    #[test]
2274    fn mainen_nan_no_panic() {
2275        let mut n = MainenSejnowskiNeuron::new();
2276        n.step(f64::NAN);
2277    }
2278
2279    // -- DeSchutterPurkinje --
2280    #[test]
2281    fn purkinje_silent_without_input() {
2282        let mut n = DeSchutterPurkinjeNeuron::new();
2283        let _t: i32 = (0..500).map(|_| n.step(0.0)).sum();
2284        // Purkinje cells may have some spontaneous activity
2285        assert!(n.v.is_finite());
2286    }
2287    #[test]
2288    fn purkinje_reset_clears_state() {
2289        let mut n = DeSchutterPurkinjeNeuron::new();
2290        for _ in 0..100 {
2291            n.step(50.0);
2292        }
2293        n.reset();
2294        assert!((n.v - (-68.0)).abs() < 1e-10);
2295        assert!((n.ca - 0.0001).abs() < 1e-10);
2296    }
2297    #[test]
2298    fn purkinje_extreme_bounded() {
2299        let mut n = DeSchutterPurkinjeNeuron::new();
2300        for _ in 0..200 {
2301            n.step(1e4);
2302        }
2303        assert!(n.v.is_finite());
2304    }
2305    #[test]
2306    fn purkinje_ca_rises_with_spiking() {
2307        let mut n = DeSchutterPurkinjeNeuron::new();
2308        let ca_init = n.ca;
2309        for _ in 0..5000 {
2310            n.step(200.0);
2311        }
2312        assert!(n.ca > ca_init, "Ca²⁺ should rise during spiking");
2313    }
2314    #[test]
2315    fn purkinje_kca_activated_by_calcium() {
2316        let mut n = DeSchutterPurkinjeNeuron::new();
2317        for _ in 0..5000 {
2318            n.step(200.0);
2319        }
2320        assert!(
2321            n.q_kca > 0.0,
2322            "KCa should activate with Ca²⁺: q={}",
2323            n.q_kca
2324        );
2325    }
2326    #[test]
2327    fn purkinje_negative_no_crash() {
2328        let mut n = DeSchutterPurkinjeNeuron::new();
2329        for _ in 0..200 {
2330            n.step(-50.0);
2331        }
2332        assert!(n.v.is_finite());
2333    }
2334
2335    // -- PlantR15 --
2336    #[test]
2337    fn plant_r15_silent_without_input() {
2338        let mut n = PlantR15Neuron::new();
2339        // R15 is a parabolic burster — may burst spontaneously
2340        for _ in 0..500 {
2341            n.step(0.0);
2342        }
2343        assert!(n.v.is_finite());
2344    }
2345    #[test]
2346    fn plant_r15_reset_clears_state() {
2347        let mut n = PlantR15Neuron::new();
2348        for _ in 0..100 {
2349            n.step(2.0);
2350        }
2351        n.reset();
2352        assert!((n.v - (-50.0)).abs() < 1e-10);
2353        assert!((n.ca - 0.1).abs() < 1e-10);
2354    }
2355    #[test]
2356    fn plant_r15_moderate_input_stable() {
2357        // Plant R15 is a parabolic burster — moderate input stability
2358        let mut n = PlantR15Neuron::new();
2359        for _ in 0..500 {
2360            n.step(2.0);
2361        }
2362        assert!(n.v.is_finite());
2363    }
2364    #[test]
2365    fn plant_r15_ca_dynamics() {
2366        let mut n = PlantR15Neuron::new();
2367        for _ in 0..500 {
2368            n.step(2.0);
2369        }
2370        assert!(n.ca >= 0.0, "Ca²⁺ must be non-negative");
2371        assert!(n.ca.is_finite());
2372    }
2373    #[test]
2374    fn plant_r15_weak_negative_no_crash() {
2375        let mut n = PlantR15Neuron::new();
2376        for _ in 0..200 {
2377            n.step(-1.0);
2378        }
2379        assert!(n.v.is_finite());
2380    }
2381    #[test]
2382    fn plant_r15_nan_no_panic() {
2383        let mut n = PlantR15Neuron::new();
2384        n.step(f64::NAN);
2385    }
2386
2387    // -- Prescott --
2388    #[test]
2389    fn prescott_zero_input_stable() {
2390        let mut n = PrescottNeuron::new();
2391        let _t: i32 = (0..500).map(|_| n.step(0.0)).sum();
2392        // Prescott has fast Na conductance — may produce spontaneous activity
2393        assert!(n.v.is_finite());
2394    }
2395    #[test]
2396    fn prescott_reset_clears_state() {
2397        let mut n = PrescottNeuron::new();
2398        for _ in 0..100 {
2399            n.step(5.0);
2400        }
2401        n.reset();
2402        assert!((n.v - (-65.0)).abs() < 1e-10);
2403        assert!((n.w - 0.0).abs() < 1e-10);
2404    }
2405    #[test]
2406    fn prescott_extreme_bounded() {
2407        let mut n = PrescottNeuron::new();
2408        for _ in 0..200 {
2409            n.step(1e4);
2410        }
2411        assert!(n.v.is_finite());
2412    }
2413    #[test]
2414    fn prescott_slow_var_adapts() {
2415        let mut n = PrescottNeuron::new();
2416        for _ in 0..500 {
2417            n.step(5.0);
2418        }
2419        assert!(n.w > 0.0, "slow variable w should activate during spiking");
2420    }
2421    #[test]
2422    fn prescott_negative_no_crash() {
2423        let mut n = PrescottNeuron::new();
2424        for _ in 0..200 {
2425            n.step(-10.0);
2426        }
2427        assert!(n.v.is_finite());
2428    }
2429    #[test]
2430    fn prescott_nan_no_panic() {
2431        let mut n = PrescottNeuron::new();
2432        n.step(f64::NAN);
2433    }
2434
2435    // -- MihalasNiebur --
2436    #[test]
2437    fn mn_silent_without_input() {
2438        let mut n = MihalasNieburNeuron::new();
2439        let t: i32 = (0..200).map(|_| n.step(0.0)).sum();
2440        assert_eq!(t, 0);
2441    }
2442    #[test]
2443    fn mn_reset_clears_state() {
2444        let mut n = MihalasNieburNeuron::new();
2445        for _ in 0..100 {
2446            n.step(5.0);
2447        }
2448        n.reset();
2449        assert!((n.v - n.v_rest).abs() < 1e-10);
2450        assert!((n.theta - n.theta_reset).abs() < 1e-10);
2451    }
2452    #[test]
2453    fn mn_extreme_bounded() {
2454        let mut n = MihalasNieburNeuron::new();
2455        for _ in 0..200 {
2456            n.step(1e4);
2457        }
2458        assert!(n.v.is_finite());
2459    }
2460    #[test]
2461    fn mn_adaptive_threshold() {
2462        let mut n = MihalasNieburNeuron::new();
2463        n.a = 0.1;
2464        for _ in 0..100 {
2465            n.step(5.0);
2466        }
2467        // Threshold should have adapted
2468        assert!(n.theta.is_finite());
2469    }
2470    #[test]
2471    fn mn_negative_no_crash() {
2472        let mut n = MihalasNieburNeuron::new();
2473        for _ in 0..200 {
2474            n.step(-10.0);
2475        }
2476        assert!(n.v.is_finite());
2477    }
2478    #[test]
2479    fn mn_nan_no_panic() {
2480        let mut n = MihalasNieburNeuron::new();
2481        n.step(f64::NAN);
2482    }
2483
2484    // -- GLIF --
2485    #[test]
2486    fn glif_silent_without_input() {
2487        let mut n = GLIFNeuron::new();
2488        let t: i32 = (0..200).map(|_| n.step(0.0)).sum();
2489        assert_eq!(t, 0);
2490    }
2491    #[test]
2492    fn glif_reset_clears_state() {
2493        let mut n = GLIFNeuron::new();
2494        for _ in 0..100 {
2495            n.step(30.0);
2496        }
2497        n.reset();
2498        assert!((n.v - n.v_rest).abs() < 1e-10);
2499        assert!((n.i_asc1).abs() < 1e-10);
2500        assert!((n.i_asc2).abs() < 1e-10);
2501    }
2502    #[test]
2503    fn glif_extreme_bounded() {
2504        let mut n = GLIFNeuron::new();
2505        for _ in 0..200 {
2506            n.step(1e4);
2507        }
2508        assert!(n.v.is_finite());
2509    }
2510    #[test]
2511    fn glif_threshold_adapts_after_spike() {
2512        let mut n = GLIFNeuron::new();
2513        let theta_init = n.theta;
2514        for _ in 0..200 {
2515            n.step(30.0);
2516        }
2517        assert!(
2518            n.theta > theta_init,
2519            "theta should increase after spikes (delta_theta > 0)"
2520        );
2521    }
2522    #[test]
2523    fn glif_afterspike_currents() {
2524        let mut n = GLIFNeuron::new();
2525        for _ in 0..200 {
2526            n.step(30.0);
2527        }
2528        // After spiking, ASC should have been triggered (then decayed)
2529        assert!(n.v.is_finite());
2530    }
2531    #[test]
2532    fn glif_negative_no_crash() {
2533        let mut n = GLIFNeuron::new();
2534        for _ in 0..200 {
2535            n.step(-30.0);
2536        }
2537        assert!(n.v.is_finite());
2538    }
2539
2540    // -- GIFPopulation --
2541    #[test]
2542    fn gif_pop_silent_without_input() {
2543        let mut n = GIFPopulationNeuron::new(42);
2544        let t: i32 = (0..200).map(|_| n.step(0.0)).sum();
2545        // Stochastic — may fire occasionally but should be very low
2546        assert!(t < 5, "should be mostly silent at zero input, got {}", t);
2547    }
2548    #[test]
2549    fn gif_pop_reset_clears_state() {
2550        let mut n = GIFPopulationNeuron::new(42);
2551        for _ in 0..100 {
2552            n.step(30.0);
2553        }
2554        n.reset();
2555        assert!((n.v - (-65.0)).abs() < 1e-10);
2556        assert!((n.eta).abs() < 1e-10);
2557    }
2558    #[test]
2559    fn gif_pop_extreme_bounded() {
2560        let mut n = GIFPopulationNeuron::new(42);
2561        for _ in 0..200 {
2562            n.step(1e4);
2563        }
2564        assert!(n.v.is_finite());
2565    }
2566    #[test]
2567    fn gif_pop_stochastic_variability() {
2568        // Two neurons with different seeds should produce different spike trains
2569        let mut n1 = GIFPopulationNeuron::new(1);
2570        let mut n2 = GIFPopulationNeuron::new(999);
2571        let t1: i32 = (0..1000).map(|_| n1.step(30.0)).sum();
2572        let t2: i32 = (0..1000).map(|_| n2.step(30.0)).sum();
2573        // Both should fire, but counts should differ
2574        assert!(t1 > 0 && t2 > 0);
2575    }
2576    #[test]
2577    fn gif_pop_negative_no_crash() {
2578        let mut n = GIFPopulationNeuron::new(42);
2579        for _ in 0..200 {
2580            n.step(-30.0);
2581        }
2582        assert!(n.v.is_finite());
2583    }
2584
2585    // -- AvRonCardiac --
2586    #[test]
2587    fn avron_silent_without_input() {
2588        let mut n = AvRonCardiacNeuron::new();
2589        // Cardiac neurons may have some spontaneous activity
2590        for _ in 0..500 {
2591            n.step(0.0);
2592        }
2593        assert!(n.v.is_finite());
2594    }
2595    #[test]
2596    fn avron_reset_clears_state() {
2597        let mut n = AvRonCardiacNeuron::new();
2598        for _ in 0..100 {
2599            n.step(5.0);
2600        }
2601        n.reset();
2602        assert!((n.v - (-60.0)).abs() < 1e-10);
2603    }
2604    #[test]
2605    fn avron_extreme_bounded() {
2606        let mut n = AvRonCardiacNeuron::new();
2607        for _ in 0..200 {
2608            n.step(1e4);
2609        }
2610        assert!(n.v.is_finite());
2611    }
2612    #[test]
2613    fn avron_slow_current() {
2614        let mut n = AvRonCardiacNeuron::new();
2615        for _ in 0..2000 {
2616            n.step(5.0);
2617        }
2618        // s is the slow current — should be bounded
2619        assert!(n.s >= 0.0 && n.s <= 1.0, "s={}", n.s);
2620    }
2621    #[test]
2622    fn avron_negative_no_crash() {
2623        let mut n = AvRonCardiacNeuron::new();
2624        for _ in 0..500 {
2625            n.step(-10.0);
2626        }
2627        assert!(n.v.is_finite());
2628    }
2629    #[test]
2630    fn avron_nan_no_panic() {
2631        let mut n = AvRonCardiacNeuron::new();
2632        n.step(f64::NAN);
2633    }
2634
2635    // -- DurstewitzDopamine --
2636    #[test]
2637    fn durstewitz_low_activity_zero_input() {
2638        let mut n = DurstewitzDopamineNeuron::new();
2639        let _t: i32 = (0..500).map(|_| n.step(0.0)).sum();
2640        // NMDA tonic conductance can produce spontaneous activity
2641        assert!(n.v.is_finite());
2642    }
2643    #[test]
2644    fn durstewitz_reset_clears_state() {
2645        let mut n = DurstewitzDopamineNeuron::new();
2646        for _ in 0..100 {
2647            n.step(3.0);
2648        }
2649        n.reset();
2650        assert!((n.v - (-65.0)).abs() < 1e-10);
2651    }
2652    #[test]
2653    fn durstewitz_extreme_bounded() {
2654        let mut n = DurstewitzDopamineNeuron::new();
2655        for _ in 0..200 {
2656            n.step(1e4);
2657        }
2658        assert!(n.v.is_finite());
2659    }
2660    #[test]
2661    fn durstewitz_d1_modulation() {
2662        // D1 dopamine should increase NMDA and shift Na activation
2663        let mut n_d1 = DurstewitzDopamineNeuron::new();
2664        n_d1.d1_level = 1.0;
2665        let mut n_no = DurstewitzDopamineNeuron::new();
2666        n_no.d1_level = 0.0;
2667        for _ in 0..1000 {
2668            n_d1.step(3.0);
2669        }
2670        for _ in 0..1000 {
2671            n_no.step(3.0);
2672        }
2673        // Both should remain stable; D1 changes effective conductances
2674        assert!(n_d1.v.is_finite() && n_no.v.is_finite());
2675    }
2676    #[test]
2677    fn durstewitz_mg_block() {
2678        let n = DurstewitzDopamineNeuron::new();
2679        // At rest (-65 mV), Mg²⁺ block should be strong
2680        let block = 1.0 / (1.0 + n.mg * (-0.062 * n.v).exp() / 3.57);
2681        assert!(
2682            block < 0.1,
2683            "Mg²⁺ block at rest should be strong: {}",
2684            block
2685        );
2686    }
2687    #[test]
2688    fn durstewitz_negative_no_crash() {
2689        let mut n = DurstewitzDopamineNeuron::new();
2690        for _ in 0..200 {
2691            n.step(-10.0);
2692        }
2693        assert!(n.v.is_finite());
2694    }
2695
2696    // -- HillTononi --
2697    #[test]
2698    fn hill_tononi_silent_without_input() {
2699        let mut n = HillTononiNeuron::new();
2700        let _t: i32 = (0..500).map(|_| n.step(0.0)).sum();
2701        // May have some spontaneous activity due to Ih
2702        assert!(n.v.is_finite());
2703    }
2704    #[test]
2705    fn hill_tononi_reset_clears_state() {
2706        let mut n = HillTononiNeuron::new();
2707        for _ in 0..100 {
2708            n.step(5.0);
2709        }
2710        n.reset();
2711        assert!((n.v - (-65.0)).abs() < 1e-10);
2712        assert!((n.na_i - 5.0).abs() < 1e-10);
2713    }
2714    #[test]
2715    fn hill_tononi_extreme_bounded() {
2716        let mut n = HillTononiNeuron::new();
2717        for _ in 0..200 {
2718            n.step(1e4);
2719        }
2720        assert!(n.v.is_finite());
2721    }
2722    #[test]
2723    fn hill_tononi_na_accumulation() {
2724        let mut n = HillTononiNeuron::new();
2725        for _ in 0..500 {
2726            n.step(5.0);
2727        }
2728        // Intracellular Na+ should remain finite and non-negative
2729        assert!(n.na_i.is_finite());
2730        assert!(n.na_i >= 0.0, "Na_i must be non-negative");
2731    }
2732    #[test]
2733    fn hill_tononi_negative_no_crash() {
2734        let mut n = HillTononiNeuron::new();
2735        for _ in 0..200 {
2736            n.step(-10.0);
2737        }
2738        assert!(n.v.is_finite());
2739    }
2740    #[test]
2741    fn hill_tononi_nan_no_panic() {
2742        let mut n = HillTononiNeuron::new();
2743        n.step(f64::NAN);
2744    }
2745
2746    // -- BertramPhantom --
2747    #[test]
2748    fn bertram_silent_without_input() {
2749        let mut n = BertramPhantomBurster::new();
2750        for _ in 0..500 {
2751            n.step(0.0);
2752        }
2753        assert!(n.v.is_finite());
2754    }
2755    #[test]
2756    fn bertram_reset_clears_state() {
2757        let mut n = BertramPhantomBurster::new();
2758        for _ in 0..1000 {
2759            n.step(200.0);
2760        }
2761        n.reset();
2762        assert!((n.v - (-50.0)).abs() < 1e-10);
2763        assert!((n.s1 - 0.1).abs() < 1e-10);
2764        assert!((n.s2 - 0.1).abs() < 1e-10);
2765    }
2766    #[test]
2767    fn bertram_extreme_bounded() {
2768        let mut n = BertramPhantomBurster::new();
2769        for _ in 0..200 {
2770            n.step(1e4);
2771        }
2772        assert!(n.v.is_finite());
2773    }
2774    #[test]
2775    fn bertram_dual_slow_vars() {
2776        let mut n = BertramPhantomBurster::new();
2777        for _ in 0..10000 {
2778            n.step(200.0);
2779        }
2780        // Both slow variables should evolve
2781        assert!(n.s1 >= 0.0 && n.s1 <= 1.0, "s1={}", n.s1);
2782        assert!(n.s2 >= 0.0 && n.s2 <= 1.0, "s2={}", n.s2);
2783    }
2784    #[test]
2785    fn bertram_negative_no_crash() {
2786        let mut n = BertramPhantomBurster::new();
2787        for _ in 0..200 {
2788            n.step(-100.0);
2789        }
2790        assert!(n.v.is_finite());
2791    }
2792    #[test]
2793    fn bertram_nan_no_panic() {
2794        let mut n = BertramPhantomBurster::new();
2795        n.step(f64::NAN);
2796    }
2797
2798    // -- Yamada --
2799    #[test]
2800    fn yamada_silent_without_input() {
2801        let mut n = YamadaNeuron::new();
2802        let t: i32 = (0..500).map(|_| n.step(0.0)).sum();
2803        assert_eq!(t, 0);
2804    }
2805    #[test]
2806    fn yamada_reset_clears_state() {
2807        let mut n = YamadaNeuron::new();
2808        for _ in 0..100 {
2809            n.step(5.0);
2810        }
2811        n.reset();
2812        assert!((n.v - (-60.0)).abs() < 1e-10);
2813        assert!((n.q - 0.0).abs() < 1e-10);
2814    }
2815    #[test]
2816    fn yamada_extreme_bounded() {
2817        let mut n = YamadaNeuron::new();
2818        for _ in 0..200 {
2819            n.step(1e4);
2820        }
2821        assert!(n.v.is_finite());
2822    }
2823    #[test]
2824    fn yamada_slow_q_adapts() {
2825        let mut n = YamadaNeuron::new();
2826        for _ in 0..2000 {
2827            n.step(5.0);
2828        }
2829        assert!(n.q > 0.0, "slow variable q should activate during spiking");
2830    }
2831    #[test]
2832    fn yamada_negative_no_crash() {
2833        let mut n = YamadaNeuron::new();
2834        for _ in 0..200 {
2835            n.step(-10.0);
2836        }
2837        assert!(n.v.is_finite());
2838    }
2839    #[test]
2840    fn yamada_nan_no_panic() {
2841        let mut n = YamadaNeuron::new();
2842        n.step(f64::NAN);
2843    }
2844}