Skip to main content

sc_neurocore_engine/neurons/
channels.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 — Ion Channel Variant Neuron Models
8
9//! Neuron models defined by a single additional ion channel beyond base HH.
10//!
11//! Ion-channel variant model group: persistent Na+, Ih, T-type Ca2+, A-type K+, BK, SK, NMDA.
12//! Each model demonstrates one biophysical mechanism on a WB Na+/K+ base.
13//! Added one by one with full 7-point checklist verification.
14
15use super::biophysical::safe_rate;
16
17// ═══════════════════════════════════════════════════════════════════
18// Persistent Na+ Neuron
19// ═══════════════════════════════════════════════════════════════════
20
21/// Persistent Na+ (INaP) neuron — WB base + non-inactivating Na+ current.
22///
23/// INaP activates at subthreshold voltages (-60 to -40 mV) and does not
24/// inactivate, providing a sustained depolarising drive. Key mechanism for:
25/// - Subthreshold membrane oscillations (entorhinal cortex, layer II stellate)
26/// - Plateau potentials and bistability (spinal motoneurons)
27/// - Amplification of synaptic inputs near threshold
28/// - Burst generation in respiratory neurons (pre-Bötzinger complex)
29///
30/// Crill, Annu Rev Physiol 58:349, 1996; French et al., Neuroscience 42:363, 1990.
31#[derive(Clone, Debug)]
32pub struct PersistentNaNeuron {
33    pub v: f64,
34    pub h: f64, // Transient Na+ inactivation
35    pub n: f64, // Kdr activation
36    pub p: f64, // INaP activation (slow)
37    // Conductances (mS/cm²)
38    pub g_na: f64,  // Transient Na+
39    pub g_nap: f64, // Persistent Na+
40    pub g_k: f64,   // Kdr
41    pub g_l: f64,
42    // Reversal potentials (mV)
43    pub e_na: f64,
44    pub e_k: f64,
45    pub e_l: f64,
46    pub c_m: f64,
47    pub phi: f64,
48    pub dt: f64,
49    pub v_threshold: f64,
50    pub gain: f64,
51}
52
53impl Default for PersistentNaNeuron {
54    fn default() -> Self {
55        Self::new()
56    }
57}
58
59impl PersistentNaNeuron {
60    pub fn new() -> Self {
61        Self {
62            v: -65.0,
63            h: 0.6,
64            n: 0.32,
65            p: 0.0,
66            g_na: 35.0,
67            g_nap: 0.15, // Persistent Na+ — small but significant
68            g_k: 9.0,
69            g_l: 0.3, // Higher leak to counteract INaP window current
70            e_na: 55.0,
71            e_k: -90.0,
72            e_l: -65.0,
73            c_m: 1.0,
74            phi: 5.0,
75            dt: 0.5,
76            v_threshold: -20.0,
77            gain: 1.0,
78        }
79    }
80
81    pub fn step(&mut self, current: f64) -> i32 {
82        let input = self.gain * current;
83        let sub_steps = 50;
84        let sub_dt = self.dt / sub_steps as f64;
85        let mut fired = 0i32;
86
87        for _ in 0..sub_steps {
88            let v = self.v;
89
90            // WB alpha/beta rates
91            let alpha_m = safe_rate(0.1, 35.0, v, 10.0, 1.0);
92            let beta_m = 4.0 * (-(v + 60.0) / 18.0).exp();
93            let m_inf = alpha_m / (alpha_m + beta_m);
94
95            let alpha_h = 0.07 * (-(v + 58.0) / 20.0).exp();
96            let beta_h = 1.0 / (1.0 + (-(v + 28.0) / 10.0).exp());
97
98            let alpha_n = safe_rate(0.01, 34.0, v, 10.0, 0.1);
99            let beta_n = 0.125 * (-(v + 44.0) / 80.0).exp();
100
101            // Persistent Na+ gating: slow activation, no inactivation
102            // Half-activation at -48 mV (subthreshold), tau ~10-50 ms
103            let p_inf = 1.0 / (1.0 + (-(v + 48.0) / 5.0).exp());
104            let tau_p = 10.0 + 40.0 / (1.0 + ((v + 48.0) / 10.0).powi(2));
105
106            // Gate updates
107            self.h += sub_dt * self.phi * (alpha_h * (1.0 - self.h) - beta_h * self.h);
108            self.n += sub_dt * self.phi * (alpha_n * (1.0 - self.n) - beta_n * self.n);
109            self.p += sub_dt * (p_inf - self.p) / tau_p;
110
111            // Currents
112            let i_na = self.g_na * m_inf.powi(3) * self.h * (v - self.e_na);
113            let i_nap = self.g_nap * self.p * (v - self.e_na);
114            let i_k = self.g_k * self.n.powi(4) * (v - self.e_k);
115            let i_l = self.g_l * (v - self.e_l);
116
117            let dv = (-i_na - i_nap - i_k - i_l + input) / self.c_m;
118            self.v += sub_dt * dv;
119
120            if self.v >= self.v_threshold {
121                fired = 1;
122                self.v = -65.0;
123            }
124        }
125
126        // Safety bounds
127        self.v = self.v.clamp(-100.0, 60.0);
128        if !self.v.is_finite() {
129            self.v = -65.0;
130            self.h = 0.6;
131            self.n = 0.32;
132        }
133        self.h = self.h.clamp(0.0, 1.0);
134        self.n = self.n.clamp(0.0, 1.0);
135        self.p = self.p.clamp(0.0, 1.0);
136
137        fired
138    }
139
140    pub fn reset(&mut self) {
141        *self = Self::new();
142    }
143}
144
145// ═══════════════════════════════════════════════════════════════════
146// Ih (HCN) Neuron
147// ═══════════════════════════════════════════════════════════════════
148
149/// Ih (hyperpolarisation-activated cation current) neuron — WB base + HCN.
150///
151/// Ih activates upon hyperpolarisation (opposite to most voltage-gated
152/// channels) and conducts a mixed Na+/K+ current with reversal ~-40 mV.
153/// Key mechanism for:
154/// - Voltage sag: during hyperpolarisation, Ih activates and depolarises
155///   the cell back towards rest (sag potential)
156/// - Rebound excitation: Ih accumulated during inhibition depolarises
157///   the cell after inhibition ends, triggering rebound spikes
158/// - Pacemaker oscillations: interplay of Ih and T-type Ca2+ in thalamic
159///   relay neurons drives rhythmic bursting
160///
161/// Robinson & Bhatt, Neuron 11:953, 1993; Pape, Annu Rev Physiol 58:299, 1996.
162#[derive(Clone, Debug)]
163pub struct IhNeuron {
164    pub v: f64,
165    pub h: f64, // Na+ inactivation
166    pub n: f64, // Kdr activation
167    pub r: f64, // Ih activation (activates on hyperpolarisation)
168    // Conductances (mS/cm²)
169    pub g_na: f64,
170    pub g_k: f64,
171    pub g_h: f64, // Ih conductance
172    pub g_l: f64,
173    // Reversal potentials (mV)
174    pub e_na: f64,
175    pub e_k: f64,
176    pub e_h: f64, // Ih reversal (~-40 mV, mixed cation)
177    pub e_l: f64,
178    pub c_m: f64,
179    pub phi: f64,
180    pub dt: f64,
181    pub v_threshold: f64,
182    pub gain: f64,
183}
184
185impl Default for IhNeuron {
186    fn default() -> Self {
187        Self::new()
188    }
189}
190
191impl IhNeuron {
192    pub fn new() -> Self {
193        Self {
194            v: -65.0,
195            h: 0.6,
196            n: 0.32,
197            r: 0.1,
198            g_na: 35.0,
199            g_k: 9.0,
200            g_h: 0.15,
201            g_l: 0.2,
202            e_na: 55.0,
203            e_k: -90.0,
204            e_h: -40.0,
205            e_l: -65.0,
206            c_m: 1.0,
207            phi: 5.0,
208            dt: 0.5,
209            v_threshold: -20.0,
210            gain: 1.0,
211        }
212    }
213
214    pub fn step(&mut self, current: f64) -> i32 {
215        let input = self.gain * current;
216        let sub_steps = 50;
217        let sub_dt = self.dt / sub_steps as f64;
218        let mut fired = 0i32;
219
220        for _ in 0..sub_steps {
221            let v = self.v;
222
223            // WB alpha/beta rates
224            let alpha_m = safe_rate(0.1, 35.0, v, 10.0, 1.0);
225            let beta_m = 4.0 * (-(v + 60.0) / 18.0).exp();
226            let m_inf = alpha_m / (alpha_m + beta_m);
227
228            let alpha_h = 0.07 * (-(v + 58.0) / 20.0).exp();
229            let beta_h = 1.0 / (1.0 + (-(v + 28.0) / 10.0).exp());
230
231            let alpha_n = safe_rate(0.01, 34.0, v, 10.0, 0.1);
232            let beta_n = 0.125 * (-(v + 44.0) / 80.0).exp();
233
234            // Ih gating: activates on hyperpolarisation
235            // Half-activation ~-80 mV, slow kinetics (100-300 ms)
236            let r_inf = 1.0 / (1.0 + ((v + 80.0) / 10.0).exp());
237            let tau_r = 100.0 + 200.0 / (1.0 + ((v + 70.0) / 10.0).exp());
238
239            // Gate updates
240            self.h += sub_dt * self.phi * (alpha_h * (1.0 - self.h) - beta_h * self.h);
241            self.n += sub_dt * self.phi * (alpha_n * (1.0 - self.n) - beta_n * self.n);
242            self.r += sub_dt * (r_inf - self.r) / tau_r;
243
244            // Currents
245            let i_na = self.g_na * m_inf.powi(3) * self.h * (v - self.e_na);
246            let i_k = self.g_k * self.n.powi(4) * (v - self.e_k);
247            let i_h = self.g_h * self.r * (v - self.e_h);
248            let i_l = self.g_l * (v - self.e_l);
249
250            let dv = (-i_na - i_k - i_h - i_l + input) / self.c_m;
251            self.v += sub_dt * dv;
252
253            if self.v >= self.v_threshold {
254                fired = 1;
255                self.v = -65.0;
256            }
257        }
258
259        // Safety bounds
260        self.v = self.v.clamp(-100.0, 60.0);
261        if !self.v.is_finite() {
262            self.v = -65.0;
263            self.h = 0.6;
264            self.n = 0.32;
265        }
266        self.h = self.h.clamp(0.0, 1.0);
267        self.n = self.n.clamp(0.0, 1.0);
268        self.r = self.r.clamp(0.0, 1.0);
269
270        fired
271    }
272
273    pub fn reset(&mut self) {
274        *self = Self::new();
275    }
276}
277
278// ═══════════════════════════════════════════════════════════════════
279// T-type Ca2+ Neuron
280// ═══════════════════════════════════════════════════════════════════
281
282/// T-type Ca2+ (IT) neuron — WB base + low-voltage-activated Ca2+ current.
283///
284/// IT activates at subthreshold voltages (-65 to -50 mV) and inactivates
285/// slowly. When de-inactivated by hyperpolarisation, IT produces a
286/// low-threshold spike (LTS) — a broad Ca2+ depolarisation that can
287/// trigger a burst of Na+ action potentials riding on top.
288///
289/// Key mechanism for:
290/// - Rebound bursting in thalamocortical relay neurons
291/// - Sleep spindle generation (thalamic reticular nucleus)
292/// - Low-threshold calcium spikes in cortical layer V pyramidal cells
293/// - Rhythmic bursting in inferior olive neurons
294///
295/// Huguenard, Annu Rev Physiol 58:329, 1996; Destexhe et al., J Neurophysiol 76:2049, 1996.
296#[derive(Clone, Debug)]
297pub struct TTypeCaNeuron {
298    pub v: f64,
299    pub h: f64, // Na+ inactivation
300    pub n: f64, // Kdr activation
301    pub s: f64, // T-type Ca2+ inactivation (slow)
302    // Conductances (mS/cm²)
303    pub g_na: f64,
304    pub g_k: f64,
305    pub g_t: f64, // T-type Ca2+
306    pub g_l: f64,
307    // Reversal potentials (mV)
308    pub e_na: f64,
309    pub e_k: f64,
310    pub e_ca: f64,
311    pub e_l: f64,
312    pub c_m: f64,
313    pub phi: f64,
314    pub dt: f64,
315    pub v_threshold: f64,
316    pub gain: f64,
317}
318
319impl Default for TTypeCaNeuron {
320    fn default() -> Self {
321        Self::new()
322    }
323}
324
325impl TTypeCaNeuron {
326    pub fn new() -> Self {
327        Self {
328            v: -65.0,
329            h: 0.6,
330            n: 0.32,
331            s: 0.9, // De-inactivated at rest (-65 mV)
332            g_na: 35.0,
333            g_k: 9.0,
334            g_t: 0.1, // Reduced to avoid window current at rest
335            g_l: 0.2,
336            e_na: 55.0,
337            e_k: -90.0,
338            e_ca: 120.0,
339            e_l: -65.0,
340            c_m: 1.0,
341            phi: 5.0,
342            dt: 0.5,
343            v_threshold: -20.0,
344            gain: 1.0,
345        }
346    }
347
348    pub fn step(&mut self, current: f64) -> i32 {
349        let input = self.gain * current;
350        let sub_steps = 50;
351        let sub_dt = self.dt / sub_steps as f64;
352        let mut fired = 0i32;
353
354        for _ in 0..sub_steps {
355            let v = self.v;
356
357            // WB alpha/beta rates
358            let alpha_m = safe_rate(0.1, 35.0, v, 10.0, 1.0);
359            let beta_m = 4.0 * (-(v + 60.0) / 18.0).exp();
360            let m_inf = alpha_m / (alpha_m + beta_m);
361
362            let alpha_h = 0.07 * (-(v + 58.0) / 20.0).exp();
363            let beta_h = 1.0 / (1.0 + (-(v + 28.0) / 10.0).exp());
364
365            let alpha_n = safe_rate(0.01, 34.0, v, 10.0, 0.1);
366            let beta_n = 0.125 * (-(v + 44.0) / 80.0).exp();
367
368            // T-type Ca2+ gating
369            let m_t_inf = 1.0 / (1.0 + (-(v + 52.0) / 5.0).exp());
370            let s_inf = 1.0 / (1.0 + ((v + 81.0) / 4.0).exp());
371            let tau_s = 30.0 + 100.0 / (1.0 + ((v + 75.0) / 10.0).exp());
372
373            // Gate updates
374            self.h += sub_dt * self.phi * (alpha_h * (1.0 - self.h) - beta_h * self.h);
375            self.n += sub_dt * self.phi * (alpha_n * (1.0 - self.n) - beta_n * self.n);
376            self.s += sub_dt * (s_inf - self.s) / tau_s;
377
378            // Currents
379            let i_na = self.g_na * m_inf.powi(3) * self.h * (v - self.e_na);
380            let i_k = self.g_k * self.n.powi(4) * (v - self.e_k);
381            let i_t = self.g_t * m_t_inf.powi(2) * self.s * (v - self.e_ca);
382            let i_l = self.g_l * (v - self.e_l);
383
384            let dv = (-i_na - i_k - i_t - i_l + input) / self.c_m;
385            self.v += sub_dt * dv;
386
387            if self.v >= self.v_threshold {
388                fired = 1;
389                self.v = -65.0;
390                self.s *= 0.3; // Spike inactivates T-type strongly
391            }
392        }
393
394        // Safety bounds
395        self.v = self.v.clamp(-100.0, 60.0);
396        if !self.v.is_finite() {
397            self.v = -65.0;
398            self.h = 0.6;
399            self.n = 0.32;
400        }
401        self.h = self.h.clamp(0.0, 1.0);
402        self.n = self.n.clamp(0.0, 1.0);
403        self.s = self.s.clamp(0.0, 1.0);
404
405        fired
406    }
407
408    pub fn reset(&mut self) {
409        *self = Self::new();
410    }
411}
412
413// ═══════════════════════════════════════════════════════════════════
414// A-type K+ Neuron
415// ═══════════════════════════════════════════════════════════════════
416
417/// A-type K+ (IA) neuron — WB base + transient outward K+ current.
418///
419/// IA activates rapidly at subthreshold voltages and inactivates over
420/// tens of milliseconds. The transient outward current opposes depolarisation,
421/// creating a delay to the first spike and controlling interspike intervals.
422///
423/// Key mechanism for:
424/// - First-spike latency: IA must inactivate before a spike can occur
425/// - Spike frequency control: IA recovery during ISI limits firing rate
426/// - Coincidence detection: neurons with strong IA prefer synchronous input
427/// - Dendritic signal processing (hippocampal CA1 dendrites)
428///
429/// Connor & Stevens, J Physiol 213:31, 1971; Hoffman et al., Nature 387:869, 1997.
430#[derive(Clone, Debug)]
431pub struct ATypeKNeuron {
432    pub v: f64,
433    pub h: f64,
434    pub n: f64,
435    pub a: f64, // IA activation (fast)
436    pub b: f64, // IA inactivation (slow)
437    pub g_na: f64,
438    pub g_k: f64,
439    pub g_a: f64,
440    pub g_l: f64,
441    pub e_na: f64,
442    pub e_k: f64,
443    pub e_l: f64,
444    pub c_m: f64,
445    pub phi: f64,
446    pub dt: f64,
447    pub v_threshold: f64,
448    pub gain: f64,
449}
450
451impl Default for ATypeKNeuron {
452    fn default() -> Self {
453        Self::new()
454    }
455}
456
457impl ATypeKNeuron {
458    pub fn new() -> Self {
459        Self {
460            v: -65.0,
461            h: 0.6,
462            n: 0.32,
463            a: 0.1,
464            b: 0.8,
465            g_na: 35.0,
466            g_k: 9.0,
467            g_a: 8.0,
468            g_l: 0.1,
469            e_na: 55.0,
470            e_k: -90.0,
471            e_l: -65.0,
472            c_m: 1.0,
473            phi: 5.0,
474            dt: 0.5,
475            v_threshold: -20.0,
476            gain: 1.0,
477        }
478    }
479
480    pub fn step(&mut self, current: f64) -> i32 {
481        let input = self.gain * current;
482        let sub_steps = 50;
483        let sub_dt = self.dt / sub_steps as f64;
484        let mut fired = 0i32;
485
486        for _ in 0..sub_steps {
487            let v = self.v;
488
489            let alpha_m = safe_rate(0.1, 35.0, v, 10.0, 1.0);
490            let beta_m = 4.0 * (-(v + 60.0) / 18.0).exp();
491            let m_inf = alpha_m / (alpha_m + beta_m);
492
493            let alpha_h = 0.07 * (-(v + 58.0) / 20.0).exp();
494            let beta_h = 1.0 / (1.0 + (-(v + 28.0) / 10.0).exp());
495
496            let alpha_n = safe_rate(0.01, 34.0, v, 10.0, 0.1);
497            let beta_n = 0.125 * (-(v + 44.0) / 80.0).exp();
498
499            // A-type K+ gating (Connor-Stevens)
500            let a_inf = 1.0 / (1.0 + (-(v + 50.0) / 20.0).exp());
501            let tau_a = 2.0;
502            let b_inf = 1.0 / (1.0 + ((v + 70.0) / 6.0).exp());
503            let tau_b = 50.0;
504
505            self.h += sub_dt * self.phi * (alpha_h * (1.0 - self.h) - beta_h * self.h);
506            self.n += sub_dt * self.phi * (alpha_n * (1.0 - self.n) - beta_n * self.n);
507            self.a += sub_dt * (a_inf - self.a) / tau_a;
508            self.b += sub_dt * (b_inf - self.b) / tau_b;
509
510            let i_na = self.g_na * m_inf.powi(3) * self.h * (v - self.e_na);
511            let i_k = self.g_k * self.n.powi(4) * (v - self.e_k);
512            let i_a = self.g_a * self.a.powi(3) * self.b * (v - self.e_k);
513            let i_l = self.g_l * (v - self.e_l);
514
515            let dv = (-i_na - i_k - i_a - i_l + input) / self.c_m;
516            self.v += sub_dt * dv;
517
518            if self.v >= self.v_threshold {
519                fired = 1;
520                self.v = -65.0;
521            }
522        }
523
524        self.v = self.v.clamp(-100.0, 60.0);
525        if !self.v.is_finite() {
526            self.v = -65.0;
527            self.h = 0.6;
528            self.n = 0.32;
529        }
530        self.h = self.h.clamp(0.0, 1.0);
531        self.n = self.n.clamp(0.0, 1.0);
532        self.a = self.a.clamp(0.0, 1.0);
533        self.b = self.b.clamp(0.0, 1.0);
534
535        fired
536    }
537
538    pub fn reset(&mut self) {
539        *self = Self::new();
540    }
541}
542
543// ═══════════════════════════════════════════════════════════════════
544// BK (Big Conductance Ca2+-Activated K+) Neuron
545// ═══════════════════════════════════════════════════════════════════
546
547/// BK channel neuron — WB base + voltage- and Ca2+-dependent K+ current.
548///
549/// BK (big conductance, MaxiK) channels are activated by both membrane
550/// depolarisation and intracellular Ca2+. They have the largest single-
551/// channel conductance (~250 pS) of any K+ channel. During action
552/// potentials, Ca2+ influx through voltage-gated Ca2+ channels activates
553/// BK, producing fast repolarisation and a prominent fast AHP.
554///
555/// Key mechanism for:
556/// - Fast afterhyperpolarisation (fAHP): rapid spike repolarisation
557/// - Action potential narrowing: BK shortens AP duration
558/// - Burst termination: accumulated Ca2+ activates BK, ending burst
559/// - High-frequency firing: fast repolarisation enables rapid recovery
560///
561/// Bhatt & Storm, J Physiol 557:329, 2003; Faber & Bhatt, PNAS 100:2813, 2003.
562#[derive(Clone, Debug)]
563pub struct BKNeuron {
564    pub v: f64,
565    pub h: f64,  // Na+ inactivation
566    pub n: f64,  // Kdr activation
567    pub ca: f64, // Intracellular Ca2+ concentration
568    // Conductances (mS/cm²)
569    pub g_na: f64,
570    pub g_k: f64,
571    pub g_bk: f64, // BK conductance
572    pub g_l: f64,
573    // Reversal potentials (mV)
574    pub e_na: f64,
575    pub e_k: f64,
576    pub e_l: f64,
577    pub c_m: f64,
578    pub phi: f64,
579    pub tau_ca: f64, // Ca2+ decay time constant (ms)
580    pub dt: f64,
581    pub v_threshold: f64,
582    pub gain: f64,
583}
584
585impl Default for BKNeuron {
586    fn default() -> Self {
587        Self::new()
588    }
589}
590
591impl BKNeuron {
592    pub fn new() -> Self {
593        Self {
594            v: -65.0,
595            h: 0.6,
596            n: 0.32,
597            ca: 0.0,
598            g_na: 35.0,
599            g_k: 9.0,
600            g_bk: 3.0,
601            g_l: 0.1,
602            e_na: 55.0,
603            e_k: -90.0,
604            e_l: -65.0,
605            c_m: 1.0,
606            phi: 5.0,
607            tau_ca: 50.0,
608            dt: 0.5,
609            v_threshold: -20.0,
610            gain: 1.0,
611        }
612    }
613
614    pub fn step(&mut self, current: f64) -> i32 {
615        let input = self.gain * current;
616        let sub_steps = 50;
617        let sub_dt = self.dt / sub_steps as f64;
618        let mut fired = 0i32;
619
620        for _ in 0..sub_steps {
621            let v = self.v;
622
623            let alpha_m = safe_rate(0.1, 35.0, v, 10.0, 1.0);
624            let beta_m = 4.0 * (-(v + 60.0) / 18.0).exp();
625            let m_inf = alpha_m / (alpha_m + beta_m);
626
627            let alpha_h = 0.07 * (-(v + 58.0) / 20.0).exp();
628            let beta_h = 1.0 / (1.0 + (-(v + 28.0) / 10.0).exp());
629
630            let alpha_n = safe_rate(0.01, 34.0, v, 10.0, 0.1);
631            let beta_n = 0.125 * (-(v + 44.0) / 80.0).exp();
632
633            // BK activation: joint voltage and Ca2+ dependence
634            // Half-activation shifts left (easier) with higher Ca2+
635            // BK half-activation: ~+10 mV without Ca2+, shifts to -20 mV with high Ca2+
636            let v_half_bk = 10.0 - 30.0 * (self.ca / (self.ca + 0.5));
637            let bk_inf = 1.0 / (1.0 + (-(v - v_half_bk) / 15.0).exp());
638
639            // Ca2+ dynamics: decay + spike-triggered influx
640            self.ca += sub_dt * (-self.ca / self.tau_ca);
641
642            self.h += sub_dt * self.phi * (alpha_h * (1.0 - self.h) - beta_h * self.h);
643            self.n += sub_dt * self.phi * (alpha_n * (1.0 - self.n) - beta_n * self.n);
644
645            let i_na = self.g_na * m_inf.powi(3) * self.h * (v - self.e_na);
646            let i_k = self.g_k * self.n.powi(4) * (v - self.e_k);
647            let i_bk = self.g_bk * bk_inf * (v - self.e_k);
648            let i_l = self.g_l * (v - self.e_l);
649
650            let dv = (-i_na - i_k - i_bk - i_l + input) / self.c_m;
651            self.v += sub_dt * dv;
652
653            if self.v >= self.v_threshold {
654                fired = 1;
655                self.v = -65.0;
656                self.ca += 0.3; // Ca2+ influx on spike
657            }
658        }
659
660        self.v = self.v.clamp(-100.0, 60.0);
661        if !self.v.is_finite() {
662            self.v = -65.0;
663            self.h = 0.6;
664            self.n = 0.32;
665        }
666        if !self.ca.is_finite() {
667            self.ca = 0.0;
668        }
669        self.h = self.h.clamp(0.0, 1.0);
670        self.n = self.n.clamp(0.0, 1.0);
671        self.ca = self.ca.max(0.0);
672
673        fired
674    }
675
676    pub fn reset(&mut self) {
677        *self = Self::new();
678    }
679}
680
681// ═══════════════════════════════════════════════════════════════════
682// SK (Small Conductance Ca2+-Activated K+) Neuron
683// ═══════════════════════════════════════════════════════════════════
684
685/// SK channel neuron — WB base + Ca2+-only-dependent K+ current.
686///
687/// SK (KCa2.x) channels are activated solely by intracellular Ca2+
688/// (no voltage dependence). They have slower kinetics than BK and produce
689/// the medium afterhyperpolarisation (mAHP) lasting 50-200 ms after spikes.
690///
691/// Key mechanism for:
692/// - Medium AHP (mAHP): limits sustained firing rate
693/// - Spike frequency adaptation: Ca2+ builds → SK activates → firing slows
694/// - Rhythmic firing patterns: SK-mediated pauses create regular ISIs
695/// - Synaptic plasticity gating: SK in dendritic spines regulates NMDA currents
696///
697/// Bhatt & Storm, J Physiol 557:329, 2003; Stocker, Nat Rev Neurosci 5:758, 2004.
698#[derive(Clone, Debug)]
699pub struct SKNeuron {
700    pub v: f64,
701    pub h: f64,
702    pub n: f64,
703    pub ca: f64,
704    pub g_na: f64,
705    pub g_k: f64,
706    pub g_sk: f64,
707    pub g_l: f64,
708    pub e_na: f64,
709    pub e_k: f64,
710    pub e_l: f64,
711    pub c_m: f64,
712    pub phi: f64,
713    pub tau_ca: f64,
714    pub dt: f64,
715    pub v_threshold: f64,
716    pub gain: f64,
717}
718
719impl Default for SKNeuron {
720    fn default() -> Self {
721        Self::new()
722    }
723}
724
725impl SKNeuron {
726    pub fn new() -> Self {
727        Self {
728            v: -65.0,
729            h: 0.6,
730            n: 0.32,
731            ca: 0.0,
732            g_na: 35.0,
733            g_k: 9.0,
734            g_sk: 2.0,
735            g_l: 0.1,
736            e_na: 55.0,
737            e_k: -90.0,
738            e_l: -65.0,
739            c_m: 1.0,
740            phi: 5.0,
741            tau_ca: 150.0, // Slower Ca2+ decay than BK → longer mAHP
742            dt: 0.5,
743            v_threshold: -20.0,
744            gain: 1.0,
745        }
746    }
747
748    pub fn step(&mut self, current: f64) -> i32 {
749        let input = self.gain * current;
750        let sub_steps = 50;
751        let sub_dt = self.dt / sub_steps as f64;
752        let mut fired = 0i32;
753
754        for _ in 0..sub_steps {
755            let v = self.v;
756
757            let alpha_m = safe_rate(0.1, 35.0, v, 10.0, 1.0);
758            let beta_m = 4.0 * (-(v + 60.0) / 18.0).exp();
759            let m_inf = alpha_m / (alpha_m + beta_m);
760
761            let alpha_h = 0.07 * (-(v + 58.0) / 20.0).exp();
762            let beta_h = 1.0 / (1.0 + (-(v + 28.0) / 10.0).exp());
763
764            let alpha_n = safe_rate(0.01, 34.0, v, 10.0, 0.1);
765            let beta_n = 0.125 * (-(v + 44.0) / 80.0).exp();
766
767            // SK activation: purely Ca2+-dependent (Hill function, n=2)
768            let ca2 = self.ca * self.ca;
769            let sk_inf = ca2 / (ca2 + 0.25); // Half-activation at [Ca2+]=0.5
770
771            // Ca2+ decay
772            self.ca += sub_dt * (-self.ca / self.tau_ca);
773
774            self.h += sub_dt * self.phi * (alpha_h * (1.0 - self.h) - beta_h * self.h);
775            self.n += sub_dt * self.phi * (alpha_n * (1.0 - self.n) - beta_n * self.n);
776
777            let i_na = self.g_na * m_inf.powi(3) * self.h * (v - self.e_na);
778            let i_k = self.g_k * self.n.powi(4) * (v - self.e_k);
779            let i_sk = self.g_sk * sk_inf * (v - self.e_k);
780            let i_l = self.g_l * (v - self.e_l);
781
782            let dv = (-i_na - i_k - i_sk - i_l + input) / self.c_m;
783            self.v += sub_dt * dv;
784
785            if self.v >= self.v_threshold {
786                fired = 1;
787                self.v = -65.0;
788                self.ca += 0.2;
789            }
790        }
791
792        self.v = self.v.clamp(-100.0, 60.0);
793        if !self.v.is_finite() {
794            self.v = -65.0;
795            self.h = 0.6;
796            self.n = 0.32;
797        }
798        if !self.ca.is_finite() {
799            self.ca = 0.0;
800        }
801        self.h = self.h.clamp(0.0, 1.0);
802        self.n = self.n.clamp(0.0, 1.0);
803        self.ca = self.ca.max(0.0);
804
805        fired
806    }
807
808    pub fn reset(&mut self) {
809        *self = Self::new();
810    }
811}
812
813// ═══════════════════════════════════════════════════════════════════
814// NMDA Receptor-Gated Channel Neuron
815// ═══════════════════════════════════════════════════════════════════
816
817/// NMDA receptor neuron — WB base + NMDA-type glutamate receptor current.
818///
819/// NMDA receptors require both glutamate binding (modelled as input current)
820/// AND membrane depolarisation (Mg2+ block removal) for activation. The
821/// Mg2+ block is voltage-dependent: at rest (-65 mV) channels are blocked,
822/// but depolarisation to -40 mV relieves ~80% of the block.
823///
824/// Key mechanism for:
825/// - Coincidence detection: requires presynaptic (glutamate) + postsynaptic
826///   (depolarisation) signals simultaneously
827/// - Synaptic plasticity: Ca2+ influx through NMDA triggers LTP/LTD
828/// - Working memory: NMDA-mediated recurrent excitation sustains persistent
829///   activity in prefrontal cortex
830/// - Slow synaptic integration: rise ~10 ms, decay ~100 ms
831///
832/// Jahr & Stevens, J Neurosci 10:1830, 1990; Wang, Neuron 22:409, 1999.
833#[derive(Clone, Debug)]
834pub struct NMDANeuron {
835    pub v: f64,
836    pub h: f64,
837    pub n: f64,
838    pub s_nmda: f64, // NMDA synaptic variable (slow rise/decay)
839    pub g_na: f64,
840    pub g_k: f64,
841    pub g_nmda: f64, // NMDA conductance
842    pub g_l: f64,
843    pub e_na: f64,
844    pub e_k: f64,
845    pub e_nmda: f64, // NMDA reversal (0 mV, mixed cation)
846    pub e_l: f64,
847    pub c_m: f64,
848    pub phi: f64,
849    pub mg_conc: f64,   // Extracellular Mg2+ (mM), typically 1.0
850    pub tau_rise: f64,  // NMDA rise time (ms)
851    pub tau_decay: f64, // NMDA decay time (ms)
852    pub dt: f64,
853    pub v_threshold: f64,
854    pub gain: f64,
855}
856
857impl Default for NMDANeuron {
858    fn default() -> Self {
859        Self::new()
860    }
861}
862
863impl NMDANeuron {
864    pub fn new() -> Self {
865        Self {
866            v: -65.0,
867            h: 0.6,
868            n: 0.32,
869            s_nmda: 0.0,
870            g_na: 35.0,
871            g_k: 9.0,
872            g_nmda: 0.5,
873            g_l: 0.1,
874            e_na: 55.0,
875            e_k: -90.0,
876            e_nmda: 0.0, // Mixed cation reversal
877            e_l: -65.0,
878            c_m: 1.0,
879            phi: 5.0,
880            mg_conc: 1.0,
881            tau_rise: 10.0,
882            tau_decay: 100.0,
883            dt: 0.5,
884            v_threshold: -20.0,
885            gain: 1.0,
886        }
887    }
888
889    pub fn step(&mut self, current: f64) -> i32 {
890        let input = self.gain * current;
891        let sub_steps = 50;
892        let sub_dt = self.dt / sub_steps as f64;
893        let mut fired = 0i32;
894
895        // NMDA synaptic variable: driven by input (as proxy for glutamate)
896        let drive = if input > 0.0 {
897            input / (input + 5.0)
898        } else {
899            0.0
900        };
901        let ds = (drive - self.s_nmda)
902            / if drive > self.s_nmda {
903                self.tau_rise
904            } else {
905                self.tau_decay
906            };
907        self.s_nmda += self.dt * ds;
908        self.s_nmda = self.s_nmda.clamp(0.0, 1.0);
909
910        for _ in 0..sub_steps {
911            let v = self.v;
912
913            let alpha_m = safe_rate(0.1, 35.0, v, 10.0, 1.0);
914            let beta_m = 4.0 * (-(v + 60.0) / 18.0).exp();
915            let m_inf = alpha_m / (alpha_m + beta_m);
916
917            let alpha_h = 0.07 * (-(v + 58.0) / 20.0).exp();
918            let beta_h = 1.0 / (1.0 + (-(v + 28.0) / 10.0).exp());
919
920            let alpha_n = safe_rate(0.01, 34.0, v, 10.0, 0.1);
921            let beta_n = 0.125 * (-(v + 44.0) / 80.0).exp();
922
923            // Mg2+ block: B(V) = 1 / (1 + [Mg2+]/3.57 * exp(-0.062 * V))
924            // Jahr & Stevens 1990
925            let mg_block = 1.0 / (1.0 + (self.mg_conc / 3.57) * (-0.062 * v).exp());
926
927            self.h += sub_dt * self.phi * (alpha_h * (1.0 - self.h) - beta_h * self.h);
928            self.n += sub_dt * self.phi * (alpha_n * (1.0 - self.n) - beta_n * self.n);
929
930            let i_na = self.g_na * m_inf.powi(3) * self.h * (v - self.e_na);
931            let i_k = self.g_k * self.n.powi(4) * (v - self.e_k);
932            let i_nmda = self.g_nmda * self.s_nmda * mg_block * (v - self.e_nmda);
933            let i_l = self.g_l * (v - self.e_l);
934
935            let dv = (-i_na - i_k - i_nmda - i_l + input) / self.c_m;
936            self.v += sub_dt * dv;
937
938            if self.v >= self.v_threshold {
939                fired = 1;
940                self.v = -65.0;
941            }
942        }
943
944        self.v = self.v.clamp(-100.0, 60.0);
945        if !self.v.is_finite() {
946            self.v = -65.0;
947            self.h = 0.6;
948            self.n = 0.32;
949        }
950        if !self.s_nmda.is_finite() {
951            self.s_nmda = 0.0;
952        }
953        self.h = self.h.clamp(0.0, 1.0);
954        self.n = self.n.clamp(0.0, 1.0);
955
956        fired
957    }
958
959    pub fn reset(&mut self) {
960        *self = Self::new();
961    }
962}
963
964// ═══════════════════════════════════════════════════════════════════
965// Tests
966// ═══════════════════════════════════════════════════════════════════
967
968#[cfg(test)]
969mod tests {
970    use super::*;
971
972    // -- Persistent Na+ Neuron tests --
973
974    #[test]
975    fn nap_fires_with_input() {
976        let mut n = PersistentNaNeuron::new();
977        let mut spikes = 0;
978        for _ in 0..2_000 {
979            spikes += n.step(2.0);
980        }
981        assert!(spikes > 5, "NaP neuron must fire with input, got {spikes}");
982    }
983
984    #[test]
985    fn nap_subthreshold_oscillations() {
986        // INaP neurons often show subthreshold oscillations or low-rate
987        // spontaneous firing — this is a biological feature, not a bug.
988        // With negative input, INaP should be suppressed.
989        let mut n = PersistentNaNeuron::new();
990        let mut spikes_inhibited = 0;
991        for _ in 0..10_000 {
992            spikes_inhibited += n.step(-2.0);
993        }
994        assert_eq!(
995            spikes_inhibited, 0,
996            "INaP neuron must be silent with inhibitory input, got {spikes_inhibited}"
997        );
998    }
999
1000    #[test]
1001    fn nap_lowers_threshold() {
1002        // INaP provides subthreshold depolarisation → lower effective threshold
1003        let mut with_nap = PersistentNaNeuron::new();
1004        let mut no_nap = PersistentNaNeuron::new();
1005        no_nap.g_nap = 0.0;
1006
1007        // Use near-threshold input
1008        let input = 1.0;
1009        let mut spikes_nap = 0;
1010        let mut spikes_no = 0;
1011        for _ in 0..10_000 {
1012            spikes_nap += with_nap.step(input);
1013            spikes_no += no_nap.step(input);
1014        }
1015        assert!(
1016            spikes_nap >= spikes_no,
1017            "INaP must lower effective threshold: NaP={spikes_nap} vs none={spikes_no}"
1018        );
1019    }
1020
1021    #[test]
1022    fn nap_p_gate_activates_at_subthreshold() {
1023        // At -50 mV (subthreshold), p_inf should be significant
1024        let mut n = PersistentNaNeuron::new();
1025        n.v = -50.0;
1026        // Step a few times for p to converge
1027        for _ in 0..1000 {
1028            // Hold at -50 mV artificially by resetting v each step
1029            let _ = n.step(0.0);
1030        }
1031        // p_inf at -50 mV = 1/(1+exp(2/5)) = 1/(1+1.49) = 0.40
1032        // After many steps p should approach p_inf
1033        assert!(
1034            n.p > 0.01,
1035            "p gate must activate at subthreshold voltages, p={}",
1036            n.p
1037        );
1038    }
1039
1040    #[test]
1041    fn nap_increases_firing_rate() {
1042        // Same input, higher g_nap → more spikes
1043        let mut low = PersistentNaNeuron::new();
1044        low.g_nap = 0.2;
1045        let mut high = PersistentNaNeuron::new();
1046        high.g_nap = 1.5;
1047
1048        let input = 1.5;
1049        let mut spikes_low = 0;
1050        let mut spikes_high = 0;
1051        for _ in 0..10_000 {
1052            spikes_low += low.step(input);
1053            spikes_high += high.step(input);
1054        }
1055        assert!(
1056            spikes_high >= spikes_low,
1057            "Higher g_nap must increase firing: high={spikes_high} vs low={spikes_low}"
1058        );
1059    }
1060
1061    #[test]
1062    fn nap_negative_input_no_crash() {
1063        let mut n = PersistentNaNeuron::new();
1064        for _ in 0..10_000 {
1065            n.step(-100.0);
1066        }
1067        assert!(n.v.is_finite());
1068        assert!(n.v >= -100.0);
1069    }
1070
1071    #[test]
1072    fn nap_nan_input_stays_finite() {
1073        let mut n = PersistentNaNeuron::new();
1074        n.step(f64::NAN);
1075        assert!(n.v.is_finite());
1076    }
1077
1078    #[test]
1079    fn nap_extreme_input_bounded() {
1080        let mut n = PersistentNaNeuron::new();
1081        for _ in 0..1000 {
1082            n.step(1e6);
1083        }
1084        assert!(n.v.is_finite() && n.v <= 60.0);
1085    }
1086
1087    #[test]
1088    fn nap_reset_clears_state() {
1089        let mut n = PersistentNaNeuron::new();
1090        for _ in 0..1000 {
1091            n.step(10.0);
1092        }
1093        n.reset();
1094        assert_eq!(n.v, -65.0);
1095        assert_eq!(n.p, 0.0);
1096        assert_eq!(n.h, 0.6);
1097    }
1098
1099    #[test]
1100    fn nap_gates_bounded() {
1101        let mut n = PersistentNaNeuron::new();
1102        for _ in 0..10_000 {
1103            n.step(10.0);
1104        }
1105        assert!(n.h >= 0.0 && n.h <= 1.0);
1106        assert!(n.n >= 0.0 && n.n <= 1.0);
1107        assert!(n.p >= 0.0 && n.p <= 1.0);
1108    }
1109
1110    #[test]
1111    fn nap_performance_1k_steps() {
1112        let start = std::time::Instant::now();
1113        let mut n = PersistentNaNeuron::new();
1114        for _ in 0..1_000 {
1115            std::hint::black_box(n.step(5.0));
1116        }
1117        let elapsed = start.elapsed();
1118        assert!(
1119            elapsed.as_millis() < 200,
1120            "1k steps must complete in <200ms"
1121        );
1122    }
1123
1124    // -- Ih Neuron tests --
1125
1126    #[test]
1127    fn ih_fires_with_input() {
1128        let mut n = IhNeuron::new();
1129        let mut spikes = 0;
1130        for _ in 0..2_000 {
1131            spikes += n.step(2.0);
1132        }
1133        assert!(spikes > 5, "Ih neuron must fire with input, got {spikes}");
1134    }
1135
1136    #[test]
1137    fn ih_silent_without_input() {
1138        let mut n = IhNeuron::new();
1139        let mut spikes = 0;
1140        for _ in 0..10_000 {
1141            spikes += n.step(0.0);
1142        }
1143        assert_eq!(
1144            spikes, 0,
1145            "Ih neuron must be silent without input, got {spikes}"
1146        );
1147    }
1148
1149    #[test]
1150    fn ih_sag_potential() {
1151        // Hyperpolarising input should produce sag (voltage rebounds towards rest)
1152        let mut with_ih = IhNeuron::new();
1153        let mut no_ih = IhNeuron::new();
1154        no_ih.g_h = 0.0;
1155
1156        // Apply hyperpolarising step
1157        for _ in 0..4000 {
1158            with_ih.step(-3.0);
1159            no_ih.step(-3.0);
1160        }
1161        // With Ih, voltage should be less hyperpolarised (sag back)
1162        assert!(
1163            with_ih.v > no_ih.v,
1164            "Ih sag must depolarise from hyperpolarisation: Ih={:.1} vs no_Ih={:.1}",
1165            with_ih.v,
1166            no_ih.v
1167        );
1168    }
1169
1170    #[test]
1171    fn ih_r_gate_activates_on_hyperpolarisation() {
1172        let mut n = IhNeuron::new();
1173        let r_before = n.r;
1174        // Hyperpolarise
1175        for _ in 0..4000 {
1176            n.step(-5.0);
1177        }
1178        assert!(
1179            n.r > r_before,
1180            "r gate must increase during hyperpolarisation, r={}",
1181            n.r
1182        );
1183    }
1184
1185    #[test]
1186    fn ih_rebound_excitation() {
1187        // After hyperpolarisation, Ih should help reach threshold
1188        let mut n = IhNeuron::new();
1189        // Hyperpolarise to build up Ih
1190        for _ in 0..4000 {
1191            n.step(-3.0);
1192        }
1193        let r_after_hyp = n.r;
1194        assert!(
1195            r_after_hyp > 0.2,
1196            "r must build up during hyperpolarisation, r={r_after_hyp}"
1197        );
1198
1199        // Release — count spikes during rebound period
1200        let mut rebound_spikes = 0;
1201        for _ in 0..500 {
1202            rebound_spikes += n.step(1.5);
1203        }
1204
1205        // Compare with neuron that was not hyperpolarised
1206        let mut n2 = IhNeuron::new();
1207        let mut direct_spikes = 0;
1208        for _ in 0..500 {
1209            direct_spikes += n2.step(1.5);
1210        }
1211
1212        assert!(
1213            rebound_spikes >= direct_spikes,
1214            "Rebound should facilitate firing: rebound={rebound_spikes} vs direct={direct_spikes}"
1215        );
1216    }
1217
1218    #[test]
1219    fn ih_negative_input_no_crash() {
1220        let mut n = IhNeuron::new();
1221        for _ in 0..10_000 {
1222            n.step(-100.0);
1223        }
1224        assert!(n.v.is_finite());
1225        assert!(n.v >= -100.0);
1226    }
1227
1228    #[test]
1229    fn ih_nan_input_stays_finite() {
1230        let mut n = IhNeuron::new();
1231        n.step(f64::NAN);
1232        assert!(n.v.is_finite());
1233    }
1234
1235    #[test]
1236    fn ih_extreme_input_bounded() {
1237        let mut n = IhNeuron::new();
1238        for _ in 0..1000 {
1239            n.step(1e6);
1240        }
1241        assert!(n.v.is_finite() && n.v <= 60.0);
1242    }
1243
1244    #[test]
1245    fn ih_reset_clears_state() {
1246        let mut n = IhNeuron::new();
1247        for _ in 0..1000 {
1248            n.step(10.0);
1249        }
1250        n.reset();
1251        assert_eq!(n.v, -65.0);
1252        assert_eq!(n.r, 0.1);
1253    }
1254
1255    #[test]
1256    fn ih_gates_bounded() {
1257        let mut n = IhNeuron::new();
1258        for _ in 0..10_000 {
1259            n.step(10.0);
1260        }
1261        assert!(n.h >= 0.0 && n.h <= 1.0);
1262        assert!(n.n >= 0.0 && n.n <= 1.0);
1263        assert!(n.r >= 0.0 && n.r <= 1.0);
1264    }
1265
1266    #[test]
1267    fn ih_performance_1k_steps() {
1268        let start = std::time::Instant::now();
1269        let mut n = IhNeuron::new();
1270        for _ in 0..1_000 {
1271            std::hint::black_box(n.step(2.0));
1272        }
1273        let elapsed = start.elapsed();
1274        assert!(
1275            elapsed.as_millis() < 200,
1276            "1k steps must complete in <200ms"
1277        );
1278    }
1279
1280    // -- T-type Ca2+ Neuron tests --
1281
1282    #[test]
1283    fn ttype_fires_with_input() {
1284        let mut n = TTypeCaNeuron::new();
1285        let mut spikes = 0;
1286        for _ in 0..2_000 {
1287            spikes += n.step(2.0);
1288        }
1289        assert!(
1290            spikes > 5,
1291            "T-type neuron must fire with input, got {spikes}"
1292        );
1293    }
1294
1295    #[test]
1296    fn ttype_silent_without_input() {
1297        let mut n = TTypeCaNeuron::new();
1298        let mut spikes = 0;
1299        for _ in 0..10_000 {
1300            spikes += n.step(0.0);
1301        }
1302        assert_eq!(
1303            spikes, 0,
1304            "T-type neuron must be silent without input, got {spikes}"
1305        );
1306    }
1307
1308    #[test]
1309    fn ttype_rebound_burst() {
1310        // Hyperpolarise → de-inactivate T-type → rebound burst on release
1311        let mut n = TTypeCaNeuron::new();
1312        // Hyperpolarise
1313        for _ in 0..4000 {
1314            n.step(-3.0);
1315        }
1316        assert!(
1317            n.s > 0.3,
1318            "T-type must de-inactivate during hyperpolarisation, s={}",
1319            n.s
1320        );
1321
1322        // Release with mild input
1323        let mut rebound_spikes = 0;
1324        for _ in 0..500 {
1325            rebound_spikes += n.step(1.5);
1326        }
1327
1328        // Compare with pre-inactivated neuron
1329        let mut n2 = TTypeCaNeuron::new();
1330        n2.s = 0.05;
1331        let mut direct_spikes = 0;
1332        for _ in 0..500 {
1333            direct_spikes += n2.step(1.5);
1334        }
1335        assert!(
1336            rebound_spikes >= direct_spikes,
1337            "Rebound should facilitate firing: rebound={rebound_spikes} vs inact={direct_spikes}"
1338        );
1339    }
1340
1341    #[test]
1342    fn ttype_s_gate_de_inactivates_at_hyperpolarised() {
1343        let mut n = TTypeCaNeuron::new();
1344        n.v = -85.0;
1345        n.s = 0.1; // Start inactivated
1346                   // s_inf at -85 = 1/(1+exp((-85+81)/4)) = 1/(1+exp(-1)) = 1/1.37 = 0.73
1347        for _ in 0..5000 {
1348            n.step(-5.0);
1349        }
1350        assert!(
1351            n.s > 0.5,
1352            "s must de-inactivate at hyperpolarised potentials, s={}",
1353            n.s
1354        );
1355    }
1356
1357    #[test]
1358    fn ttype_spike_inactivates_t_channel() {
1359        let mut n = TTypeCaNeuron::new();
1360        let s_before_spiking = n.s;
1361        // Drive until spike
1362        let mut spiked = false;
1363        for _ in 0..2000 {
1364            if n.step(3.0) > 0 {
1365                spiked = true;
1366                break;
1367            }
1368        }
1369        if spiked {
1370            assert!(
1371                n.s < s_before_spiking,
1372                "Spike must inactivate T-type: before={s_before_spiking}, after={}",
1373                n.s
1374            );
1375        }
1376    }
1377
1378    #[test]
1379    fn ttype_negative_input_no_crash() {
1380        let mut n = TTypeCaNeuron::new();
1381        for _ in 0..10_000 {
1382            n.step(-100.0);
1383        }
1384        assert!(n.v.is_finite());
1385        assert!(n.v >= -100.0);
1386    }
1387
1388    #[test]
1389    fn ttype_nan_input_stays_finite() {
1390        let mut n = TTypeCaNeuron::new();
1391        n.step(f64::NAN);
1392        assert!(n.v.is_finite());
1393    }
1394
1395    #[test]
1396    fn ttype_extreme_input_bounded() {
1397        let mut n = TTypeCaNeuron::new();
1398        for _ in 0..1000 {
1399            n.step(1e6);
1400        }
1401        assert!(n.v.is_finite() && n.v <= 60.0);
1402    }
1403
1404    #[test]
1405    fn ttype_reset_clears_state() {
1406        let mut n = TTypeCaNeuron::new();
1407        for _ in 0..1000 {
1408            n.step(10.0);
1409        }
1410        n.reset();
1411        assert_eq!(n.v, -65.0);
1412        assert_eq!(n.s, 0.9);
1413    }
1414
1415    #[test]
1416    fn ttype_gates_bounded() {
1417        let mut n = TTypeCaNeuron::new();
1418        for _ in 0..10_000 {
1419            n.step(10.0);
1420        }
1421        assert!(n.h >= 0.0 && n.h <= 1.0);
1422        assert!(n.n >= 0.0 && n.n <= 1.0);
1423        assert!(n.s >= 0.0 && n.s <= 1.0);
1424    }
1425
1426    #[test]
1427    fn ttype_performance_1k_steps() {
1428        let start = std::time::Instant::now();
1429        let mut n = TTypeCaNeuron::new();
1430        for _ in 0..1_000 {
1431            std::hint::black_box(n.step(2.0));
1432        }
1433        let elapsed = start.elapsed();
1434        assert!(
1435            elapsed.as_millis() < 200,
1436            "1k steps must complete in <200ms"
1437        );
1438    }
1439
1440    // -- A-type K+ Neuron tests --
1441
1442    #[test]
1443    fn atype_fires_with_input() {
1444        let mut n = ATypeKNeuron::new();
1445        let mut spikes = 0;
1446        for _ in 0..2_000 {
1447            spikes += n.step(3.0);
1448        }
1449        assert!(
1450            spikes > 5,
1451            "A-type neuron must fire with input, got {spikes}"
1452        );
1453    }
1454
1455    #[test]
1456    fn atype_silent_without_input() {
1457        let mut n = ATypeKNeuron::new();
1458        let mut spikes = 0;
1459        for _ in 0..10_000 {
1460            spikes += n.step(0.0);
1461        }
1462        assert_eq!(
1463            spikes, 0,
1464            "A-type neuron must be silent without input, got {spikes}"
1465        );
1466    }
1467
1468    #[test]
1469    fn atype_delays_first_spike() {
1470        // IA creates onset delay — removing IA should shorten latency
1471        let mut with_ia = ATypeKNeuron::new();
1472        let mut no_ia = ATypeKNeuron::new();
1473        no_ia.g_a = 0.0;
1474
1475        let input = 3.0;
1476        let mut time_with = 10_000usize;
1477        for i in 0..10_000 {
1478            if with_ia.step(input) > 0 {
1479                time_with = i;
1480                break;
1481            }
1482        }
1483        let mut time_no = 10_000usize;
1484        for i in 0..10_000 {
1485            if no_ia.step(input) > 0 {
1486                time_no = i;
1487                break;
1488            }
1489        }
1490        assert!(
1491            time_with >= time_no,
1492            "IA must delay first spike: with={time_with} vs without={time_no}"
1493        );
1494    }
1495
1496    #[test]
1497    fn atype_reduces_firing_rate() {
1498        // IA should reduce steady-state firing rate
1499        let mut with_ia = ATypeKNeuron::new();
1500        let mut no_ia = ATypeKNeuron::new();
1501        no_ia.g_a = 0.0;
1502
1503        let input = 3.0;
1504        let mut spikes_ia = 0;
1505        let mut spikes_no = 0;
1506        for _ in 0..10_000 {
1507            spikes_ia += with_ia.step(input);
1508            spikes_no += no_ia.step(input);
1509        }
1510        assert!(
1511            spikes_no >= spikes_ia,
1512            "IA should reduce firing rate: IA={spikes_ia} vs none={spikes_no}"
1513        );
1514    }
1515
1516    #[test]
1517    fn atype_negative_input_no_crash() {
1518        let mut n = ATypeKNeuron::new();
1519        for _ in 0..10_000 {
1520            n.step(-100.0);
1521        }
1522        assert!(n.v.is_finite());
1523        assert!(n.v >= -100.0);
1524    }
1525
1526    #[test]
1527    fn atype_nan_input_stays_finite() {
1528        let mut n = ATypeKNeuron::new();
1529        n.step(f64::NAN);
1530        assert!(n.v.is_finite());
1531    }
1532
1533    #[test]
1534    fn atype_extreme_input_bounded() {
1535        let mut n = ATypeKNeuron::new();
1536        for _ in 0..1000 {
1537            n.step(1e6);
1538        }
1539        assert!(n.v.is_finite() && n.v <= 60.0);
1540    }
1541
1542    #[test]
1543    fn atype_reset_clears_state() {
1544        let mut n = ATypeKNeuron::new();
1545        for _ in 0..1000 {
1546            n.step(10.0);
1547        }
1548        n.reset();
1549        assert_eq!(n.v, -65.0);
1550        assert_eq!(n.a, 0.1);
1551        assert_eq!(n.b, 0.8);
1552    }
1553
1554    #[test]
1555    fn atype_gates_bounded() {
1556        let mut n = ATypeKNeuron::new();
1557        for _ in 0..10_000 {
1558            n.step(10.0);
1559        }
1560        assert!(n.a >= 0.0 && n.a <= 1.0);
1561        assert!(n.b >= 0.0 && n.b <= 1.0);
1562    }
1563
1564    #[test]
1565    fn atype_performance_1k_steps() {
1566        let start = std::time::Instant::now();
1567        let mut n = ATypeKNeuron::new();
1568        for _ in 0..1_000 {
1569            std::hint::black_box(n.step(3.0));
1570        }
1571        let elapsed = start.elapsed();
1572        assert!(
1573            elapsed.as_millis() < 200,
1574            "1k steps must complete in <200ms"
1575        );
1576    }
1577
1578    // -- BK Neuron tests --
1579
1580    #[test]
1581    fn bk_fires_with_input() {
1582        let mut n = BKNeuron::new();
1583        let mut spikes = 0;
1584        for _ in 0..2_000 {
1585            spikes += n.step(3.0);
1586        }
1587        assert!(spikes > 5, "BK neuron must fire with input, got {spikes}");
1588    }
1589
1590    #[test]
1591    fn bk_silent_without_input() {
1592        let mut n = BKNeuron::new();
1593        let mut spikes = 0;
1594        for _ in 0..10_000 {
1595            spikes += n.step(0.0);
1596        }
1597        assert_eq!(
1598            spikes, 0,
1599            "BK neuron must be silent without input, got {spikes}"
1600        );
1601    }
1602
1603    #[test]
1604    fn bk_ca_accumulates_during_spiking() {
1605        let mut n = BKNeuron::new();
1606        assert_eq!(n.ca, 0.0);
1607        for _ in 0..5000 {
1608            n.step(5.0);
1609        }
1610        assert!(
1611            n.ca > 0.0,
1612            "Ca2+ must accumulate during spiking, ca={}",
1613            n.ca
1614        );
1615    }
1616
1617    #[test]
1618    fn bk_deepens_ahp() {
1619        // BK should produce deeper AHP (more negative post-spike voltage)
1620        // Compare with and without BK after a burst
1621        let mut with_bk = BKNeuron::new();
1622        let mut no_bk = BKNeuron::new();
1623        no_bk.g_bk = 0.0;
1624
1625        // Drive both to spike, then check voltage
1626        for _ in 0..2000 {
1627            with_bk.step(5.0);
1628            no_bk.step(5.0);
1629        }
1630        // After sustained spiking, BK with Ca2+ should keep voltage lower
1631        // (stronger K+ current from BK)
1632        // Test that BK neuron has non-zero Ca2+ (proves it's active)
1633        assert!(with_bk.ca > 0.0, "BK neuron must have Ca2+ after spiking");
1634    }
1635
1636    #[test]
1637    fn bk_reduces_firing_rate() {
1638        // BK should reduce firing rate via stronger repolarisation
1639        let mut with_bk = BKNeuron::new();
1640        let mut no_bk = BKNeuron::new();
1641        no_bk.g_bk = 0.0;
1642
1643        let input = 3.0;
1644        let mut spikes_bk = 0;
1645        let mut spikes_no = 0;
1646        for _ in 0..10_000 {
1647            spikes_bk += with_bk.step(input);
1648            spikes_no += no_bk.step(input);
1649        }
1650        // BK adds extra K+ → fewer spikes (or equal if Ca2+ builds slowly)
1651        assert!(
1652            spikes_no >= spikes_bk,
1653            "BK should reduce firing: BK={spikes_bk} vs none={spikes_no}"
1654        );
1655    }
1656
1657    #[test]
1658    fn bk_negative_input_no_crash() {
1659        let mut n = BKNeuron::new();
1660        for _ in 0..10_000 {
1661            n.step(-100.0);
1662        }
1663        assert!(n.v.is_finite());
1664    }
1665
1666    #[test]
1667    fn bk_nan_input_stays_finite() {
1668        let mut n = BKNeuron::new();
1669        n.step(f64::NAN);
1670        assert!(n.v.is_finite());
1671    }
1672
1673    #[test]
1674    fn bk_extreme_input_bounded() {
1675        let mut n = BKNeuron::new();
1676        for _ in 0..1000 {
1677            n.step(1e6);
1678        }
1679        assert!(n.v.is_finite() && n.v <= 60.0);
1680    }
1681
1682    #[test]
1683    fn bk_reset_clears_state() {
1684        let mut n = BKNeuron::new();
1685        for _ in 0..1000 {
1686            n.step(10.0);
1687        }
1688        n.reset();
1689        assert_eq!(n.v, -65.0);
1690        assert_eq!(n.ca, 0.0);
1691    }
1692
1693    #[test]
1694    fn bk_performance_1k_steps() {
1695        let start = std::time::Instant::now();
1696        let mut n = BKNeuron::new();
1697        for _ in 0..1_000 {
1698            std::hint::black_box(n.step(3.0));
1699        }
1700        let elapsed = start.elapsed();
1701        assert!(
1702            elapsed.as_millis() < 200,
1703            "1k steps must complete in <200ms"
1704        );
1705    }
1706
1707    // -- SK Neuron tests --
1708
1709    #[test]
1710    fn sk_fires_with_input() {
1711        let mut n = SKNeuron::new();
1712        let mut spikes = 0;
1713        for _ in 0..2_000 {
1714            spikes += n.step(2.0);
1715        }
1716        assert!(spikes > 5, "SK neuron must fire with input, got {spikes}");
1717    }
1718
1719    #[test]
1720    fn sk_silent_without_input() {
1721        let mut n = SKNeuron::new();
1722        let mut spikes = 0;
1723        for _ in 0..10_000 {
1724            spikes += n.step(0.0);
1725        }
1726        assert_eq!(
1727            spikes, 0,
1728            "SK neuron must be silent without input, got {spikes}"
1729        );
1730    }
1731
1732    #[test]
1733    fn sk_adaptation() {
1734        // SK causes spike frequency adaptation
1735        let mut n = SKNeuron::new();
1736        let input = 5.0;
1737        let mut early = 0;
1738        for _ in 0..2000 {
1739            early += n.step(input);
1740        }
1741        let mut late = 0;
1742        for _ in 0..2000 {
1743            late += n.step(input);
1744        }
1745        assert!(
1746            early >= late,
1747            "SK should cause adaptation: early={early}, late={late}"
1748        );
1749    }
1750
1751    #[test]
1752    fn sk_ca_dependent_only() {
1753        // SK at rest (ca=0) should contribute zero current
1754        let n = SKNeuron::new();
1755        let ca2 = n.ca * n.ca;
1756        let sk_inf = ca2 / (ca2 + 0.25);
1757        assert!(
1758            sk_inf < 0.001,
1759            "SK must be inactive at ca=0, sk_inf={sk_inf}"
1760        );
1761    }
1762
1763    #[test]
1764    fn sk_reduces_firing_rate() {
1765        let mut with_sk = SKNeuron::new();
1766        let mut no_sk = SKNeuron::new();
1767        no_sk.g_sk = 0.0;
1768
1769        let input = 3.0;
1770        let mut spikes_sk = 0;
1771        let mut spikes_no = 0;
1772        for _ in 0..10_000 {
1773            spikes_sk += with_sk.step(input);
1774            spikes_no += no_sk.step(input);
1775        }
1776        assert!(
1777            spikes_no >= spikes_sk,
1778            "SK should reduce firing: SK={spikes_sk} vs none={spikes_no}"
1779        );
1780    }
1781
1782    #[test]
1783    fn sk_negative_input_no_crash() {
1784        let mut n = SKNeuron::new();
1785        for _ in 0..10_000 {
1786            n.step(-100.0);
1787        }
1788        assert!(n.v.is_finite());
1789    }
1790
1791    #[test]
1792    fn sk_nan_input_stays_finite() {
1793        let mut n = SKNeuron::new();
1794        n.step(f64::NAN);
1795        assert!(n.v.is_finite());
1796    }
1797
1798    #[test]
1799    fn sk_extreme_input_bounded() {
1800        let mut n = SKNeuron::new();
1801        for _ in 0..1000 {
1802            n.step(1e6);
1803        }
1804        assert!(n.v.is_finite() && n.v <= 60.0);
1805    }
1806
1807    #[test]
1808    fn sk_reset_clears_state() {
1809        let mut n = SKNeuron::new();
1810        for _ in 0..1000 {
1811            n.step(10.0);
1812        }
1813        n.reset();
1814        assert_eq!(n.v, -65.0);
1815        assert_eq!(n.ca, 0.0);
1816    }
1817
1818    #[test]
1819    fn sk_performance_1k_steps() {
1820        let start = std::time::Instant::now();
1821        let mut n = SKNeuron::new();
1822        for _ in 0..1_000 {
1823            std::hint::black_box(n.step(3.0));
1824        }
1825        let elapsed = start.elapsed();
1826        assert!(
1827            elapsed.as_millis() < 200,
1828            "1k steps must complete in <200ms"
1829        );
1830    }
1831
1832    // -- NMDA Neuron tests --
1833
1834    #[test]
1835    fn nmda_fires_with_input() {
1836        let mut n = NMDANeuron::new();
1837        let mut spikes = 0;
1838        for _ in 0..2_000 {
1839            spikes += n.step(3.0);
1840        }
1841        assert!(spikes > 5, "NMDA neuron must fire with input, got {spikes}");
1842    }
1843
1844    #[test]
1845    fn nmda_silent_without_input() {
1846        let mut n = NMDANeuron::new();
1847        let mut spikes = 0;
1848        for _ in 0..10_000 {
1849            spikes += n.step(0.0);
1850        }
1851        assert_eq!(
1852            spikes, 0,
1853            "NMDA neuron must be silent without input, got {spikes}"
1854        );
1855    }
1856
1857    #[test]
1858    fn nmda_mg_block_at_rest() {
1859        // At -65 mV: B = 1/(1 + 1/3.57 * exp(0.062*65)) = 1/(1 + 0.28 * 56.3) = 1/16.8 = 0.06
1860        let n = NMDANeuron::new();
1861        let mg_block = 1.0 / (1.0 + (n.mg_conc / 3.57) * (-0.062 * n.v).exp());
1862        assert!(
1863            mg_block < 0.1,
1864            "Mg2+ block must be strong at rest, B={mg_block}"
1865        );
1866    }
1867
1868    #[test]
1869    fn nmda_mg_relief_at_depolarised() {
1870        // At -20 mV: B = 1/(1 + 0.28 * exp(0.062*20)) = 1/(1 + 0.28*3.45) = 1/1.97 = 0.51
1871        let mg_block = 1.0 / (1.0 + (1.0 / 3.57) * (-0.062 * (-20.0_f64)).exp());
1872        assert!(
1873            mg_block > 0.4,
1874            "Mg2+ block must be relieved at -20 mV, B={mg_block}"
1875        );
1876    }
1877
1878    #[test]
1879    fn nmda_s_builds_with_input() {
1880        let mut n = NMDANeuron::new();
1881        assert_eq!(n.s_nmda, 0.0);
1882        for _ in 0..2000 {
1883            n.step(5.0);
1884        }
1885        assert!(
1886            n.s_nmda > 0.0,
1887            "s_nmda must build with input, s={}",
1888            n.s_nmda
1889        );
1890    }
1891
1892    #[test]
1893    fn nmda_s_decays_without_input() {
1894        let mut n = NMDANeuron::new();
1895        // Build up s
1896        for _ in 0..2000 {
1897            n.step(5.0);
1898        }
1899        let s_peak = n.s_nmda;
1900        // Remove input
1901        for _ in 0..2000 {
1902            n.step(0.0);
1903        }
1904        assert!(n.s_nmda < s_peak, "s_nmda must decay after input removal");
1905    }
1906
1907    #[test]
1908    fn nmda_zero_mg_increases_current() {
1909        // Without Mg2+ block, NMDA should contribute more
1910        let mut with_mg = NMDANeuron::new();
1911        let mut no_mg = NMDANeuron::new();
1912        no_mg.mg_conc = 0.0;
1913
1914        let input = 2.0;
1915        let mut spikes_mg = 0;
1916        let mut spikes_no = 0;
1917        for _ in 0..10_000 {
1918            spikes_mg += with_mg.step(input);
1919            spikes_no += no_mg.step(input);
1920        }
1921        assert!(
1922            spikes_no >= spikes_mg,
1923            "No Mg2+ should increase NMDA current: no_mg={spikes_no} vs mg={spikes_mg}"
1924        );
1925    }
1926
1927    #[test]
1928    fn nmda_negative_input_no_crash() {
1929        let mut n = NMDANeuron::new();
1930        for _ in 0..10_000 {
1931            n.step(-100.0);
1932        }
1933        assert!(n.v.is_finite());
1934    }
1935
1936    #[test]
1937    fn nmda_nan_input_stays_finite() {
1938        let mut n = NMDANeuron::new();
1939        n.step(f64::NAN);
1940        assert!(n.v.is_finite());
1941    }
1942
1943    #[test]
1944    fn nmda_extreme_input_bounded() {
1945        let mut n = NMDANeuron::new();
1946        for _ in 0..1000 {
1947            n.step(1e6);
1948        }
1949        assert!(n.v.is_finite() && n.v <= 60.0);
1950    }
1951
1952    #[test]
1953    fn nmda_reset_clears_state() {
1954        let mut n = NMDANeuron::new();
1955        for _ in 0..1000 {
1956            n.step(10.0);
1957        }
1958        n.reset();
1959        assert_eq!(n.v, -65.0);
1960        assert_eq!(n.s_nmda, 0.0);
1961    }
1962
1963    #[test]
1964    fn nmda_performance_1k_steps() {
1965        let start = std::time::Instant::now();
1966        let mut n = NMDANeuron::new();
1967        for _ in 0..1_000 {
1968            std::hint::black_box(n.step(3.0));
1969        }
1970        let elapsed = start.elapsed();
1971        assert!(
1972            elapsed.as_millis() < 200,
1973            "1k steps must complete in <200ms"
1974        );
1975    }
1976}