Skip to main content

sc_neurocore_engine/neurons/
misc.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 — Miscellaneous Neuron and Cell Models
8
9//! Miscellaneous neuron and cell models.
10//!
11//! Miscellaneous model group: graded synapse, gap junction, axon, cardiac,
12//! smooth muscle, and endocrine models.
13//! Added one by one with full 7-point checklist verification.
14
15// ═══════════════════════════════════════════════════════════════════
16// Graded Synapse Neuron (non-spiking interneuron)
17// ═══════════════════════════════════════════════════════════════════
18
19/// Non-spiking interneuron with graded synaptic release.
20///
21/// Models interneurons that communicate via graded potential changes
22/// rather than action potentials (e.g., retinal bipolar/amacrine cells,
23/// C. elegans interneurons, crustacean stomatogastric neurons).
24///
25/// The membrane potential follows passive RC dynamics with saturation:
26///
27///   C dV/dt = -g_L(V - E_L) + g_in * I_ext
28///
29/// Transmitter release is a sigmoid function of V:
30///
31///   release = 1 / (1 + exp(-(V - V_half) / k_release))
32///
33/// A "spike" event is emitted when V crosses a threshold from below,
34/// representing a significant release event.
35///
36/// Roberts & Bush, J Comp Physiol A 185:549, 1999.
37#[derive(Clone, Debug)]
38pub struct GradedSynapseNeuron {
39    pub v: f64,           // Membrane potential (mV)
40    pub c_m: f64,         // Membrane capacitance (normalised)
41    pub g_l: f64,         // Leak conductance
42    pub e_l: f64,         // Leak reversal potential (mV)
43    pub g_in: f64,        // Input conductance scaling
44    pub v_half: f64,      // Release sigmoid half-activation (mV)
45    pub k_release: f64,   // Release sigmoid slope
46    pub v_min: f64,       // Saturation floor (mV)
47    pub v_max: f64,       // Saturation ceiling (mV)
48    pub v_threshold: f64, // "Spike" detection threshold (mV)
49    pub dt: f64,
50    pub gain: f64,
51}
52
53impl Default for GradedSynapseNeuron {
54    fn default() -> Self {
55        Self::new()
56    }
57}
58
59impl GradedSynapseNeuron {
60    pub fn new() -> Self {
61        Self {
62            v: -60.0,
63            c_m: 1.0,
64            g_l: 0.05, // Moderate leak
65            e_l: -60.0,
66            g_in: 0.1,      // Input scaling
67            v_half: -40.0,  // Release kicks in at depolarised potential
68            k_release: 5.0, // Sigmoid slope
69            v_min: -80.0,
70            v_max: -10.0,
71            v_threshold: -35.0, // "Spike" threshold for pipeline
72            dt: 0.1,
73            gain: 1.0,
74        }
75    }
76
77    /// Returns the graded transmitter release level [0, 1].
78    pub fn release(&self) -> f64 {
79        1.0 / (1.0 + (-(self.v - self.v_half) / self.k_release).exp())
80    }
81
82    pub fn step(&mut self, current: f64) -> i32 {
83        let input = self.gain * current;
84        let v_prev = self.v;
85
86        let dv = (-self.g_l * (self.v - self.e_l) + self.g_in * input) / self.c_m;
87        self.v += self.dt * dv;
88
89        // Saturation bounds
90        self.v = self.v.clamp(self.v_min, self.v_max);
91        if !self.v.is_finite() {
92            self.v = self.e_l;
93        }
94
95        // Threshold crossing = significant release event
96        if self.v >= self.v_threshold && v_prev < self.v_threshold {
97            1
98        } else {
99            0
100        }
101    }
102
103    pub fn reset(&mut self) {
104        *self = Self::new();
105    }
106}
107
108// ═══════════════════════════════════════════════════════════════════
109// Gap Junction Neuron
110// ═══════════════════════════════════════════════════════════════════
111
112/// Neuron with electrical synapse (gap junction) coupling.
113///
114/// Models neurons coupled via connexin-based gap junctions that allow
115/// direct electrical current flow between cells. Found extensively in:
116/// - Inferior olive neurons (climbing fibre system)
117/// - Retinal ganglion cells (coupled networks)
118/// - Cortical interneuron networks (PV+ basket cell syncytia)
119/// - Thalamic reticular nucleus
120///
121/// Includes voltage-dependent rectification (Cx36 gating):
122///   g_eff = g_gap * g_inf(V_j)
123///   g_inf = g_min + (1 - g_min) / (1 + exp(A * (|V_j| - V_0)))
124///
125/// where V_j = V_neighbor - V is the transjunctional voltage,
126/// g_min is the residual conductance at large V_j, V_0 is the
127/// half-inactivation voltage (~30 mV for Cx36), and A is the
128/// voltage sensitivity (~0.1 mV⁻¹).
129///
130/// At small |V_j| < V_0: near-full conductance (bidirectional).
131/// At large |V_j| > V_0: conductance drops to g_min (rectification).
132///
133/// C dV/dt = -g_L(V - E_L) + g_eff * (V_neighbor - V) + I_tonic
134///
135/// Connors & Long, Annu Rev Neurosci 27:393, 2004.
136/// Vervaeke et al., Neuron 65:801, 2010 (Cx36 voltage gating).
137#[derive(Clone, Debug)]
138pub struct GapJunctionNeuron {
139    pub v: f64,       // Membrane potential (mV)
140    pub c_m: f64,     // Membrane capacitance
141    pub g_l: f64,     // Leak conductance
142    pub e_l: f64,     // Leak reversal (mV)
143    pub g_gap: f64,   // Maximal gap junction conductance
144    pub i_tonic: f64, // Tonic depolarising current
145    pub v_threshold: f64,
146    pub v_reset: f64,
147    pub refractory: f64, // Refractory period (ms)
148    pub refrac_timer: f64,
149    // Voltage-dependent rectification (Cx36)
150    pub rect_v0: f64,   // Half-inactivation voltage (mV), ~30 for Cx36
151    pub rect_a: f64,    // Voltage sensitivity (mV⁻¹), ~0.1 for Cx36
152    pub rect_gmin: f64, // Residual conductance fraction [0,1], ~0.1
153    pub dt: f64,
154    pub gain: f64,
155}
156
157impl Default for GapJunctionNeuron {
158    fn default() -> Self {
159        Self::new()
160    }
161}
162
163impl GapJunctionNeuron {
164    pub fn new() -> Self {
165        Self {
166            v: -65.0,
167            c_m: 1.0,
168            g_l: 0.1,
169            e_l: -65.0,
170            g_gap: 0.15,  // Gap junction coupling (maximal)
171            i_tonic: 0.0, // No tonic drive by default
172            v_threshold: -50.0,
173            v_reset: -65.0,
174            refractory: 2.0, // 2 ms refractory
175            refrac_timer: 0.0,
176            rect_v0: 30.0,  // Cx36: half-inactivation at ~30 mV Vj
177            rect_a: 0.1,    // Cx36: voltage sensitivity
178            rect_gmin: 0.1, // Cx36: ~10% residual conductance
179            dt: 0.1,
180            gain: 1.0,
181        }
182    }
183
184    /// Voltage-dependent gap junction conductance (Cx36 gating).
185    ///
186    /// g_inf = g_min + (1 - g_min) / (1 + exp(A * (|V_j| - V_0)))
187    ///
188    /// Symmetric in |V_j|: rectification acts for both polarities.
189    #[inline]
190    fn rect_conductance(&self, v_j: f64) -> f64 {
191        self.rect_gmin
192            + (1.0 - self.rect_gmin) / (1.0 + (self.rect_a * (v_j.abs() - self.rect_v0)).exp())
193    }
194
195    pub fn step(&mut self, current: f64) -> i32 {
196        // current = mean neighbour voltage or external drive
197        let input = self.gain * current;
198
199        if self.refrac_timer > 0.0 {
200            self.refrac_timer -= self.dt;
201            return 0;
202        }
203
204        // Transjunctional voltage
205        let v_j = input - self.v;
206        // Voltage-dependent effective conductance
207        let g_eff = self.g_gap * self.rect_conductance(v_j);
208        let i_gap = g_eff * v_j;
209        let dv = (-self.g_l * (self.v - self.e_l) + i_gap + self.i_tonic) / self.c_m;
210        self.v += self.dt * dv;
211
212        // Safety
213        self.v = self.v.clamp(-100.0, 40.0);
214        if !self.v.is_finite() {
215            self.v = self.e_l;
216        }
217
218        // Spike
219        if self.v >= self.v_threshold {
220            self.v = self.v_reset;
221            self.refrac_timer = self.refractory;
222            return 1;
223        }
224        0
225    }
226
227    pub fn reset(&mut self) {
228        *self = Self::new();
229    }
230}
231
232// ═══════════════════════════════════════════════════════════════════
233// Frankenhaeuser-Huxley Axon
234// ═══════════════════════════════════════════════════════════════════
235
236/// Frankenhaeuser-Huxley 1964 — myelinated nerve fibre model.
237///
238/// Extension of HH for myelinated axons (Xenopus node of Ranvier).
239/// Uses Goldman-Hodgkin-Katz (GHK) permeability-based current equations
240/// with 4 gating variables: m (Na activation), h (Na inactivation),
241/// n (delayed rectifier K), p (slow non-specific current).
242///
243/// GHK current for monovalent ion:
244///   I = P * F²V/(RT) * (C_i - C_o * exp(-FV/RT)) / (1 - exp(-FV/RT))
245///
246/// Simplified (FH convention, V relative to rest, temperature factor absorbed):
247///   I_Na = P_Na * m² * h * ghk_drive(V, Na_i/Na_o)
248///   I_K  = P_K  * n² * ghk_drive(V, K_i/K_o)
249///   I_p  = P_p  * p² * ghk_drive(V, Na_i/Na_o)  [non-specific, Na-like]
250///   I_L  = g_L * (V - E_L)
251///
252/// C dV/dt = -(I_Na + I_K + I_p + I_L) + I_ext
253///
254/// The GHK driving force is the key distinction from conductance-based
255/// HH — it is nonlinear in V and depends on concentration ratios.
256///
257/// Frankenhaeuser & Huxley, J Physiol 171:302, 1964.
258/// Frankenhaeuser, J Physiol 160:46, 1962 (rate constants).
259#[derive(Clone, Debug)]
260pub struct FrankenhaeUserHuxleyAxon {
261    pub v: f64,    // Membrane potential (mV, relative to rest)
262    pub m: f64,    // Na activation
263    pub h: f64,    // Na inactivation
264    pub n: f64,    // K delayed rectifier
265    pub p: f64,    // Slow non-specific
266    pub c_m: f64,  // µF/cm²
267    pub p_na: f64, // Na permeability (10⁻³ cm/s, FH units)
268    pub p_k: f64,  // K permeability
269    pub p_p: f64,  // Slow current permeability
270    pub g_l: f64,  // Leak conductance (mS/cm²)
271    pub e_l: f64,  // Leak reversal (mV relative to rest)
272    // Concentration ratios (C_i / C_o) for GHK
273    pub na_ratio: f64, // [Na]_i / [Na]_o (~0.1 for frog)
274    pub k_ratio: f64,  // [K]_i / [K]_o (~30 for frog)
275    pub v_t: f64,      // RT/F thermal voltage (mV), ~25.3 at 20°C
276    pub dt: f64,
277    pub sub_steps: usize,
278    pub gain: f64,
279}
280
281impl Default for FrankenhaeUserHuxleyAxon {
282    fn default() -> Self {
283        Self::new()
284    }
285}
286
287impl FrankenhaeUserHuxleyAxon {
288    pub fn new() -> Self {
289        Self {
290            v: 0.0, // Relative to resting potential
291            m: 0.005,
292            h: 0.8,
293            n: 0.01,
294            p: 0.01,
295            c_m: 2.0, // µF/cm² (myelinated node, FH Table 4)
296            // Effective permeabilities: P_raw * F * [C]_o / 1000
297            // FH Table 4: P_Na=8e-3, P_K=1.2e-3, P_p=0.54e-3 cm/s
298            // [Na]_o=114.5, [K]_o=2.5 mM; F=96.485 C/mmol
299            p_na: 88.4,     // 8e-3 * 96.485 * 114.5 / 1000 (mA/cm² per unit gating)
300            p_k: 0.29,      // 1.2e-3 * 96.485 * 2.5 / 1000
301            p_p: 5.96,      // 0.54e-3 * 96.485 * 114.5 / 1000 (Na-like)
302            g_l: 30.3,      // Leak (FH Table 4: 30.3 mS/cm²)
303            e_l: 0.026,     // Leak reversal (FH Table 4: 0.026 mV)
304            na_ratio: 0.12, // [Na]_i/[Na]_o = 13.74/114.5 (FH 1962)
305            k_ratio: 48.0,  // [K]_i/[K]_o = 120/2.5 (FH 1962)
306            v_t: 25.3,      // RT/F at 20°C (293K)
307            dt: 0.5,        // External step (ms)
308            sub_steps: 50,  // dt_sub = 0.01 ms
309            gain: 1.0,
310        }
311    }
312
313    /// GHK current for monovalent ion (FH convention).
314    ///
315    /// Returns current density contribution in mA/cm² when P is in
316    /// FH-scaled units (absorbs F*[C_o]*1e-3 into P).
317    ///
318    /// I = P_eff * gates * V/V_T * (r - exp(-V/V_T)) / (1 - exp(-V/V_T))
319    ///
320    /// where r = [ion]_i / [ion]_o, V_T = RT/F.
321    /// At V→0: uses L'Hôpital limit = P_eff * (r - 1).
322    ///
323    /// The Faraday scaling: P_eff = P_raw * F * [C]_o / 1000
324    /// is absorbed into the permeability constant (FH Table 4 values
325    /// are already in these effective units: mA/cm² at unit gating).
326    #[inline]
327    fn ghk_current(v: f64, c_ratio: f64, v_t: f64) -> f64 {
328        if v.abs() < 0.01 {
329            // L'Hôpital limit
330            c_ratio - 1.0
331        } else {
332            let u = v / v_t;
333            let exp_neg_u = (-u).exp();
334            u * (c_ratio - exp_neg_u) / (1.0 - exp_neg_u)
335        }
336    }
337
338    pub fn step(&mut self, current: f64) -> i32 {
339        let input = self.gain * current;
340        let dt_sub = self.dt / self.sub_steps as f64;
341        let v_prev = self.v;
342
343        for _ in 0..self.sub_steps {
344            let v = self.v;
345
346            // FH alpha/beta rate functions (Frankenhaeuser 1962, Table 1)
347            // All rates in ms⁻¹, V in mV relative to rest.
348
349            // m gate (Na activation)
350            let am = if (v - 22.0).abs() < 0.1 {
351                1.87
352            } else {
353                0.36 * (v - 22.0) / (1.0 - (-(v - 22.0) / 3.0).exp())
354            };
355            let bm = if (v - 13.0).abs() < 0.1 {
356                1.87
357            } else {
358                0.4 * (13.0 - v) / (1.0 - ((v - 13.0) / 20.0).exp())
359            };
360
361            // h gate (Na inactivation)
362            let ah = if (v + 10.0).abs() < 0.1 {
363                0.08
364            } else {
365                0.1 * (-10.0 - v) / (1.0 - ((v + 10.0) / 6.0).exp())
366            };
367            let bh = 4.5 / (1.0 + ((45.0 - v) / 10.0).exp());
368
369            // n gate (K delayed rectifier)
370            let an = if (v - 13.0).abs() < 0.1 {
371                0.1
372            } else {
373                0.02 * (v - 13.0) / (1.0 - (-(v - 13.0) / 10.0).exp())
374            };
375            let bn = if (v - 23.0).abs() < 0.1 {
376                0.05
377            } else {
378                0.05 * (23.0 - v) / (1.0 - ((v - 23.0) / 10.0).exp())
379            };
380
381            // p gate (slow non-specific)
382            let ap = if (v - 21.0).abs() < 0.1 {
383                0.04
384            } else {
385                0.006 * (v - 21.0) / (1.0 - (-(v - 21.0) / 2.0).exp())
386            };
387            let bp = if (v + 4.0).abs() < 0.1 {
388                0.04
389            } else {
390                0.09 * (-4.0 - v) / (1.0 - ((v + 4.0) / 2.0).exp())
391            };
392
393            // Ensure rates are non-negative
394            let am = am.max(0.0);
395            let bm = bm.max(0.0);
396            let ah = ah.max(0.0);
397            let bh = bh.max(0.0);
398            let an = an.max(0.0);
399            let bn = bn.max(0.0);
400            let ap = ap.max(0.0);
401            let bp = bp.max(0.0);
402
403            // Gate updates
404            self.m += dt_sub * (am * (1.0 - self.m) - bm * self.m);
405            self.h += dt_sub * (ah * (1.0 - self.h) - bh * self.h);
406            self.n += dt_sub * (an * (1.0 - self.n) - bn * self.n);
407            self.p += dt_sub * (ap * (1.0 - self.p) - bp * self.p);
408
409            // Clamp gates
410            self.m = self.m.clamp(0.0, 1.0);
411            self.h = self.h.clamp(0.0, 1.0);
412            self.n = self.n.clamp(0.0, 1.0);
413            self.p = self.p.clamp(0.0, 1.0);
414
415            // GHK permeability-based currents (FH Table 4)
416            let i_na = self.p_na
417                * self.m
418                * self.m
419                * self.h
420                * Self::ghk_current(v, self.na_ratio, self.v_t);
421            let i_k = self.p_k * self.n * self.n * Self::ghk_current(v, self.k_ratio, self.v_t);
422            // p-current uses Na-like concentration ratio (non-specific cation)
423            let i_p = self.p_p * self.p * self.p * Self::ghk_current(v, self.na_ratio, self.v_t);
424            let i_l = self.g_l * (self.v - self.e_l);
425
426            let dv = (-(i_na + i_k + i_p + i_l) + input) / self.c_m;
427            self.v += dt_sub * dv;
428        }
429
430        // Safety
431        self.v = self.v.clamp(-50.0, 150.0);
432        if !self.v.is_finite() {
433            self.v = 0.0;
434        }
435        if !self.m.is_finite() {
436            self.m = 0.005;
437        }
438        if !self.h.is_finite() {
439            self.h = 0.8;
440        }
441        if !self.n.is_finite() {
442            self.n = 0.01;
443        }
444        if !self.p.is_finite() {
445            self.p = 0.01;
446        }
447
448        // Spike detection: V crosses 40 mV upward
449        if self.v >= 40.0 && v_prev < 40.0 {
450            1
451        } else {
452            0
453        }
454    }
455
456    pub fn reset(&mut self) {
457        *self = Self::new();
458    }
459}
460
461// ═══════════════════════════════════════════════════════════════════
462// Node of Ranvier (McIntyre-Richardson-Grill 2002)
463// ═══════════════════════════════════════════════════════════════════
464
465/// Node of Ranvier — McIntyre-Richardson-Grill 2002 model.
466///
467/// Gold-standard nodal model for mammalian myelinated axons. Includes
468/// the specific channel complement of nodes of Ranvier:
469///
470/// - **INaT** (transient Na, Nav1.6): m³h gating, fast activation
471/// - **INaP** (persistent Na, Nav1.6): p³ gating, subthreshold amplification
472/// - **IKs** (slow K, Kv7/KCNQ): s gating, membrane stabilisation
473/// - **IKf** (fast K, Kv3.1-like): fast repolarisation (n⁴ HH-style, optional)
474/// - **IL** (leak)
475///
476/// The persistent Na current (INaP) is critical — it provides subthreshold
477/// amplification and lowers the effective firing threshold, a key feature
478/// of Nav1.6-rich nodes that distinguishes them from generic HH models.
479///
480/// C dV/dt = -(INaT + INaP + IKs + IL) + I_ext
481///
482/// Gating uses Boltzmann steady-state + time-constant formulation
483/// (not alpha/beta) following MRG convention.
484///
485/// McIntyre, Richardson & Grill, J Neurophysiol 87:995, 2002.
486#[derive(Clone, Debug)]
487pub struct NodeOfRanvier {
488    pub v: f64,     // Membrane potential (mV)
489    pub m: f64,     // Nav1.6 transient activation
490    pub h: f64,     // Nav1.6 transient inactivation
491    pub p: f64,     // Nav1.6 persistent activation
492    pub s: f64,     // Kv7 slow K activation
493    pub c_m: f64,   // Nodal capacitance (µF/cm²)
494    pub g_nat: f64, // Transient Na conductance (mS/cm²)
495    pub g_nap: f64, // Persistent Na conductance
496    pub g_ks: f64,  // Slow K (Kv7) conductance
497    pub g_l: f64,   // Leak conductance
498    pub e_na: f64,  // Na reversal (mV)
499    pub e_k: f64,   // K reversal (mV)
500    pub e_l: f64,   // Leak reversal (mV)
501    pub dt: f64,    // External time step (ms)
502    pub sub_steps: usize,
503    pub gain: f64,
504}
505
506impl Default for NodeOfRanvier {
507    fn default() -> Self {
508        Self::new()
509    }
510}
511
512impl NodeOfRanvier {
513    pub fn new() -> Self {
514        Self {
515            v: -80.0,
516            m: 0.01,
517            h: 0.75,
518            p: 0.01,
519            s: 0.05,
520            c_m: 2.0,      // µF/cm² (MRG nodal value)
521            g_nat: 3000.0, // mS/cm² (high Nav1.6 density)
522            g_nap: 5.0,    // Persistent Na (small but critical)
523            g_ks: 80.0,    // Kv7/KCNQ slow K
524            g_l: 7.0,      // Nodal leak (higher than soma)
525            e_na: 50.0,
526            e_k: -90.0,
527            e_l: -90.0,    // MRG nodal resting ~-80 mV
528            dt: 0.5,       // External step (ms)
529            sub_steps: 20, // dt_sub = 0.025 ms
530            gain: 1.0,
531        }
532    }
533
534    /// Boltzmann steady-state: 1 / (1 + exp(-(V - V_half) / k))
535    #[inline]
536    fn boltz(v: f64, v_half: f64, k: f64) -> f64 {
537        1.0 / (1.0 + (-(v - v_half) / k).exp())
538    }
539
540    pub fn step(&mut self, current: f64) -> i32 {
541        let input = self.gain * current;
542        let dt_sub = self.dt / self.sub_steps as f64;
543        let v_prev_ext = self.v;
544
545        for _ in 0..self.sub_steps {
546            let v = self.v;
547
548            // Nav1.6 transient: m gate (fast activation)
549            // MRG: V_half = -26.8 mV, k = 9.2 mV
550            let m_inf = Self::boltz(v, -26.8, 9.2);
551            let tau_m = 0.025 + 0.14 / (1.0 + ((v + 25.0) / 10.0).powi(2)).max(0.01);
552            self.m += dt_sub * (m_inf - self.m) / tau_m;
553
554            // Nav1.6 transient: h gate (inactivation)
555            // MRG: V_half = -55.2 mV, k = -7.4 mV (negative slope)
556            let h_inf = Self::boltz(v, -55.2, -7.4);
557            let tau_h = 0.6 + 4.0 / (1.0 + ((v + 45.0) / 10.0).powi(2)).max(0.01);
558            self.h += dt_sub * (h_inf - self.h) / tau_h;
559
560            // Nav1.6 persistent: p gate (slow activation)
561            // MRG: V_half = -44.0 mV, k = 5.0 mV
562            let p_inf = Self::boltz(v, -44.0, 5.0);
563            let tau_p = 1.0 + 6.0 / (1.0 + ((v + 40.0) / 10.0).powi(2)).max(0.01);
564            self.p += dt_sub * (p_inf - self.p) / tau_p;
565
566            // Kv7 slow K: s gate
567            // MRG: V_half = -30.0 mV, k = 10.0 mV, slow
568            let s_inf = Self::boltz(v, -30.0, 10.0);
569            let tau_s = 20.0 + 60.0 / (1.0 + ((v + 30.0) / 15.0).powi(2)).max(0.01);
570            self.s += dt_sub * (s_inf - self.s) / tau_s;
571
572            // Clamp gates
573            self.m = self.m.clamp(0.0, 1.0);
574            self.h = self.h.clamp(0.0, 1.0);
575            self.p = self.p.clamp(0.0, 1.0);
576            self.s = self.s.clamp(0.0, 1.0);
577
578            // Currents
579            let i_nat = self.g_nat * self.m.powi(3) * self.h * (v - self.e_na);
580            let i_nap = self.g_nap * self.p.powi(3) * (v - self.e_na);
581            let i_ks = self.g_ks * self.s * (v - self.e_k);
582            let i_l = self.g_l * (v - self.e_l);
583
584            let dv = (-(i_nat + i_nap + i_ks + i_l) + input) / self.c_m;
585            self.v += dt_sub * dv;
586        }
587
588        // Safety bounds
589        self.v = self.v.clamp(-120.0, 60.0);
590        if !self.v.is_finite() {
591            self.v = -80.0;
592        }
593        if !self.m.is_finite() {
594            self.m = 0.01;
595        }
596        if !self.h.is_finite() {
597            self.h = 0.75;
598        }
599        if !self.p.is_finite() {
600            self.p = 0.01;
601        }
602        if !self.s.is_finite() {
603            self.s = 0.05;
604        }
605
606        // Spike: V crosses -10 mV upward
607        if self.v >= -10.0 && v_prev_ext < -10.0 {
608            1
609        } else {
610            0
611        }
612    }
613
614    pub fn reset(&mut self) {
615        *self = Self::new();
616    }
617}
618
619// ═══════════════════════════════════════════════════════════════════
620// Myelinated Axon (Saltatory Conduction Segment)
621// ═══════════════════════════════════════════════════════════════════
622
623/// Myelinated axon segment — node of Ranvier + internode cable.
624///
625/// Models a single saltatory conduction unit per the MRG 2002
626/// double-cable architecture: an active node (using the NodeOfRanvier
627/// model) coupled to a passive internode represented as a lumped
628/// RC cable.
629///
630/// The internode has:
631/// - Very low capacitance (~0.001 µF/cm², myelin layers)
632/// - Very high resistance (myelin sheath insulation)
633/// - Paranodal seal resistance (leakage at node-internode junction)
634///
635/// The node voltage drives current through the paranodal seal into
636/// the internode, and the internode voltage feeds back into the node
637/// via the return path. This bidirectional coupling determines the
638/// conduction velocity and safety factor.
639///
640/// The external input represents current arriving from the upstream
641/// node (saltatory propagation).
642///
643/// V_node equation: C_n dV_n/dt = I_ionic(node) + g_para*(V_i - V_n) + I_ext
644/// V_internode equation: C_i dV_i/dt = -g_l_myelin*(V_i - E_l) + g_para*(V_n - V_i)
645///
646/// McIntyre, Richardson & Grill, J Neurophysiol 87:995, 2002.
647/// Richardson et al., Clin Neurophysiol 111:2175, 2000.
648#[derive(Clone, Debug)]
649pub struct MyelinatedAxon {
650    // Node (active, MRG)
651    pub node: NodeOfRanvier,
652    // Internode (passive cable)
653    pub v_inter: f64,    // Internode voltage (mV)
654    pub c_inter: f64,    // Internode capacitance (µF/cm², very low)
655    pub g_l_myelin: f64, // Myelin leak conductance (very low)
656    pub e_l_myelin: f64, // Myelin leak reversal
657    pub g_para: f64,     // Paranodal seal conductance
658    pub dt: f64,
659    pub gain: f64,
660}
661
662impl Default for MyelinatedAxon {
663    fn default() -> Self {
664        Self::new()
665    }
666}
667
668impl MyelinatedAxon {
669    pub fn new() -> Self {
670        Self {
671            node: NodeOfRanvier::new(),
672            v_inter: -80.0,
673            c_inter: 0.001,    // Very low (myelin layers)
674            g_l_myelin: 0.001, // Very low leak through myelin
675            e_l_myelin: -80.0,
676            g_para: 0.01, // Paranodal seal conductance
677            dt: 0.5,
678            gain: 1.0,
679        }
680    }
681
682    pub fn step(&mut self, current: f64) -> i32 {
683        let input = self.gain * current;
684
685        // Paranodal coupling: node ↔ internode
686        let i_para_to_node = self.g_para * (self.v_inter - self.node.v);
687        let i_para_to_inter = self.g_para * (self.node.v - self.v_inter);
688
689        // Internode passive cable dynamics
690        let dv_inter =
691            (-self.g_l_myelin * (self.v_inter - self.e_l_myelin) + i_para_to_inter) / self.c_inter;
692        self.v_inter += self.node.dt / self.node.sub_steps as f64 * dv_inter;
693
694        // Safety bounds for internode
695        self.v_inter = self.v_inter.clamp(-120.0, 60.0);
696        if !self.v_inter.is_finite() {
697            self.v_inter = -80.0;
698        }
699
700        // Step the node with external input + paranodal current
701        // We modify the node's current to include paranodal coupling
702        let total_input = input + i_para_to_node * 100.0; // Scale for node's C_m
703        self.node.step(total_input)
704    }
705
706    /// Access the node membrane potential.
707    pub fn v(&self) -> f64 {
708        self.node.v
709    }
710
711    pub fn reset(&mut self) {
712        self.node.reset();
713        self.v_inter = -80.0;
714    }
715}
716
717// ═══════════════════════════════════════════════════════════════════
718// Cardiac Purkinje Fibre
719// ═══════════════════════════════════════════════════════════════════
720
721/// Cardiac Purkinje fibre — DiFrancesco-Noble 1985 model.
722///
723/// Specialised cardiac conduction cell with long action potentials
724/// (~300 ms) and pacemaker capability via If (funny/HCN current).
725///
726/// 6 major ionic currents:
727/// - **INa** (fast Na, m³h): rapid depolarisation (phase 0)
728/// - **ICaL** (L-type Ca²⁺, d·f): plateau maintenance (phase 2)
729/// - **IKr** (rapid delayed rectifier K, x_r): phase 3 repolarisation
730/// - **IK1** (inward rectifier K): resting potential stabilisation
731/// - **If** (funny current, HCN, y): pacemaker depolarisation (phase 4)
732/// - **IL** (leak)
733///
734/// Action potential phases:
735/// 0 — rapid depolarisation (INa)
736/// 1 — early repolarisation notch
737/// 2 — plateau (ICaL vs IKr balance)
738/// 3 — repolarisation (IKr dominates)
739/// 4 — pacemaker depolarisation (If)
740///
741/// Uses 10 sub-steps (dt_sub = 0.05 ms) for Na gating stability.
742///
743/// DiFrancesco & Noble, Phil Trans R Soc Lond B 307:353, 1985.
744/// Noble, J Physiol 353:1, 1984 (review).
745#[derive(Clone, Debug)]
746pub struct CardiacPurkinjeFibre {
747    pub v: f64,
748    pub m: f64,   // Na activation
749    pub h: f64,   // Na inactivation
750    pub d: f64,   // CaL activation
751    pub f: f64,   // CaL inactivation
752    pub x_r: f64, // IKr activation
753    pub y: f64,   // If (HCN) activation
754    pub c_m: f64,
755    pub g_na: f64,
756    pub g_cal: f64,
757    pub g_kr: f64,
758    pub g_k1: f64,
759    pub g_f: f64, // Funny current conductance
760    pub g_l: f64,
761    pub e_na: f64,
762    pub e_ca: f64,
763    pub e_k: f64,
764    pub e_f: f64, // If reversal (~-20 mV, mixed cation)
765    pub e_l: f64,
766    pub dt: f64,
767    pub sub_steps: usize,
768    pub gain: f64,
769}
770
771impl Default for CardiacPurkinjeFibre {
772    fn default() -> Self {
773        Self::new()
774    }
775}
776
777impl CardiacPurkinjeFibre {
778    pub fn new() -> Self {
779        Self {
780            v: -85.0,
781            m: 0.001,
782            h: 0.99,
783            d: 0.001,
784            f: 0.99,
785            x_r: 0.01,
786            y: 0.05,
787            c_m: 1.0,
788            g_na: 15.0,  // Fast Na
789            g_cal: 0.05, // L-type Ca²⁺ (small but sustains plateau)
790            g_kr: 0.015, // Rapid delayed rectifier
791            g_k1: 0.4,   // Inward rectifier
792            g_f: 0.01,   // Funny current (pacemaker)
793            g_l: 0.03,
794            e_na: 40.0,
795            e_ca: 65.0,
796            e_k: -90.0,
797            e_f: -20.0, // Mixed Na⁺/K⁺ cation
798            e_l: -50.0,
799            dt: 0.5,
800            sub_steps: 10, // dt_sub = 0.05 ms
801            gain: 1.0,
802        }
803    }
804
805    #[inline]
806    fn boltz(v: f64, vh: f64, k: f64) -> f64 {
807        1.0 / (1.0 + (-(v - vh) / k).exp())
808    }
809
810    pub fn step(&mut self, current: f64) -> i32 {
811        let input = self.gain * current;
812        let dt_sub = self.dt / self.sub_steps as f64;
813        let v_prev = self.v;
814
815        for _ in 0..self.sub_steps {
816            let v = self.v;
817
818            // Na m gate (fast)
819            let m_inf = Self::boltz(v, -40.0, 8.0);
820            let tau_m = 0.05 + 0.3 / (1.0 + ((v + 40.0) / 10.0).powi(2)).max(0.01);
821            self.m += dt_sub * (m_inf - self.m) / tau_m;
822
823            // Na h gate (inactivation)
824            let h_inf = Self::boltz(v, -65.0, -7.0);
825            let tau_h = 0.5 + 8.0 / (1.0 + ((v + 65.0) / 15.0).powi(2)).max(0.01);
826            self.h += dt_sub * (h_inf - self.h) / tau_h;
827
828            // CaL d gate (activation)
829            let d_inf = Self::boltz(v, -10.0, 6.0);
830            let tau_d = 2.0 + 5.0 / (1.0 + ((v + 10.0) / 10.0).powi(2)).max(0.01);
831            self.d += dt_sub * (d_inf - self.d) / tau_d;
832
833            // CaL f gate (inactivation, slow)
834            let f_inf = Self::boltz(v, -30.0, -8.0);
835            let tau_f = 20.0 + 100.0 / (1.0 + ((v + 30.0) / 10.0).powi(2)).max(0.01);
836            self.f += dt_sub * (f_inf - self.f) / tau_f;
837
838            // IKr x_r gate (slow activation)
839            let xr_inf = Self::boltz(v, -20.0, 10.0);
840            let tau_xr = 50.0 + 200.0 / (1.0 + ((v + 20.0) / 15.0).powi(2)).max(0.01);
841            self.x_r += dt_sub * (xr_inf - self.x_r) / tau_xr;
842
843            // If y gate (activates at hyperpolarised V)
844            let y_inf = Self::boltz(v, -80.0, -10.0);
845            let tau_y = 100.0 + 500.0 / (1.0 + ((v + 80.0) / 20.0).powi(2)).max(0.01);
846            self.y += dt_sub * (y_inf - self.y) / tau_y;
847
848            // Clamp gates
849            self.m = self.m.clamp(0.0, 1.0);
850            self.h = self.h.clamp(0.0, 1.0);
851            self.d = self.d.clamp(0.0, 1.0);
852            self.f = self.f.clamp(0.0, 1.0);
853            self.x_r = self.x_r.clamp(0.0, 1.0);
854            self.y = self.y.clamp(0.0, 1.0);
855
856            // IK1: inward rectifier (voltage-dependent, Boltzmann)
857            let k1_inf = 1.0 / (1.0 + ((v - self.e_k + 10.0) / 10.0).exp());
858
859            // Currents
860            let i_na = self.g_na * self.m.powi(3) * self.h * (v - self.e_na);
861            let i_cal = self.g_cal * self.d * self.f * (v - self.e_ca);
862            let i_kr = self.g_kr * self.x_r * (v - self.e_k);
863            let i_k1 = self.g_k1 * k1_inf * (v - self.e_k);
864            let i_f = self.g_f * self.y * (v - self.e_f);
865            let i_l = self.g_l * (v - self.e_l);
866
867            let dv = (-(i_na + i_cal + i_kr + i_k1 + i_f + i_l) + input) / self.c_m;
868            self.v += dt_sub * dv;
869        }
870
871        // Safety
872        self.v = self.v.clamp(-120.0, 60.0);
873        if !self.v.is_finite() {
874            self.v = -85.0;
875        }
876        if !self.m.is_finite() {
877            self.m = 0.001;
878        }
879        if !self.h.is_finite() {
880            self.h = 0.99;
881        }
882        if !self.d.is_finite() {
883            self.d = 0.001;
884        }
885        if !self.f.is_finite() {
886            self.f = 0.99;
887        }
888        if !self.x_r.is_finite() {
889            self.x_r = 0.01;
890        }
891        if !self.y.is_finite() {
892            self.y = 0.05;
893        }
894
895        // Spike: V crosses -20 mV upward
896        if self.v >= -20.0 && v_prev < -20.0 {
897            1
898        } else {
899            0
900        }
901    }
902
903    pub fn reset(&mut self) {
904        *self = Self::new();
905    }
906}
907
908// ═══════════════════════════════════════════════════════════════════
909// Smooth Muscle Cell
910// ═══════════════════════════════════════════════════════════════════
911
912/// Smooth muscle cell — slow oscillatory electrical activity.
913///
914/// Visceral/vascular smooth muscle with Ca²⁺-dependent oscillations.
915/// Key features distinct from neurons:
916/// - **No fast Na⁺**: depolarisation is Ca²⁺-dependent (L-type)
917/// - **BK (Ca²⁺-activated K⁺)**: repolarisation via BK channels
918/// - **IP3-mediated Ca²⁺ release**: intracellular Ca²⁺ oscillations
919///   from ER/SR via IP3 receptors drive slow waves
920/// - **SERCA pump**: Ca²⁺ reuptake into stores
921/// - **Slow oscillations**: ~3-12 cycles/min (GI slow waves)
922///
923/// dV/dt = (-ICaL - IBK - IL + I_ext) / C_m
924/// dCa/dt = -alpha*ICaL - SERCA(Ca) + IP3_release(Ca, IP3) - Ca/tau_ca
925///
926/// Hirst & Edwards, J Physiol 531:567, 2001.
927/// Imtiaz et al., Biophys J 83:1877, 2002.
928#[derive(Clone, Debug)]
929pub struct SmoothMuscleCell {
930    pub v: f64,
931    pub d: f64,        // CaL activation
932    pub f: f64,        // CaL inactivation
933    pub ca: f64,       // Cytosolic Ca²⁺ (µM)
934    pub ca_store: f64, // ER/SR Ca²⁺ store (µM)
935    pub c_m: f64,
936    pub g_cal: f64, // L-type Ca²⁺
937    pub g_bk: f64,  // BK channel
938    pub g_l: f64,   // Leak
939    pub e_ca: f64,
940    pub e_k: f64,
941    pub e_l: f64,
942    pub tau_ca: f64,   // Ca²⁺ decay (ms)
943    pub v_serca: f64,  // SERCA pump max rate
944    pub k_serca: f64,  // SERCA Km (µM)
945    pub ip3: f64,      // IP3 concentration (µM, constant or input-driven)
946    pub v_ip3r: f64,   // IP3R max release rate
947    pub k_ip3: f64,    // IP3R half-activation (µM)
948    pub k_ca_ip3: f64, // Ca²⁺ co-activation of IP3R (µM)
949    pub kd_bk: f64,    // BK Ca²⁺ half-activation (µM)
950    pub dt: f64,
951    pub sub_steps: usize,
952    pub gain: f64,
953}
954
955impl Default for SmoothMuscleCell {
956    fn default() -> Self {
957        Self::new()
958    }
959}
960
961impl SmoothMuscleCell {
962    pub fn new() -> Self {
963        Self {
964            v: -60.0,
965            d: 0.01,
966            f: 0.95,
967            ca: 0.1,
968            ca_store: 100.0, // ER/SR store (high, ~100 µM)
969            c_m: 1.0,
970            g_cal: 2.0, // L-type Ca²⁺
971            g_bk: 1.0,  // BK
972            g_l: 0.1,
973            e_ca: 60.0,
974            e_k: -80.0,
975            e_l: -50.0,
976            tau_ca: 50.0,
977            v_serca: 0.5,  // SERCA pump rate
978            k_serca: 0.3,  // SERCA Km
979            ip3: 0.5,      // Tonic IP3
980            v_ip3r: 2.0,   // IP3R release rate
981            k_ip3: 0.3,    // IP3R half-act
982            k_ca_ip3: 0.3, // Ca²⁺ co-activation
983            kd_bk: 0.5,    // BK Ca²⁺ Kd
984            dt: 1.0,       // Slow dynamics (1 ms step)
985            sub_steps: 4,
986            gain: 1.0,
987        }
988    }
989
990    #[inline]
991    fn boltz(v: f64, vh: f64, k: f64) -> f64 {
992        1.0 / (1.0 + (-(v - vh) / k).exp())
993    }
994
995    pub fn step(&mut self, current: f64) -> i32 {
996        let input = self.gain * current;
997        let dt_sub = self.dt / self.sub_steps as f64;
998        let v_prev = self.v;
999
1000        for _ in 0..self.sub_steps {
1001            let v = self.v;
1002
1003            // CaL d gate (activation)
1004            let d_inf = Self::boltz(v, -20.0, 6.0);
1005            let tau_d = 5.0 + 20.0 / (1.0 + ((v + 20.0) / 10.0).powi(2)).max(0.01);
1006            self.d += dt_sub * (d_inf - self.d) / tau_d;
1007
1008            // CaL f gate (slow inactivation)
1009            let f_inf = Self::boltz(v, -35.0, -8.0);
1010            let tau_f = 50.0 + 200.0 / (1.0 + ((v + 35.0) / 10.0).powi(2)).max(0.01);
1011            self.f += dt_sub * (f_inf - self.f) / tau_f;
1012
1013            self.d = self.d.clamp(0.0, 1.0);
1014            self.f = self.f.clamp(0.0, 1.0);
1015
1016            // BK: Ca²⁺-dependent + voltage-dependent
1017            let bk_ca = self.ca * self.ca / (self.ca * self.ca + self.kd_bk * self.kd_bk);
1018            let bk_v = Self::boltz(v, -10.0, 15.0);
1019            let bk_inf = bk_ca * bk_v;
1020
1021            // Currents
1022            let i_cal = self.g_cal * self.d * self.f * (v - self.e_ca);
1023            let i_bk = self.g_bk * bk_inf * (v - self.e_k);
1024            let i_l = self.g_l * (v - self.e_l);
1025
1026            let dv = (-(i_cal + i_bk + i_l) + input) / self.c_m;
1027            self.v += dt_sub * dv;
1028
1029            // Ca²⁺ dynamics
1030            // Entry via CaL (inward = negative current = Ca²⁺ entry)
1031            let ca_entry = if i_cal < 0.0 { -i_cal * 0.01 } else { 0.0 };
1032
1033            // IP3R release from store: Ca²⁺-induced Ca²⁺ release (CICR)
1034            let ip3_act = self.ip3 / (self.ip3 + self.k_ip3);
1035            let ca_act = self.ca / (self.ca + self.k_ca_ip3);
1036            let ip3_release = self.v_ip3r * ip3_act * ca_act * self.ca_store;
1037
1038            // SERCA pump (reuptake into store)
1039            let serca = self.v_serca * self.ca * self.ca
1040                / (self.ca * self.ca + self.k_serca * self.k_serca);
1041
1042            // Ca²⁺ dynamics
1043            self.ca += dt_sub * (ca_entry + ip3_release - serca - self.ca / self.tau_ca);
1044            self.ca_store += dt_sub * (serca - ip3_release);
1045
1046            self.ca = self.ca.max(0.0);
1047            self.ca_store = self.ca_store.max(0.0);
1048        }
1049
1050        // Safety
1051        self.v = self.v.clamp(-100.0, 40.0);
1052        if !self.v.is_finite() {
1053            self.v = -60.0;
1054        }
1055        if !self.ca.is_finite() {
1056            self.ca = 0.1;
1057        }
1058        if !self.ca_store.is_finite() {
1059            self.ca_store = 100.0;
1060        }
1061
1062        // "Spike" = slow wave crossing -30 mV
1063        if self.v >= -30.0 && v_prev < -30.0 {
1064            1
1065        } else {
1066            0
1067        }
1068    }
1069
1070    pub fn reset(&mut self) {
1071        *self = Self::new();
1072    }
1073}
1074
1075// ═══════════════════════════════════════════════════════════════════
1076// Endocrine Beta Cell (Pancreatic)
1077// ═══════════════════════════════════════════════════════════════════
1078
1079/// Pancreatic beta cell — glucose-dependent bursting.
1080///
1081/// Beta cells in the islets of Langerhans secrete insulin in response
1082/// to elevated glucose. The electrical signature is bursting: clusters
1083/// of spikes on a slow wave, driven by:
1084/// - **ICaL** (L-type Ca²⁺): spike depolarisation
1085/// - **IK_dr** (delayed rectifier K): spike repolarisation
1086/// - **IK_ATP** (ATP-sensitive K): glucose-dependent, closes with
1087///   rising glucose (metabolic coupling)
1088/// - **IK_Ca** (Ca²⁺-activated K, SK): slow burst termination
1089/// - **IL** (leak)
1090///
1091/// Burst mechanism: Ca²⁺ accumulates during spike burst → SK
1092/// activates → hyperpolarises → Ca²⁺ decays → SK deactivates →
1093/// next burst. IK_ATP sets the threshold: low glucose → open
1094/// IK_ATP → silent; high glucose → closed IK_ATP → bursting.
1095///
1096/// Chay & Keizer, Biophys J 42:181, 1983.
1097/// Sherman et al., Biophys J 54:411, 1988.
1098#[derive(Clone, Debug)]
1099pub struct EndocrineBetaCell {
1100    pub v: f64,
1101    pub n: f64,  // K_dr activation
1102    pub ca: f64, // Intracellular Ca²⁺ (µM)
1103    pub c_m: f64,
1104    pub g_cal: f64,  // L-type Ca²⁺
1105    pub g_kdr: f64,  // Delayed rectifier K
1106    pub g_katp: f64, // ATP-sensitive K (glucose-dependent)
1107    pub g_kca: f64,  // Ca²⁺-activated K (SK, burst termination)
1108    pub g_l: f64,
1109    pub e_ca: f64,
1110    pub e_k: f64,
1111    pub e_l: f64,
1112    pub tau_ca: f64,    // Ca²⁺ decay (ms)
1113    pub kd_kca: f64,    // SK Ca²⁺ Kd (µM)
1114    pub atp_level: f64, // ATP/ADP ratio (proxy for glucose, 0-1)
1115    pub dt: f64,
1116    pub sub_steps: usize,
1117    pub gain: f64,
1118}
1119
1120impl Default for EndocrineBetaCell {
1121    fn default() -> Self {
1122        Self::new()
1123    }
1124}
1125
1126impl EndocrineBetaCell {
1127    pub fn new() -> Self {
1128        Self {
1129            v: -70.0,
1130            n: 0.01,
1131            ca: 0.1,
1132            c_m: 1.0,
1133            g_cal: 5.0,  // L-type Ca²⁺ (strong, no Na for depolarisation)
1134            g_kdr: 4.0,  // Delayed rectifier
1135            g_katp: 3.0, // ATP-sensitive K (max conductance)
1136            g_kca: 2.0,  // SK for burst termination
1137            g_l: 0.1,
1138            e_ca: 50.0,
1139            e_k: -75.0,
1140            e_l: -30.0,     // Depolarised leak (typical for beta cells)
1141            tau_ca: 100.0,  // Ca²⁺ decay (ms)
1142            kd_kca: 0.5,    // SK Kd
1143            atp_level: 0.3, // Moderate glucose → some IK_ATP open
1144            dt: 0.5,
1145            sub_steps: 4,
1146            gain: 1.0,
1147        }
1148    }
1149
1150    #[inline]
1151    fn boltz(v: f64, vh: f64, k: f64) -> f64 {
1152        1.0 / (1.0 + (-(v - vh) / k).exp())
1153    }
1154
1155    pub fn step(&mut self, current: f64) -> i32 {
1156        let input = self.gain * current;
1157        let dt_sub = self.dt / self.sub_steps as f64;
1158        let v_prev = self.v;
1159
1160        for _ in 0..self.sub_steps {
1161            let v = self.v;
1162
1163            // CaL (instantaneous m, m_inf Boltzmann)
1164            let m_cal_inf = Self::boltz(v, -20.0, 8.0);
1165
1166            // K_dr n gate
1167            let n_inf = Self::boltz(v, -15.0, 6.0);
1168            let tau_n = 5.0 + 20.0 / (1.0 + ((v + 15.0) / 10.0).powi(2)).max(0.01);
1169            self.n += dt_sub * (n_inf - self.n) / tau_n;
1170            self.n = self.n.clamp(0.0, 1.0);
1171
1172            // IK_ATP: closes with rising ATP (glucose)
1173            // g_eff = g_katp * (1 - atp_level)
1174            let g_katp_eff = self.g_katp * (1.0 - self.atp_level);
1175
1176            // IK_Ca: Ca²⁺-dependent (Hill n=2)
1177            let kca_inf = self.ca * self.ca / (self.ca * self.ca + self.kd_kca * self.kd_kca);
1178
1179            // Currents
1180            let i_cal = self.g_cal * m_cal_inf * (v - self.e_ca);
1181            let i_kdr = self.g_kdr * self.n.powi(4) * (v - self.e_k);
1182            let i_katp = g_katp_eff * (v - self.e_k);
1183            let i_kca = self.g_kca * kca_inf * (v - self.e_k);
1184            let i_l = self.g_l * (v - self.e_l);
1185
1186            let dv = (-(i_cal + i_kdr + i_katp + i_kca + i_l) + input) / self.c_m;
1187            self.v += dt_sub * dv;
1188
1189            // Ca²⁺ dynamics
1190            let ca_entry = if i_cal < 0.0 { -i_cal * 0.002 } else { 0.0 };
1191            self.ca += dt_sub * (ca_entry - self.ca / self.tau_ca);
1192            self.ca = self.ca.max(0.0);
1193        }
1194
1195        // Safety
1196        self.v = self.v.clamp(-100.0, 40.0);
1197        if !self.v.is_finite() {
1198            self.v = -70.0;
1199        }
1200        if !self.n.is_finite() {
1201            self.n = 0.01;
1202        }
1203        if !self.ca.is_finite() {
1204            self.ca = 0.1;
1205        }
1206
1207        // Spike: V crosses -20 mV
1208        if self.v >= -20.0 && v_prev < -20.0 {
1209            1
1210        } else {
1211            0
1212        }
1213    }
1214
1215    pub fn reset(&mut self) {
1216        *self = Self::new();
1217    }
1218}
1219
1220// ═══════════════════════════════════════════════════════════════════
1221// Tests
1222// ═══════════════════════════════════════════════════════════════════
1223
1224#[cfg(test)]
1225mod tests {
1226    use super::*;
1227
1228    // -- Graded Synapse Neuron tests --
1229
1230    #[test]
1231    fn graded_depolarises_with_input() {
1232        let mut n = GradedSynapseNeuron::new();
1233        let v0 = n.v;
1234        for _ in 0..10_000 {
1235            n.step(200.0);
1236        }
1237        assert!(
1238            n.v > v0,
1239            "Must depolarise with positive input: v0={v0}, v={}",
1240            n.v
1241        );
1242    }
1243
1244    #[test]
1245    fn graded_hyperpolarises_with_negative_input() {
1246        let mut n = GradedSynapseNeuron::new();
1247        let v0 = n.v;
1248        for _ in 0..10_000 {
1249            n.step(-200.0);
1250        }
1251        assert!(
1252            n.v < v0,
1253            "Must hyperpolarise with negative input: v0={v0}, v={}",
1254            n.v
1255        );
1256    }
1257
1258    #[test]
1259    fn graded_fires_with_strong_input() {
1260        let mut n = GradedSynapseNeuron::new();
1261        let mut spikes = 0;
1262        for _ in 0..50_000 {
1263            spikes += n.step(500.0);
1264        }
1265        assert!(
1266            spikes > 0,
1267            "Must cross threshold with strong input, got {spikes}"
1268        );
1269    }
1270
1271    #[test]
1272    fn graded_silent_without_input() {
1273        let mut n = GradedSynapseNeuron::new();
1274        let mut spikes = 0;
1275        for _ in 0..50_000 {
1276            spikes += n.step(0.0);
1277        }
1278        assert_eq!(
1279            spikes, 0,
1280            "Must be silent without input (V starts at E_L), got {spikes}"
1281        );
1282    }
1283
1284    #[test]
1285    fn graded_release_monotonic() {
1286        // Release should increase with depolarisation
1287        let mut n = GradedSynapseNeuron::new();
1288        n.v = -70.0;
1289        let r_low = n.release();
1290        n.v = -40.0;
1291        let r_mid = n.release();
1292        n.v = -10.0;
1293        let r_high = n.release();
1294        assert!(
1295            r_low < r_mid && r_mid < r_high,
1296            "Release must be monotonic: r_low={r_low:.3}, r_mid={r_mid:.3}, r_high={r_high:.3}"
1297        );
1298    }
1299
1300    #[test]
1301    fn graded_release_bounded() {
1302        let mut n = GradedSynapseNeuron::new();
1303        n.v = -100.0;
1304        assert!(n.release() >= 0.0 && n.release() <= 1.0);
1305        n.v = 50.0;
1306        assert!(n.release() >= 0.0 && n.release() <= 1.0);
1307    }
1308
1309    #[test]
1310    fn graded_v_saturates() {
1311        let mut n = GradedSynapseNeuron::new();
1312        for _ in 0..50_000 {
1313            n.step(1e6);
1314        }
1315        assert!(
1316            n.v <= n.v_max,
1317            "V must not exceed v_max={}, got {}",
1318            n.v_max,
1319            n.v
1320        );
1321        n.reset();
1322        for _ in 0..50_000 {
1323            n.step(-1e6);
1324        }
1325        assert!(
1326            n.v >= n.v_min,
1327            "V must not go below v_min={}, got {}",
1328            n.v_min,
1329            n.v
1330        );
1331    }
1332
1333    #[test]
1334    fn graded_nan_input_stays_finite() {
1335        let mut n = GradedSynapseNeuron::new();
1336        n.step(f64::NAN);
1337        assert!(n.v.is_finite());
1338    }
1339
1340    #[test]
1341    fn graded_reset_clears_state() {
1342        let mut n = GradedSynapseNeuron::new();
1343        for _ in 0..10_000 {
1344            n.step(500.0);
1345        }
1346        n.reset();
1347        assert_eq!(n.v, -60.0);
1348    }
1349
1350    #[test]
1351    fn graded_performance_100k_steps() {
1352        let start = std::time::Instant::now();
1353        let mut n = GradedSynapseNeuron::new();
1354        for _ in 0..100_000 {
1355            std::hint::black_box(n.step(100.0));
1356        }
1357        let elapsed = start.elapsed();
1358        assert!(
1359            elapsed.as_millis() < 50,
1360            "100k steps must complete in <50ms"
1361        );
1362    }
1363
1364    // -- Gap Junction Neuron tests --
1365
1366    #[test]
1367    fn gap_fires_with_depolarising_drive() {
1368        // Input as V_neighbor = 0 mV (depolarised relative to -65 mV rest)
1369        let mut n = GapJunctionNeuron::new();
1370        let mut spikes = 0;
1371        for _ in 0..50_000 {
1372            spikes += n.step(0.0); // V_neighbor = 0 mV → depolarising
1373        }
1374        assert!(
1375            spikes > 0,
1376            "Gap junction must fire with depolarising drive, got {spikes}"
1377        );
1378    }
1379
1380    #[test]
1381    fn gap_silent_at_rest() {
1382        // Input = E_L → no gap junction current → silent
1383        let mut n = GapJunctionNeuron::new();
1384        let mut spikes = 0;
1385        for _ in 0..50_000 {
1386            spikes += n.step(-65.0); // V_neighbor = E_L → zero gap current
1387        }
1388        assert_eq!(
1389            spikes, 0,
1390            "Must be silent when V_neighbor = E_L, got {spikes}"
1391        );
1392    }
1393
1394    #[test]
1395    fn gap_junction_pulls_toward_neighbor() {
1396        // If V_neighbor > V, gap junction depolarises; if V_neighbor < V, hyperpolarises
1397        let mut n = GapJunctionNeuron::new(); // V = -65
1398        for _ in 0..5_000 {
1399            n.step(-40.0);
1400        } // V_neighbor = -40 → depolarising
1401        assert!(
1402            n.v > -65.0 || n.refrac_timer > 0.0,
1403            "Gap junction must pull V toward neighbor: v={}",
1404            n.v
1405        );
1406    }
1407
1408    #[test]
1409    fn gap_stronger_coupling_more_spikes() {
1410        let mut weak = GapJunctionNeuron::new();
1411        weak.g_gap = 0.01;
1412        let mut strong = GapJunctionNeuron::new();
1413        strong.g_gap = 0.1;
1414        let (mut sw, mut ss) = (0, 0);
1415        for _ in 0..50_000 {
1416            sw += weak.step(-20.0);
1417            ss += strong.step(-20.0);
1418        }
1419        assert!(
1420            ss >= sw,
1421            "Stronger coupling → more spikes: strong={ss} vs weak={sw}"
1422        );
1423    }
1424
1425    #[test]
1426    fn gap_refractory_enforced() {
1427        let mut n = GapJunctionNeuron::new();
1428        // Drive until first spike (V_neighbor = 0 → strong depolarising)
1429        let mut first_spike_t = 0;
1430        for t in 0..10_000 {
1431            if n.step(0.0) == 1 {
1432                first_spike_t = t;
1433                break;
1434            }
1435        }
1436        assert!(first_spike_t > 0, "Must spike");
1437        // Next step should be in refractory
1438        assert!(n.refrac_timer > 0.0, "Must be in refractory after spike");
1439        assert_eq!(n.step(0.0), 0, "Must not spike during refractory");
1440    }
1441
1442    #[test]
1443    fn gap_hyperpolarising_drive_silent() {
1444        // V_neighbor = -100 → strong hyperpolarising gap current
1445        let mut n = GapJunctionNeuron::new();
1446        let mut spikes = 0;
1447        for _ in 0..50_000 {
1448            spikes += n.step(-100.0);
1449        }
1450        assert_eq!(
1451            spikes, 0,
1452            "Hyperpolarising drive must keep silent, got {spikes}"
1453        );
1454    }
1455
1456    #[test]
1457    fn gap_tonic_current_depolarises() {
1458        let mut n = GapJunctionNeuron::new();
1459        n.i_tonic = 5.0; // Strong tonic drive
1460        let mut spikes = 0;
1461        for _ in 0..50_000 {
1462            spikes += n.step(-65.0); // No gap drive, but tonic current
1463        }
1464        assert!(
1465            spikes > 0,
1466            "Tonic current should produce spikes, got {spikes}"
1467        );
1468    }
1469
1470    #[test]
1471    fn gap_nan_input_stays_finite() {
1472        let mut n = GapJunctionNeuron::new();
1473        n.step(f64::NAN);
1474        assert!(n.v.is_finite());
1475    }
1476
1477    #[test]
1478    fn gap_reset_clears_state() {
1479        let mut n = GapJunctionNeuron::new();
1480        for _ in 0..10_000 {
1481            n.step(-20.0);
1482        }
1483        n.reset();
1484        assert_eq!(n.v, -65.0);
1485        assert_eq!(n.refrac_timer, 0.0);
1486    }
1487
1488    #[test]
1489    fn gap_rectification_reduces_at_large_vj() {
1490        // At large |Vj|, rectification should reduce effective conductance
1491        let n = GapJunctionNeuron::new();
1492        let g_small = n.rect_conductance(5.0); // |Vj|=5 mV (small)
1493        let g_large = n.rect_conductance(60.0); // |Vj|=60 mV (large)
1494        assert!(
1495            g_small > g_large,
1496            "Rectification must reduce g at large Vj: g(5)={g_small:.3} vs g(60)={g_large:.3}"
1497        );
1498        assert!(
1499            g_large >= n.rect_gmin,
1500            "Conductance must not drop below g_min={}: got {g_large:.3}",
1501            n.rect_gmin
1502        );
1503    }
1504
1505    #[test]
1506    fn gap_performance_100k_steps() {
1507        let start = std::time::Instant::now();
1508        let mut n = GapJunctionNeuron::new();
1509        for _ in 0..100_000 {
1510            std::hint::black_box(n.step(-20.0));
1511        }
1512        let elapsed = start.elapsed();
1513        assert!(
1514            elapsed.as_millis() < 50,
1515            "100k steps must complete in <50ms"
1516        );
1517    }
1518
1519    // -- Frankenhaeuser-Huxley Axon tests --
1520
1521    #[test]
1522    fn fh_fires_with_input() {
1523        // FH model uses µA/cm² — need ~1000+ for spiking (FH 1964 Fig 3)
1524        let mut n = FrankenhaeUserHuxleyAxon::new();
1525        let mut spikes = 0;
1526        for _ in 0..2_000 {
1527            spikes += n.step(2000.0);
1528        }
1529        assert!(
1530            spikes > 0,
1531            "FH axon must fire with strong input, got {spikes}"
1532        );
1533    }
1534
1535    #[test]
1536    fn fh_silent_without_input() {
1537        let mut n = FrankenhaeUserHuxleyAxon::new();
1538        let mut spikes = 0;
1539        for _ in 0..5_000 {
1540            spikes += n.step(0.0);
1541        }
1542        assert_eq!(
1543            spikes, 0,
1544            "FH axon must be silent without input, got {spikes}"
1545        );
1546    }
1547
1548    #[test]
1549    fn fh_action_potential_shape() {
1550        // AP should depolarise well above 60 mV (spike threshold)
1551        let mut n = FrankenhaeUserHuxleyAxon::new();
1552        let mut v_max = -100.0_f64;
1553        for _ in 0..500 {
1554            n.step(2000.0);
1555            v_max = v_max.max(n.v);
1556        }
1557        assert!(v_max > 40.0, "AP peak should exceed 40 mV, got {v_max:.1}");
1558    }
1559
1560    #[test]
1561    fn fh_gating_evolves() {
1562        let mut n = FrankenhaeUserHuxleyAxon::new();
1563        let m0 = n.m;
1564        let h0 = n.h;
1565        for _ in 0..100 {
1566            n.step(2000.0);
1567        }
1568        assert!(n.m != m0 || n.h != h0, "Gating variables must evolve");
1569    }
1570
1571    #[test]
1572    fn fh_four_gates() {
1573        // All 4 gates (m, h, n, p) must evolve during spiking
1574        let mut n = FrankenhaeUserHuxleyAxon::new();
1575        for _ in 0..200 {
1576            n.step(2000.0);
1577        }
1578        // After spiking: m should have risen, h should have fallen
1579        // n and p should have changed from initial
1580        assert!(
1581            n.m > 0.005 || n.h < 0.8 || n.n > 0.01 || n.p > 0.01,
1582            "All gates must evolve: m={:.3}, h={:.3}, n={:.3}, p={:.3}",
1583            n.m,
1584            n.h,
1585            n.n,
1586            n.p
1587        );
1588    }
1589
1590    #[test]
1591    fn fh_stronger_input_more_spikes() {
1592        let mut weak = FrankenhaeUserHuxleyAxon::new();
1593        let mut strong = FrankenhaeUserHuxleyAxon::new();
1594        let (mut sw, mut ss) = (0, 0);
1595        for _ in 0..2_000 {
1596            sw += weak.step(1000.0);
1597            ss += strong.step(3000.0);
1598        }
1599        assert!(
1600            ss >= sw,
1601            "Stronger input → more spikes: strong={ss} vs weak={sw}"
1602        );
1603    }
1604
1605    #[test]
1606    fn fh_all_gates_bounded() {
1607        let mut n = FrankenhaeUserHuxleyAxon::new();
1608        for _ in 0..2_000 {
1609            n.step(3000.0);
1610        }
1611        assert!(n.m >= 0.0 && n.m <= 1.0, "m out of bounds: {}", n.m);
1612        assert!(n.h >= 0.0 && n.h <= 1.0, "h out of bounds: {}", n.h);
1613        assert!(n.n >= 0.0 && n.n <= 1.0, "n out of bounds: {}", n.n);
1614        assert!(n.p >= 0.0 && n.p <= 1.0, "p out of bounds: {}", n.p);
1615    }
1616
1617    #[test]
1618    fn fh_nan_input_stays_finite() {
1619        let mut n = FrankenhaeUserHuxleyAxon::new();
1620        n.step(f64::NAN);
1621        assert!(n.v.is_finite());
1622        assert!(n.m.is_finite());
1623    }
1624
1625    #[test]
1626    fn fh_reset_clears_state() {
1627        let mut n = FrankenhaeUserHuxleyAxon::new();
1628        for _ in 0..500 {
1629            n.step(2000.0);
1630        }
1631        n.reset();
1632        assert_eq!(n.v, 0.0);
1633        assert_eq!(n.m, 0.005);
1634        assert_eq!(n.h, 0.8);
1635    }
1636
1637    #[test]
1638    fn fh_performance_1k_steps() {
1639        let start = std::time::Instant::now();
1640        let mut n = FrankenhaeUserHuxleyAxon::new();
1641        for _ in 0..1_000 {
1642            std::hint::black_box(n.step(1500.0));
1643        }
1644        let elapsed = start.elapsed();
1645        // 50 sub-steps per step → 50k total iterations
1646        assert!(
1647            elapsed.as_millis() < 100,
1648            "1k steps must complete in <100ms"
1649        );
1650    }
1651
1652    // -- Node of Ranvier (MRG 2002) tests --
1653
1654    #[test]
1655    fn nor_fires_with_input() {
1656        let mut n = NodeOfRanvier::new();
1657        let mut spikes = 0;
1658        for _ in 0..2_000 {
1659            spikes += n.step(500.0);
1660        }
1661        assert!(
1662            spikes > 0,
1663            "Node of Ranvier must fire with input, got {spikes}"
1664        );
1665    }
1666
1667    #[test]
1668    fn nor_silent_without_input() {
1669        let mut n = NodeOfRanvier::new();
1670        let mut spikes = 0;
1671        for _ in 0..5_000 {
1672            spikes += n.step(0.0);
1673        }
1674        assert_eq!(spikes, 0, "Must be silent without input, got {spikes}");
1675    }
1676
1677    #[test]
1678    fn nor_high_nat_density() {
1679        // Node of Ranvier has g_nat=3000 (much higher than standard HH ~120)
1680        let n = NodeOfRanvier::new();
1681        assert!(
1682            n.g_nat > 1000.0,
1683            "Nodal transient Na should be very high: g_nat={}",
1684            n.g_nat
1685        );
1686    }
1687
1688    #[test]
1689    fn nor_has_persistent_na() {
1690        // MRG model must include persistent Na — distinguishes from generic HH
1691        let n = NodeOfRanvier::new();
1692        assert!(
1693            n.g_nap > 0.0,
1694            "MRG model must have persistent Na current: g_nap={}",
1695            n.g_nap
1696        );
1697    }
1698
1699    #[test]
1700    fn nor_has_kv7_slow_k() {
1701        // Kv7 (KCNQ) is the dominant K channel at nodes, not Kv3 or Kv1
1702        let n = NodeOfRanvier::new();
1703        assert!(
1704            n.g_ks > 0.0,
1705            "MRG model must have slow K (Kv7): g_ks={}",
1706            n.g_ks
1707        );
1708    }
1709
1710    #[test]
1711    fn nor_persistent_na_lowers_threshold() {
1712        // With persistent Na, less current is needed to fire
1713        let mut with_nap = NodeOfRanvier::new();
1714        let mut no_nap = NodeOfRanvier::new();
1715        no_nap.g_nap = 0.0;
1716        let (mut s_with, mut s_without) = (0, 0);
1717        for _ in 0..2_000 {
1718            s_with += with_nap.step(200.0);
1719            s_without += no_nap.step(200.0);
1720        }
1721        assert!(
1722            s_with >= s_without,
1723            "Persistent Na should lower threshold: with={s_with} vs without={s_without}"
1724        );
1725    }
1726
1727    #[test]
1728    fn nor_gating_evolves() {
1729        let mut n = NodeOfRanvier::new();
1730        let m0 = n.m;
1731        let p0 = n.p;
1732        for _ in 0..100 {
1733            n.step(500.0);
1734        }
1735        assert!(
1736            n.m != m0 || n.p != p0,
1737            "Gating must evolve: m={:.3}, p={:.3}",
1738            n.m,
1739            n.p
1740        );
1741    }
1742
1743    #[test]
1744    fn nor_nan_input_stays_finite() {
1745        let mut n = NodeOfRanvier::new();
1746        n.step(f64::NAN);
1747        assert!(n.v.is_finite());
1748        assert!(n.m.is_finite());
1749        assert!(n.p.is_finite());
1750        assert!(n.s.is_finite());
1751    }
1752
1753    #[test]
1754    fn nor_reset_clears_state() {
1755        let mut n = NodeOfRanvier::new();
1756        for _ in 0..500 {
1757            n.step(500.0);
1758        }
1759        n.reset();
1760        assert_eq!(n.v, -80.0);
1761        assert_eq!(n.m, 0.01);
1762        assert_eq!(n.p, 0.01);
1763        assert_eq!(n.s, 0.05);
1764    }
1765
1766    #[test]
1767    fn nor_performance_1k_steps() {
1768        let start = std::time::Instant::now();
1769        let mut n = NodeOfRanvier::new();
1770        for _ in 0..1_000 {
1771            std::hint::black_box(n.step(500.0));
1772        }
1773        let elapsed = start.elapsed();
1774        assert!(elapsed.as_millis() < 50, "1k steps must complete in <50ms");
1775    }
1776
1777    // -- Myelinated Axon tests --
1778
1779    #[test]
1780    fn myelin_fires_with_input() {
1781        let mut n = MyelinatedAxon::new();
1782        let mut spikes = 0;
1783        for _ in 0..2_000 {
1784            spikes += n.step(500.0);
1785        }
1786        assert!(spikes > 0, "Myelinated axon must fire, got {spikes}");
1787    }
1788
1789    #[test]
1790    fn myelin_silent_without_input() {
1791        let mut n = MyelinatedAxon::new();
1792        let mut spikes = 0;
1793        for _ in 0..5_000 {
1794            spikes += n.step(0.0);
1795        }
1796        assert_eq!(spikes, 0, "Must be silent without input, got {spikes}");
1797    }
1798
1799    #[test]
1800    fn myelin_internode_coupling() {
1801        // Internode voltage should be affected by node spiking
1802        let mut n = MyelinatedAxon::new();
1803        let v_inter_0 = n.v_inter;
1804        for _ in 0..500 {
1805            n.step(500.0);
1806        }
1807        assert!(
1808            (n.v_inter - v_inter_0).abs() > 0.001,
1809            "Internode voltage should change with node activity: v_inter={}",
1810            n.v_inter
1811        );
1812    }
1813
1814    #[test]
1815    fn myelin_has_low_capacitance() {
1816        let n = MyelinatedAxon::new();
1817        assert!(
1818            n.c_inter < 0.01,
1819            "Myelin capacitance must be very low: {}",
1820            n.c_inter
1821        );
1822    }
1823
1824    #[test]
1825    fn myelin_has_low_myelin_leak() {
1826        let n = MyelinatedAxon::new();
1827        assert!(
1828            n.g_l_myelin < 0.01,
1829            "Myelin leak must be very low: {}",
1830            n.g_l_myelin
1831        );
1832    }
1833
1834    #[test]
1835    fn myelin_has_paranodal_seal() {
1836        let n = MyelinatedAxon::new();
1837        assert!(n.g_para > 0.0, "Must have paranodal seal conductance");
1838    }
1839
1840    #[test]
1841    fn myelin_stronger_input_more_spikes() {
1842        let mut weak = MyelinatedAxon::new();
1843        let mut strong = MyelinatedAxon::new();
1844        let (mut sw, mut ss) = (0, 0);
1845        for _ in 0..2_000 {
1846            sw += weak.step(300.0);
1847            ss += strong.step(1000.0);
1848        }
1849        assert!(ss >= sw, "Stronger → more spikes: strong={ss} vs weak={sw}");
1850    }
1851
1852    #[test]
1853    fn myelin_nan_input_stays_finite() {
1854        let mut n = MyelinatedAxon::new();
1855        n.step(f64::NAN);
1856        assert!(n.v().is_finite());
1857        assert!(n.v_inter.is_finite());
1858    }
1859
1860    #[test]
1861    fn myelin_reset_clears_state() {
1862        let mut n = MyelinatedAxon::new();
1863        for _ in 0..500 {
1864            n.step(500.0);
1865        }
1866        n.reset();
1867        assert_eq!(n.v_inter, -80.0);
1868        assert_eq!(n.node.v, -80.0);
1869    }
1870
1871    #[test]
1872    fn myelin_performance_1k_steps() {
1873        let start = std::time::Instant::now();
1874        let mut n = MyelinatedAxon::new();
1875        for _ in 0..1_000 {
1876            std::hint::black_box(n.step(500.0));
1877        }
1878        let elapsed = start.elapsed();
1879        assert!(elapsed.as_millis() < 50, "1k steps must complete in <50ms");
1880    }
1881
1882    // -- Cardiac Purkinje Fibre tests --
1883
1884    #[test]
1885    fn cardiac_fires_with_input() {
1886        let mut n = CardiacPurkinjeFibre::new();
1887        let mut spikes = 0;
1888        for _ in 0..2_000 {
1889            spikes += n.step(5.0);
1890        }
1891        assert!(
1892            spikes > 0,
1893            "Cardiac Purkinje must fire with input, got {spikes}"
1894        );
1895    }
1896
1897    #[test]
1898    fn cardiac_silent_without_input() {
1899        // Without If-driven pacemaking (test with g_f=0)
1900        let mut n = CardiacPurkinjeFibre::new();
1901        n.g_f = 0.0; // Disable pacemaker
1902        let mut spikes = 0;
1903        for _ in 0..5_000 {
1904            spikes += n.step(0.0);
1905        }
1906        assert!(
1907            spikes <= 1,
1908            "Must be essentially silent without pacemaker, got {spikes}"
1909        );
1910    }
1911
1912    #[test]
1913    fn cardiac_has_funny_current() {
1914        // If (HCN) is the hallmark pacemaker current
1915        let n = CardiacPurkinjeFibre::new();
1916        assert!(n.g_f > 0.0, "Must have funny current (If/HCN)");
1917    }
1918
1919    #[test]
1920    fn cardiac_has_cal() {
1921        // L-type Ca²⁺ sustains the plateau
1922        let n = CardiacPurkinjeFibre::new();
1923        assert!(n.g_cal > 0.0, "Must have L-type Ca²⁺ for plateau");
1924    }
1925
1926    #[test]
1927    fn cardiac_has_inward_rectifier() {
1928        // IK1 stabilises resting potential
1929        let n = CardiacPurkinjeFibre::new();
1930        assert!(n.g_k1 > 0.0, "Must have IK1 inward rectifier");
1931    }
1932
1933    #[test]
1934    fn cardiac_six_currents() {
1935        let n = CardiacPurkinjeFibre::new();
1936        assert!(
1937            n.g_na > 0.0
1938                && n.g_cal > 0.0
1939                && n.g_kr > 0.0
1940                && n.g_k1 > 0.0
1941                && n.g_f > 0.0
1942                && n.g_l > 0.0,
1943            "Must have all 6 currents"
1944        );
1945    }
1946
1947    #[test]
1948    fn cardiac_gating_evolves() {
1949        let mut n = CardiacPurkinjeFibre::new();
1950        let d0 = n.d;
1951        let y0 = n.y;
1952        for _ in 0..200 {
1953            n.step(5.0);
1954        }
1955        assert!(n.d != d0 || n.y != y0, "Gating must evolve");
1956    }
1957
1958    #[test]
1959    fn cardiac_nan_input_stays_finite() {
1960        let mut n = CardiacPurkinjeFibre::new();
1961        n.step(f64::NAN);
1962        assert!(n.v.is_finite());
1963    }
1964
1965    #[test]
1966    fn cardiac_reset_clears_state() {
1967        let mut n = CardiacPurkinjeFibre::new();
1968        for _ in 0..500 {
1969            n.step(5.0);
1970        }
1971        n.reset();
1972        assert_eq!(n.v, -85.0);
1973        assert_eq!(n.m, 0.001);
1974    }
1975
1976    #[test]
1977    fn cardiac_performance_1k_steps() {
1978        let start = std::time::Instant::now();
1979        let mut n = CardiacPurkinjeFibre::new();
1980        for _ in 0..1_000 {
1981            std::hint::black_box(n.step(3.0));
1982        }
1983        let elapsed = start.elapsed();
1984        assert!(elapsed.as_millis() < 50, "1k steps must complete in <50ms");
1985    }
1986
1987    // -- Smooth Muscle Cell tests --
1988
1989    #[test]
1990    fn smooth_fires_with_input() {
1991        let mut n = SmoothMuscleCell::new();
1992        let mut spikes = 0;
1993        for _ in 0..10_000 {
1994            spikes += n.step(5.0);
1995        }
1996        assert!(
1997            spikes > 0,
1998            "Smooth muscle must produce slow waves, got {spikes}"
1999        );
2000    }
2001
2002    #[test]
2003    fn smooth_silent_without_input() {
2004        let mut n = SmoothMuscleCell::new();
2005        n.ip3 = 0.0; // No IP3 oscillation driver
2006        let mut spikes = 0;
2007        for _ in 0..5_000 {
2008            spikes += n.step(0.0);
2009        }
2010        assert!(
2011            spikes <= 1,
2012            "Should be essentially silent without drive, got {spikes}"
2013        );
2014    }
2015
2016    #[test]
2017    fn smooth_has_ip3_pathway() {
2018        let n = SmoothMuscleCell::new();
2019        assert!(n.v_ip3r > 0.0, "Must have IP3R release pathway");
2020        assert!(n.ip3 > 0.0, "Must have tonic IP3");
2021    }
2022
2023    #[test]
2024    fn smooth_has_serca() {
2025        let n = SmoothMuscleCell::new();
2026        assert!(n.v_serca > 0.0, "Must have SERCA pump");
2027    }
2028
2029    #[test]
2030    fn smooth_ca_store_exists() {
2031        let n = SmoothMuscleCell::new();
2032        assert!(n.ca_store > 0.0, "Must have ER/SR Ca²⁺ store");
2033    }
2034
2035    #[test]
2036    fn smooth_has_bk() {
2037        let n = SmoothMuscleCell::new();
2038        assert!(n.g_bk > 0.0, "Must have BK (Ca²⁺-activated K) channel");
2039    }
2040
2041    #[test]
2042    fn smooth_no_fast_na() {
2043        // Smooth muscle does NOT have fast Na channels
2044        let n = SmoothMuscleCell::new();
2045        // Verify no g_na field exists by checking only CaL and BK
2046        assert!(n.g_cal > 0.0, "Depolarisation must be Ca²⁺-dependent");
2047    }
2048
2049    #[test]
2050    fn smooth_nan_stays_finite() {
2051        let mut n = SmoothMuscleCell::new();
2052        n.step(f64::NAN);
2053        assert!(n.v.is_finite());
2054        assert!(n.ca.is_finite());
2055    }
2056
2057    #[test]
2058    fn smooth_reset_clears() {
2059        let mut n = SmoothMuscleCell::new();
2060        for _ in 0..1000 {
2061            n.step(3.0);
2062        }
2063        n.reset();
2064        assert_eq!(n.v, -60.0);
2065        assert_eq!(n.ca, 0.1);
2066        assert_eq!(n.ca_store, 100.0);
2067    }
2068
2069    #[test]
2070    fn smooth_performance_1k_steps() {
2071        let start = std::time::Instant::now();
2072        let mut n = SmoothMuscleCell::new();
2073        for _ in 0..1_000 {
2074            std::hint::black_box(n.step(2.0));
2075        }
2076        let elapsed = start.elapsed();
2077        assert!(elapsed.as_millis() < 50, "1k steps must complete in <50ms");
2078    }
2079
2080    // -- Endocrine Beta Cell tests --
2081
2082    #[test]
2083    fn beta_fires_with_glucose() {
2084        let mut n = EndocrineBetaCell::new();
2085        n.atp_level = 0.9; // High glucose → closed IK_ATP → excitable
2086        let mut spikes = 0;
2087        for _ in 0..10_000 {
2088            spikes += n.step(5.0);
2089        }
2090        assert!(
2091            spikes > 0,
2092            "Beta cell must burst with high glucose, got {spikes}"
2093        );
2094    }
2095
2096    #[test]
2097    fn beta_silent_low_glucose() {
2098        let mut n = EndocrineBetaCell::new();
2099        n.atp_level = 0.0; // Low glucose → open IK_ATP → silent
2100        let mut spikes = 0;
2101        for _ in 0..5_000 {
2102            spikes += n.step(0.0);
2103        }
2104        assert_eq!(
2105            spikes, 0,
2106            "Beta cell must be silent at low glucose, got {spikes}"
2107        );
2108    }
2109
2110    #[test]
2111    fn beta_katp_glucose_dependent() {
2112        // Higher glucose (more ATP) → less IK_ATP → more excitable
2113        let mut low = EndocrineBetaCell::new();
2114        low.atp_level = 0.2;
2115        let mut high = EndocrineBetaCell::new();
2116        high.atp_level = 0.9;
2117        let (mut sl, mut sh) = (0, 0);
2118        for _ in 0..5_000 {
2119            sl += low.step(1.0);
2120            sh += high.step(1.0);
2121        }
2122        assert!(
2123            sh >= sl,
2124            "High glucose → more spikes: high={sh} vs low={sl}"
2125        );
2126    }
2127
2128    #[test]
2129    fn beta_has_katp() {
2130        let n = EndocrineBetaCell::new();
2131        assert!(n.g_katp > 0.0, "Must have ATP-sensitive K channel");
2132    }
2133
2134    #[test]
2135    fn beta_has_kca_for_bursting() {
2136        let n = EndocrineBetaCell::new();
2137        assert!(n.g_kca > 0.0, "Must have IK_Ca for burst termination");
2138    }
2139
2140    #[test]
2141    fn beta_ca_rises_with_spiking() {
2142        let mut n = EndocrineBetaCell::new();
2143        n.atp_level = 0.8;
2144        let ca0 = n.ca;
2145        for _ in 0..5_000 {
2146            n.step(2.0);
2147        }
2148        assert!(
2149            n.ca > ca0,
2150            "Ca²⁺ should rise during bursting: ca0={ca0}, ca={}",
2151            n.ca
2152        );
2153    }
2154
2155    #[test]
2156    fn beta_nan_stays_finite() {
2157        let mut n = EndocrineBetaCell::new();
2158        n.step(f64::NAN);
2159        assert!(n.v.is_finite());
2160        assert!(n.ca.is_finite());
2161    }
2162
2163    #[test]
2164    fn beta_reset_clears() {
2165        let mut n = EndocrineBetaCell::new();
2166        for _ in 0..1000 {
2167            n.step(2.0);
2168        }
2169        n.reset();
2170        assert_eq!(n.v, -70.0);
2171        assert_eq!(n.ca, 0.1);
2172    }
2173
2174    #[test]
2175    fn beta_no_fast_na() {
2176        // Beta cells do not have fast Na — depolarisation is CaL-dependent
2177        let n = EndocrineBetaCell::new();
2178        assert!(n.g_cal > 0.0, "CaL must drive depolarisation");
2179    }
2180
2181    #[test]
2182    fn beta_performance_1k_steps() {
2183        let start = std::time::Instant::now();
2184        let mut n = EndocrineBetaCell::new();
2185        for _ in 0..1_000 {
2186            std::hint::black_box(n.step(2.0));
2187        }
2188        let elapsed = start.elapsed();
2189        assert!(elapsed.as_millis() < 50, "1k steps must complete in <50ms");
2190    }
2191}