Skip to main content

sc_neurocore_engine/neurons/
special.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 — Stochastic generators, spike-response models, and neural
8
9//! Stochastic generators, spike-response models, and neural mass/population models.
10
11use rand::{RngExt, SeedableRng};
12use rand_xoshiro::Xoshiro256PlusPlus;
13
14/// Poisson spike generator — P(spike in dt) = λ·dt.
15#[derive(Clone, Debug)]
16pub struct PoissonNeuron {
17    pub rate_hz: f64,
18    pub dt_ms: f64,
19    rng: Xoshiro256PlusPlus,
20}
21
22impl PoissonNeuron {
23    pub fn new(rate_hz: f64, dt_ms: f64, seed: u64) -> Self {
24        Self {
25            rate_hz,
26            dt_ms,
27            rng: Xoshiro256PlusPlus::seed_from_u64(seed),
28        }
29    }
30    pub fn step(&mut self, rate_override: f64) -> i32 {
31        let r = if rate_override < 0.0 {
32            self.rate_hz
33        } else {
34            rate_override
35        };
36        let p = r * self.dt_ms / 1000.0;
37        if self.rng.random::<f64>() < p {
38            1
39        } else {
40            0
41        }
42    }
43    pub fn reset(&mut self) {}
44}
45
46/// Inhomogeneous Poisson — rate passed per step.
47#[derive(Clone, Debug)]
48pub struct InhomogeneousPoissonNeuron {
49    pub dt_ms: f64,
50    rng: Xoshiro256PlusPlus,
51}
52
53impl InhomogeneousPoissonNeuron {
54    pub fn new(dt_ms: f64, seed: u64) -> Self {
55        Self {
56            dt_ms,
57            rng: Xoshiro256PlusPlus::seed_from_u64(seed),
58        }
59    }
60    pub fn step(&mut self, rate_hz: f64) -> i32 {
61        let p = rate_hz.max(0.0) * self.dt_ms / 1000.0;
62        if self.rng.random::<f64>() < p {
63            1
64        } else {
65            0
66        }
67    }
68    pub fn reset(&mut self) {}
69}
70
71/// Gamma-renewal process — order-k Poisson with refractory ISI structure.
72#[derive(Clone, Debug)]
73pub struct GammaRenewalNeuron {
74    pub rate_hz: f64,
75    pub shape_k: u32,
76    pub dt_ms: f64,
77    time_since_spike: f64,
78    rng: Xoshiro256PlusPlus,
79}
80
81impl GammaRenewalNeuron {
82    pub fn new(rate_hz: f64, shape_k: u32, seed: u64) -> Self {
83        Self {
84            rate_hz,
85            shape_k,
86            dt_ms: 1.0,
87            time_since_spike: 0.0,
88            rng: Xoshiro256PlusPlus::seed_from_u64(seed),
89        }
90    }
91    fn log_gamma_int(k: u32) -> f64 {
92        // ln((k-1)!) for integer k
93        (1..k).map(|i| (i as f64).ln()).sum()
94    }
95    pub fn step(&mut self, rate_override: f64) -> i32 {
96        self.time_since_spike += self.dt_ms;
97        let r = if rate_override < 0.0 {
98            self.rate_hz
99        } else {
100            rate_override
101        };
102        let lam = r / 1000.0;
103        let k = self.shape_k;
104        let t = self.time_since_spike;
105        // Gamma hazard approximation: h(t) ≈ (λk)^k * t^(k-1) / Γ(k) * exp(-λk*t) / S(t)
106        // Simplified: use Poisson sub-events approach
107        let mu = lam * (k as f64);
108        let log_pdf = (k as f64 - 1.0) * (mu * t).max(1e-30).ln() + mu.max(1e-30).ln()
109            - mu * t
110            - Self::log_gamma_int(k);
111        let surv = 1.0 - self.incomplete_gamma_ratio(k, mu * t);
112        let hazard = if surv > 1e-15 {
113            log_pdf.exp() / surv
114        } else {
115            mu
116        };
117        let p = hazard * self.dt_ms;
118        if self.rng.random::<f64>() < p {
119            self.time_since_spike = 0.0;
120            1
121        } else {
122            0
123        }
124    }
125    fn incomplete_gamma_ratio(&self, k: u32, x: f64) -> f64 {
126        // Regularized lower incomplete gamma P(k, x) via series for integer k
127        if x <= 0.0 {
128            return 0.0;
129        }
130        let mut sum = 0.0_f64;
131        let mut term = 1.0_f64;
132        for n in 0..k {
133            if n > 0 {
134                term *= x / n as f64;
135            }
136            sum += term;
137        }
138        1.0 - (-x).exp() * sum
139    }
140    pub fn reset(&mut self) {
141        self.time_since_spike = 0.0;
142    }
143}
144
145/// Stochastic IF — Ornstein-Uhlenbeck noise on LIF membrane.
146#[derive(Clone, Debug)]
147pub struct StochasticIFNeuron {
148    pub v: f64,
149    pub v_rest: f64,
150    pub v_reset: f64,
151    pub v_threshold: f64,
152    pub tau_m: f64,
153    pub sigma: f64,
154    pub dt: f64,
155    rng: Xoshiro256PlusPlus,
156}
157
158impl StochasticIFNeuron {
159    pub fn new(seed: u64) -> Self {
160        Self {
161            v: -70.0,
162            v_rest: -70.0,
163            v_reset: -70.0,
164            v_threshold: -50.0,
165            tau_m: 20.0,
166            sigma: 3.0,
167            dt: 1.0,
168            rng: Xoshiro256PlusPlus::seed_from_u64(seed),
169        }
170    }
171    fn randn(&mut self) -> f64 {
172        // Box-Muller
173        let u1: f64 = self.rng.random::<f64>().max(1e-30);
174        let u2: f64 = self.rng.random::<f64>();
175        (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
176    }
177    pub fn step(&mut self, current: f64) -> i32 {
178        let noise = self.sigma * (self.dt / self.tau_m).sqrt() * self.randn();
179        self.v += (-(self.v - self.v_rest) + current) / self.tau_m * self.dt + noise;
180        if self.v >= self.v_threshold {
181            self.v = self.v_reset;
182            1
183        } else {
184            0
185        }
186    }
187    pub fn reset(&mut self) {
188        self.v = self.v_rest;
189    }
190}
191
192/// Galves-Löcherbach 2013 — stochastic point process with memory.
193#[derive(Clone, Debug)]
194pub struct GalvesLocherbachNeuron {
195    pub v: f64,
196    pub v_rest: f64,
197    pub decay: f64,
198    pub threshold_rate: f64,
199    pub steepness: f64,
200    pub dt: f64,
201    rng: Xoshiro256PlusPlus,
202}
203
204impl GalvesLocherbachNeuron {
205    pub fn new(seed: u64) -> Self {
206        Self {
207            v: 0.0,
208            v_rest: 0.0,
209            decay: 0.95,
210            threshold_rate: 0.5,
211            steepness: 5.0,
212            dt: 1.0,
213            rng: Xoshiro256PlusPlus::seed_from_u64(seed),
214        }
215    }
216    pub fn step(&mut self, weighted_input: f64) -> i32 {
217        self.v = self.decay * self.v + weighted_input;
218        let p = 1.0 / (1.0 + (-(self.steepness * (self.v - self.threshold_rate))).exp());
219        if self.rng.random::<f64>() < p * self.dt {
220            self.v = self.v_rest;
221            1
222        } else {
223            0
224        }
225    }
226    pub fn reset(&mut self) {
227        self.v = self.v_rest;
228    }
229}
230
231/// SRM0 — Spike Response Model (kernel-based). Gerstner 1995.
232#[derive(Clone, Debug)]
233pub struct SpikeResponseNeuron {
234    pub v: f64,
235    pub v_threshold: f64,
236    pub tau_eta: f64,
237    pub tau_kappa: f64,
238    pub eta_reset: f64,
239    pub time_since_spike: f64,
240    pub dt: f64,
241}
242
243impl SpikeResponseNeuron {
244    pub fn new() -> Self {
245        Self {
246            v: 0.0,
247            v_threshold: 1.0,
248            tau_eta: 10.0,
249            tau_kappa: 5.0,
250            eta_reset: -5.0,
251            time_since_spike: 1000.0,
252            dt: 1.0,
253        }
254    }
255    pub fn step(&mut self, weighted_input: f64) -> i32 {
256        self.time_since_spike += self.dt;
257        let eta = self.eta_reset * (-self.time_since_spike / self.tau_eta).exp();
258        let kappa = weighted_input * (1.0 - (-self.dt / self.tau_kappa).exp());
259        self.v = eta + kappa;
260        if self.v >= self.v_threshold {
261            self.time_since_spike = 0.0;
262            1
263        } else {
264            0
265        }
266    }
267    pub fn reset(&mut self) {
268        self.v = 0.0;
269        self.time_since_spike = 1000.0;
270    }
271}
272impl Default for SpikeResponseNeuron {
273    fn default() -> Self {
274        Self::new()
275    }
276}
277
278/// GLM neuron — point-process generalized linear model. Pillow et al. 2008.
279#[derive(Clone, Debug)]
280pub struct GLMNeuron {
281    pub mu: f64,
282    pub dt_ms: f64,
283    pub k: Vec<f64>,
284    pub h: Vec<f64>,
285    stim_buf: Vec<f64>,
286    spike_buf: Vec<f64>,
287    rng: Xoshiro256PlusPlus,
288}
289
290impl GLMNeuron {
291    pub fn new(n_k: usize, n_h: usize, seed: u64) -> Self {
292        Self {
293            mu: -3.0,
294            dt_ms: 1.0,
295            k: vec![0.1; n_k],
296            h: vec![-0.5; n_h],
297            stim_buf: vec![0.0; n_k],
298            spike_buf: vec![0.0; n_h],
299            rng: Xoshiro256PlusPlus::seed_from_u64(seed),
300        }
301    }
302    pub fn step(&mut self, stimulus: f64) -> i32 {
303        // Shift buffers
304        let nk = self.stim_buf.len();
305        let nh = self.spike_buf.len();
306        for i in (1..nk).rev() {
307            self.stim_buf[i] = self.stim_buf[i - 1];
308        }
309        if nk > 0 {
310            self.stim_buf[0] = stimulus;
311        }
312        let dot_k: f64 = self
313            .k
314            .iter()
315            .zip(self.stim_buf.iter())
316            .map(|(a, b)| a * b)
317            .sum();
318        let dot_h: f64 = self
319            .h
320            .iter()
321            .zip(self.spike_buf.iter())
322            .map(|(a, b)| a * b)
323            .sum();
324        let lam = (dot_k + dot_h + self.mu).exp();
325        let p = lam * self.dt_ms / 1000.0;
326        let spike = if self.rng.random::<f64>() < p { 1 } else { 0 };
327        for i in (1..nh).rev() {
328            self.spike_buf[i] = self.spike_buf[i - 1];
329        }
330        if nh > 0 {
331            self.spike_buf[0] = spike as f64;
332        }
333        spike
334    }
335    pub fn reset(&mut self) {
336        self.stim_buf.fill(0.0);
337        self.spike_buf.fill(0.0);
338    }
339}
340
341/// Wilson-Cowan 1972 — excitatory/inhibitory population rate model.
342#[derive(Clone, Debug)]
343pub struct WilsonCowanUnit {
344    pub e: f64,
345    pub i: f64,
346    pub w_ee: f64,
347    pub w_ei: f64,
348    pub w_ie: f64,
349    pub w_ii: f64,
350    pub tau_e: f64,
351    pub tau_i: f64,
352    pub a: f64,
353    pub theta: f64,
354    pub dt: f64,
355}
356
357impl WilsonCowanUnit {
358    pub fn new() -> Self {
359        Self {
360            e: 0.1,
361            i: 0.05,
362            w_ee: 10.0,
363            w_ei: 6.0,
364            w_ie: 10.0,
365            w_ii: 1.0,
366            tau_e: 1.0,
367            tau_i: 2.0,
368            a: 1.2,
369            theta: 4.0,
370            dt: 0.1,
371        }
372    }
373    fn logistic(&self, z: f64) -> f64 {
374        if z >= 0.0 {
375            1.0 / (1.0 + (-z).exp())
376        } else {
377            let exp_z = z.exp();
378            exp_z / (1.0 + exp_z)
379        }
380    }
381    fn sigmoid(&self, x: f64) -> f64 {
382        self.logistic(self.a * (x - self.theta)) - self.logistic(-self.a * self.theta)
383    }
384    fn derivatives(&self, e: f64, i: f64, ext_input: f64) -> (f64, f64) {
385        let se = self.sigmoid(self.w_ee * e - self.w_ei * i + ext_input);
386        let si = self.sigmoid(self.w_ie * e - self.w_ii * i);
387        ((-e + se) / self.tau_e, (-i + si) / self.tau_i)
388    }
389    pub fn step(&mut self, ext_input: f64) -> f64 {
390        let e = self.e;
391        let i = self.i;
392        let (k1_e, k1_i) = self.derivatives(e, i, ext_input);
393        let (k2_e, k2_i) = self.derivatives(
394            e + 0.5 * self.dt * k1_e,
395            i + 0.5 * self.dt * k1_i,
396            ext_input,
397        );
398        let (k3_e, k3_i) = self.derivatives(
399            e + 0.5 * self.dt * k2_e,
400            i + 0.5 * self.dt * k2_i,
401            ext_input,
402        );
403        let (k4_e, k4_i) = self.derivatives(e + self.dt * k3_e, i + self.dt * k3_i, ext_input);
404        self.e = e + self.dt * (k1_e + 2.0 * k2_e + 2.0 * k3_e + k4_e) / 6.0;
405        self.i = i + self.dt * (k1_i + 2.0 * k2_i + 2.0 * k3_i + k4_i) / 6.0;
406        self.e
407    }
408    pub fn reset(&mut self) {
409        self.e = 0.1;
410        self.i = 0.05;
411    }
412}
413impl Default for WilsonCowanUnit {
414    fn default() -> Self {
415        Self::new()
416    }
417}
418
419/// Jansen-Rit 1995 — neural mass model for EEG generation.
420#[derive(Clone, Debug)]
421pub struct JansenRitUnit {
422    pub y: [f64; 6],
423    pub a_exc: f64,
424    pub b_exc: f64,
425    pub a_rate: f64,
426    pub b_rate: f64,
427    pub c: f64,
428    pub e0: f64,
429    pub v0: f64,
430    pub r: f64,
431    pub dt: f64,
432}
433
434impl JansenRitUnit {
435    pub fn new() -> Self {
436        Self {
437            y: [0.0; 6],
438            a_exc: 3.25,
439            b_exc: 22.0,
440            a_rate: 100.0,
441            b_rate: 50.0,
442            c: 135.0,
443            e0: 2.5,
444            v0: 6.0,
445            r: 0.56,
446            dt: 0.001,
447        }
448    }
449    fn sigmoid(&self, x: f64) -> f64 {
450        2.0 * self.e0 / (1.0 + (self.r * (self.v0 - x)).exp())
451    }
452    pub fn step(&mut self, p_ext: f64) -> f64 {
453        let s1 = self.sigmoid(self.y[1] - self.y[2]);
454        let s0 = self.sigmoid(self.c * 0.8 * self.y[0]);
455        let s2 = self.sigmoid(self.c * 0.25 * self.y[0]);
456        let dy0 = self.y[3];
457        let dy3 = self.a_exc * self.a_rate * s1
458            - 2.0 * self.a_rate * self.y[3]
459            - self.a_rate.powi(2) * self.y[0];
460        let dy1 = self.y[4];
461        let dy4 = self.a_exc * self.a_rate * (p_ext + self.c * 0.8 * s0)
462            - 2.0 * self.a_rate * self.y[4]
463            - self.a_rate.powi(2) * self.y[1];
464        let dy2 = self.y[5];
465        let dy5 = self.b_exc * self.b_rate * self.c * 0.25 * s2
466            - 2.0 * self.b_rate * self.y[5]
467            - self.b_rate.powi(2) * self.y[2];
468        self.y[0] += dy0 * self.dt;
469        self.y[3] += dy3 * self.dt;
470        self.y[1] += dy1 * self.dt;
471        self.y[4] += dy4 * self.dt;
472        self.y[2] += dy2 * self.dt;
473        self.y[5] += dy5 * self.dt;
474        self.y[1] - self.y[2]
475    }
476    pub fn reset(&mut self) {
477        self.y = [0.0; 6];
478    }
479}
480impl Default for JansenRitUnit {
481    fn default() -> Self {
482        Self::new()
483    }
484}
485
486/// Wong-Wang 2006 — attractor decision-making model.
487#[derive(Clone, Debug)]
488pub struct WongWangUnit {
489    pub s1: f64,
490    pub s2: f64,
491    pub tau_s: f64,
492    pub gamma: f64,
493    pub j_n: f64,
494    pub j_cross: f64,
495    pub i_0: f64,
496    pub sigma: f64,
497    pub dt: f64,
498    rng: Xoshiro256PlusPlus,
499}
500
501impl WongWangUnit {
502    pub fn new(seed: u64) -> Self {
503        Self {
504            s1: 0.1,
505            s2: 0.1,
506            tau_s: 0.1,
507            gamma: 0.641,
508            j_n: 0.2609,
509            j_cross: 0.0497,
510            i_0: 0.3255,
511            sigma: 0.02,
512            dt: 0.001,
513            rng: Xoshiro256PlusPlus::seed_from_u64(seed),
514        }
515    }
516    fn phi(&self, i_total: f64) -> f64 {
517        let a = 270.0;
518        let b = 108.0;
519        let d = 0.154;
520        let x = a * i_total - b;
521        let denom = 1.0 - (-d * x).exp();
522        if denom.abs() < 1e-10 {
523            1.0 / d
524        } else {
525            x / denom
526        }
527    }
528    fn derivatives(
529        &self,
530        s1: f64,
531        s2: f64,
532        stim1: f64,
533        stim2: f64,
534        noise1: f64,
535        noise2: f64,
536    ) -> (f64, f64, f64, f64) {
537        let i1 = self.j_n * s1 - self.j_cross * s2 + self.i_0 + stim1 + noise1;
538        let i2 = self.j_n * s2 - self.j_cross * s1 + self.i_0 + stim2 + noise2;
539        let r1 = self.phi(i1);
540        let r2 = self.phi(i2);
541        (
542            -s1 / self.tau_s + self.gamma * (1.0 - s1) * r1,
543            -s2 / self.tau_s + self.gamma * (1.0 - s2) * r2,
544            r1,
545            r2,
546        )
547    }
548    fn randn(&mut self) -> f64 {
549        let u1 = self.rng.random::<f64>().max(1e-30);
550        let u2 = self.rng.random::<f64>();
551        (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
552    }
553    pub fn step(&mut self, stim1: f64, stim2: f64) -> (f64, f64) {
554        let noise1 = self.sigma * self.randn();
555        let noise2 = self.sigma * self.randn();
556        let (k1_s1, k1_s2, r1, r2) =
557            self.derivatives(self.s1, self.s2, stim1, stim2, noise1, noise2);
558        let (k2_s1, k2_s2, _, _) = self.derivatives(
559            self.s1 + 0.5 * self.dt * k1_s1,
560            self.s2 + 0.5 * self.dt * k1_s2,
561            stim1,
562            stim2,
563            noise1,
564            noise2,
565        );
566        let (k3_s1, k3_s2, _, _) = self.derivatives(
567            self.s1 + 0.5 * self.dt * k2_s1,
568            self.s2 + 0.5 * self.dt * k2_s2,
569            stim1,
570            stim2,
571            noise1,
572            noise2,
573        );
574        let (k4_s1, k4_s2, _, _) = self.derivatives(
575            self.s1 + self.dt * k3_s1,
576            self.s2 + self.dt * k3_s2,
577            stim1,
578            stim2,
579            noise1,
580            noise2,
581        );
582        self.s1 =
583            (self.s1 + self.dt * (k1_s1 + 2.0 * k2_s1 + 2.0 * k3_s1 + k4_s1) / 6.0).clamp(0.0, 1.0);
584        self.s2 =
585            (self.s2 + self.dt * (k1_s2 + 2.0 * k2_s2 + 2.0 * k3_s2 + k4_s2) / 6.0).clamp(0.0, 1.0);
586        (r1, r2)
587    }
588    pub fn reset(&mut self) {
589        self.s1 = 0.1;
590        self.s2 = 0.1;
591    }
592}
593
594/// Ermentrout-Kopell / Montbrió theta-neuron mean field. Montbrió et al. 2015.
595#[derive(Clone, Debug)]
596pub struct ErmentroutKopellPopulation {
597    pub r: f64,
598    pub v: f64,
599    pub tau: f64,
600    pub delta: f64,
601    pub eta_bar: f64,
602    pub j: f64,
603    pub dt: f64,
604}
605
606impl ErmentroutKopellPopulation {
607    pub fn new() -> Self {
608        Self {
609            r: 0.1,
610            v: -2.0,
611            tau: 1.0,
612            delta: 1.0,
613            eta_bar: -5.0,
614            j: 15.0,
615            dt: 0.01,
616        }
617    }
618    pub fn step(&mut self, ext_input: f64) -> f64 {
619        let dr =
620            (self.delta / (std::f64::consts::PI * self.tau) + 2.0 * self.r * self.v) / self.tau;
621        let dv = (self.v * self.v + self.eta_bar + ext_input + self.j * self.tau * self.r
622            - std::f64::consts::PI.powi(2) * self.tau.powi(2) * self.r.powi(2))
623            / self.tau;
624        self.r += dr * self.dt;
625        self.v += dv * self.dt;
626        self.r = self.r.max(0.0);
627        self.r
628    }
629    pub fn reset(&mut self) {
630        self.r = 0.1;
631        self.v = -2.0;
632    }
633}
634impl Default for ErmentroutKopellPopulation {
635    fn default() -> Self {
636        Self::new()
637    }
638}
639
640/// Wendling et al. 2002 — extended Jansen-Rit with slow GABA_B.
641#[derive(Clone, Debug)]
642pub struct WendlingNeuron {
643    pub y: [f64; 10],
644    pub a_exc: f64,
645    pub b_fast: f64,
646    pub g_slow: f64,
647    pub a_rate: f64,
648    pub b_rate: f64,
649    pub g_rate: f64,
650    pub c: f64,
651    pub e0: f64,
652    pub v0: f64,
653    pub r: f64,
654    pub dt: f64,
655}
656
657impl WendlingNeuron {
658    pub fn new() -> Self {
659        Self {
660            y: [0.0; 10],
661            a_exc: 3.25,
662            b_fast: 22.0,
663            g_slow: 10.0,
664            a_rate: 100.0,
665            b_rate: 500.0,
666            g_rate: 20.0,
667            c: 135.0,
668            e0: 2.5,
669            v0: 6.0,
670            r: 0.56,
671            dt: 0.001,
672        }
673    }
674    fn sigmoid(&self, x: f64) -> f64 {
675        2.0 * self.e0 / (1.0 + (self.r * (self.v0 - x)).exp())
676    }
677    pub fn step(&mut self, p_ext: f64) -> f64 {
678        let sig_main = self.sigmoid(self.y[1] - self.y[2] - self.y[3]);
679        let sig_0 = self.sigmoid(self.c * 0.8 * self.y[0]);
680        let sig_fast = self.sigmoid(self.c * 0.25 * self.y[0]);
681        let sig_slow = self.sigmoid(self.c * 0.1 * self.y[0]);
682        let a = self.a_rate;
683        let b = self.b_rate;
684        let g = self.g_rate;
685        let dy0 = self.y[5];
686        let dy5 = self.a_exc * a * sig_main - 2.0 * a * self.y[5] - a * a * self.y[0];
687        let dy1 = self.y[6];
688        let dy6 = self.a_exc * a * (p_ext + self.c * 0.8 * sig_0)
689            - 2.0 * a * self.y[6]
690            - a * a * self.y[1];
691        let dy2 = self.y[7];
692        let dy7 =
693            self.b_fast * b * self.c * 0.25 * sig_fast - 2.0 * b * self.y[7] - b * b * self.y[2];
694        let dy3 = self.y[8];
695        let dy8 =
696            self.g_slow * g * self.c * 0.1 * sig_slow - 2.0 * g * self.y[8] - g * g * self.y[3];
697        self.y[0] += dy0 * self.dt;
698        self.y[5] += dy5 * self.dt;
699        self.y[1] += dy1 * self.dt;
700        self.y[6] += dy6 * self.dt;
701        self.y[2] += dy2 * self.dt;
702        self.y[7] += dy7 * self.dt;
703        self.y[3] += dy3 * self.dt;
704        self.y[8] += dy8 * self.dt;
705        self.y[1] - self.y[2] - self.y[3]
706    }
707    pub fn reset(&mut self) {
708        self.y = [0.0; 10];
709    }
710}
711impl Default for WendlingNeuron {
712    fn default() -> Self {
713        Self::new()
714    }
715}
716
717/// Larter-Breakspear 2003 — neural mass with ion channels for whole-brain modelling.
718#[derive(Clone, Debug)]
719pub struct LarterBreakspearNeuron {
720    pub v: f64,
721    pub w: f64,
722    pub z: f64,
723    pub g_ca: f64,
724    pub g_na: f64,
725    pub g_k: f64,
726    pub g_l: f64,
727    pub v_ca: f64,
728    pub v_na: f64,
729    pub v_k: f64,
730    pub v_l: f64,
731    pub phi: f64,
732    pub tau_k: f64,
733    pub b: f64,
734    pub a_ee: f64,
735    pub i_ext: f64,
736    pub dt: f64,
737}
738
739impl LarterBreakspearNeuron {
740    pub fn new() -> Self {
741        Self {
742            v: -0.5,
743            w: 0.0,
744            z: 0.0,
745            g_ca: 1.1,
746            g_na: 6.7,
747            g_k: 2.0,
748            g_l: 0.5,
749            v_ca: 1.0,
750            v_na: 0.53,
751            v_k: -0.7,
752            v_l: -0.5,
753            phi: 0.7,
754            tau_k: 1.0,
755            b: 0.1,
756            a_ee: 0.36,
757            i_ext: 0.3,
758            dt: 0.01,
759        }
760    }
761    pub fn step(&mut self, coupling: f64) -> f64 {
762        let m_ca = 0.5 * (1.0 + ((self.v + 0.01) / 0.15).tanh());
763        let m_na = 0.5 * (1.0 + ((self.v - 0.12) / 0.15).tanh());
764        let m_k = 0.5 * (1.0 + (self.v / 0.3).tanh());
765        let i_ca = self.g_ca * m_ca * (self.v - self.v_ca);
766        let i_na = self.g_na * m_na * (self.v - self.v_na);
767        let i_k = self.g_k * self.w * (self.v - self.v_k);
768        let i_l = self.g_l * (self.v - self.v_l);
769        let dv = -i_ca - i_na - i_k - i_l + self.i_ext + coupling + self.a_ee * self.v;
770        let dw = self.phi * (m_k - self.w) / self.tau_k;
771        let dz = self.b * (self.v + 0.5 - self.z);
772        self.v += dv * self.dt;
773        self.w += dw * self.dt;
774        self.z += dz * self.dt;
775        self.v
776    }
777    pub fn reset(&mut self) {
778        self.v = -0.5;
779        self.w = 0.0;
780        self.z = 0.0;
781    }
782}
783impl Default for LarterBreakspearNeuron {
784    fn default() -> Self {
785        Self::new()
786    }
787}
788
789#[cfg(test)]
790mod tests {
791    use super::*;
792
793    #[test]
794    fn poisson_fires() {
795        let mut n = PoissonNeuron::new(200.0, 1.0, 42);
796        let t: i32 = (0..1000).map(|_| n.step(-1.0)).sum();
797        assert!(t > 10);
798    }
799    #[test]
800    fn inhom_poisson_fires() {
801        let mut n = InhomogeneousPoissonNeuron::new(1.0, 42);
802        let t: i32 = (0..1000).map(|_| n.step(200.0)).sum();
803        assert!(t > 10);
804    }
805    #[test]
806    fn gamma_renewal_fires() {
807        let mut n = GammaRenewalNeuron::new(100.0, 3, 42);
808        let t: i32 = (0..2000).map(|_| n.step(-1.0)).sum();
809        assert!(t > 0);
810    }
811    #[test]
812    fn stochastic_if_fires() {
813        let mut n = StochasticIFNeuron::new(42);
814        let t: i32 = (0..500).map(|_| n.step(30.0)).sum();
815        assert!(t > 0);
816    }
817    #[test]
818    fn gl_fires() {
819        let mut n = GalvesLocherbachNeuron::new(42);
820        let t: i32 = (0..200).map(|_| n.step(2.0)).sum();
821        assert!(t > 0);
822    }
823    #[test]
824    fn srm_fires() {
825        let mut n = SpikeResponseNeuron::new();
826        let t: i32 = (0..200).map(|_| n.step(10.0)).sum();
827        assert!(t > 0);
828    }
829    #[test]
830    fn glm_fires() {
831        let mut n = GLMNeuron::new(5, 10, 42);
832        let t: i32 = (0..5000).map(|_| n.step(20.0)).sum();
833        assert!(t > 0);
834    }
835    #[test]
836    fn wc_oscillates() {
837        let mut n = WilsonCowanUnit::new();
838        let mut last = n.e;
839        let mut changes = 0;
840        for _ in 0..500 {
841            let e = n.step(5.0);
842            if (e - last).abs() > 0.001 {
843                changes += 1;
844            }
845            last = e;
846        }
847        assert!(changes > 0);
848    }
849    #[test]
850    fn jr_produces_eeg() {
851        let mut n = JansenRitUnit::new();
852        let mut nz = 0;
853        for _ in 0..5000 {
854            let v = n.step(220.0);
855            if v.abs() > 0.001 {
856                nz += 1;
857            }
858        }
859        assert!(nz > 0);
860    }
861    #[test]
862    fn ww_diverges() {
863        let mut n = WongWangUnit::new(42);
864        for _ in 0..5000 {
865            n.step(0.02, 0.0);
866        }
867        assert!((n.s1 - n.s2).abs() > 0.001);
868    }
869    #[test]
870    fn ek_pop_fires() {
871        let mut n = ErmentroutKopellPopulation::new();
872        for _ in 0..1000 {
873            n.step(0.0);
874        }
875        assert!(n.r > 0.0);
876    }
877    #[test]
878    fn wendling_produces() {
879        let mut n = WendlingNeuron::new();
880        let mut nz = 0;
881        for _ in 0..5000 {
882            let v = n.step(220.0);
883            if v.abs() > 0.001 {
884                nz += 1;
885            }
886        }
887        assert!(nz > 0);
888    }
889    #[test]
890    fn lb_evolves() {
891        let mut n = LarterBreakspearNeuron::new();
892        let v0 = n.v;
893        for _ in 0..1000 {
894            n.step(0.0);
895        }
896        assert!((n.v - v0).abs() > 0.001);
897    }
898
899    // ── Multi-angle tests for special models ──
900
901    // -- Poisson --
902    #[test]
903    fn poisson_reset_no_panic() {
904        let mut n = PoissonNeuron::new(200.0, 1.0, 42);
905        for _ in 0..100 {
906            n.step(-1.0);
907        }
908        n.reset();
909    }
910    #[test]
911    fn poisson_nan_no_panic() {
912        let mut n = PoissonNeuron::new(200.0, 1.0, 42);
913        n.step(f64::NAN);
914    }
915    #[test]
916    fn poisson_seed_varies() {
917        let mut n1 = PoissonNeuron::new(200.0, 1.0, 1);
918        let mut n2 = PoissonNeuron::new(200.0, 1.0, 999);
919        let t1: i32 = (0..1000).map(|_| n1.step(-1.0)).sum();
920        let t2: i32 = (0..1000).map(|_| n2.step(-1.0)).sum();
921        assert!(t1 > 0 && t2 > 0);
922    }
923
924    // -- InhomogeneousPoisson --
925    #[test]
926    fn inhom_poisson_zero_rate() {
927        let mut n = InhomogeneousPoissonNeuron::new(1.0, 42);
928        let t: i32 = (0..1000).map(|_| n.step(0.0)).sum();
929        assert_eq!(t, 0);
930    }
931    #[test]
932    fn inhom_poisson_nan_no_panic() {
933        let mut n = InhomogeneousPoissonNeuron::new(1.0, 42);
934        n.step(f64::NAN);
935    }
936
937    // -- GammaRenewal --
938    #[test]
939    fn gamma_renewal_reset_clears() {
940        let mut n = GammaRenewalNeuron::new(100.0, 3, 42);
941        for _ in 0..100 {
942            n.step(-1.0);
943        }
944        n.reset();
945        assert!((n.time_since_spike - 0.0).abs() < 1e-10);
946    }
947    #[test]
948    fn gamma_renewal_nan_no_panic() {
949        let mut n = GammaRenewalNeuron::new(100.0, 3, 42);
950        n.step(f64::NAN);
951    }
952
953    // -- StochasticIF --
954    #[test]
955    fn stochastic_if_reset_clears() {
956        let mut n = StochasticIFNeuron::new(42);
957        for _ in 0..100 {
958            n.step(30.0);
959        }
960        n.reset();
961        assert!((n.v - n.v_rest).abs() < 1e-10);
962    }
963    #[test]
964    fn stochastic_if_bounded() {
965        let mut n = StochasticIFNeuron::new(42);
966        for _ in 0..1000 {
967            n.step(1e4);
968        }
969        assert!(n.v.is_finite());
970    }
971    #[test]
972    fn stochastic_if_nan_no_panic() {
973        StochasticIFNeuron::new(42).step(f64::NAN);
974    }
975    #[test]
976    fn stochastic_if_negative_no_crash() {
977        let mut n = StochasticIFNeuron::new(42);
978        for _ in 0..500 {
979            n.step(-10.0);
980        }
981        assert!(n.v.is_finite());
982    }
983
984    // -- GalvesLocherbach --
985    #[test]
986    fn gl_reset_clears() {
987        let mut n = GalvesLocherbachNeuron::new(42);
988        for _ in 0..100 {
989            n.step(2.0);
990        }
991        n.reset();
992        assert!((n.v - n.v_rest).abs() < 1e-10);
993    }
994    #[test]
995    fn gl_bounded() {
996        let mut n = GalvesLocherbachNeuron::new(42);
997        for _ in 0..1000 {
998            n.step(1e4);
999        }
1000        assert!(n.v.is_finite());
1001    }
1002    #[test]
1003    fn gl_nan_no_panic() {
1004        GalvesLocherbachNeuron::new(42).step(f64::NAN);
1005    }
1006
1007    // -- SpikeResponse --
1008    #[test]
1009    fn srm_reset_clears() {
1010        let mut n = SpikeResponseNeuron::new();
1011        for _ in 0..100 {
1012            n.step(10.0);
1013        }
1014        n.reset();
1015        assert!((n.v - 0.0).abs() < 1e-10);
1016    }
1017    #[test]
1018    fn srm_bounded() {
1019        let mut n = SpikeResponseNeuron::new();
1020        for _ in 0..1000 {
1021            n.step(1e4);
1022        }
1023        assert!(n.v.is_finite());
1024    }
1025    #[test]
1026    fn srm_nan_no_panic() {
1027        SpikeResponseNeuron::new().step(f64::NAN);
1028    }
1029    #[test]
1030    fn srm_silent_without_input() {
1031        let mut n = SpikeResponseNeuron::new();
1032        let t: i32 = (0..200).map(|_| n.step(0.0)).sum();
1033        assert_eq!(t, 0);
1034    }
1035
1036    // -- GLM --
1037    #[test]
1038    fn glm_reset_clears() {
1039        let mut n = GLMNeuron::new(5, 10, 42);
1040        for _ in 0..100 {
1041            n.step(20.0);
1042        }
1043        n.reset();
1044    }
1045    #[test]
1046    fn glm_nan_no_panic() {
1047        GLMNeuron::new(5, 10, 42).step(f64::NAN);
1048    }
1049
1050    // -- WilsonCowan --
1051    #[test]
1052    fn wc_reset_clears() {
1053        let mut n = WilsonCowanUnit::new();
1054        for _ in 0..200 {
1055            n.step(5.0);
1056        }
1057        n.reset();
1058        assert!((n.e - 0.1).abs() < 1e-10);
1059        assert!((n.i - 0.05).abs() < 1e-10);
1060    }
1061    #[test]
1062    fn wc_bounded() {
1063        let mut n = WilsonCowanUnit::new();
1064        for _ in 0..5000 {
1065            n.step(1e3);
1066        }
1067        assert!(n.e.is_finite());
1068        assert!(n.i.is_finite());
1069    }
1070    #[test]
1071    fn wc_nan_no_panic() {
1072        WilsonCowanUnit::new().step(f64::NAN);
1073    }
1074
1075    // -- JansenRit --
1076    #[test]
1077    fn jr_reset_clears() {
1078        let mut n = JansenRitUnit::new();
1079        for _ in 0..1000 {
1080            n.step(220.0);
1081        }
1082        n.reset();
1083        assert!(n.y.iter().all(|&x| x == 0.0));
1084    }
1085    #[test]
1086    fn jr_bounded() {
1087        let mut n = JansenRitUnit::new();
1088        for _ in 0..5000 {
1089            n.step(1e3);
1090        }
1091        assert!(n.y.iter().all(|x| x.is_finite()));
1092    }
1093    #[test]
1094    fn jr_nan_no_panic() {
1095        JansenRitUnit::new().step(f64::NAN);
1096    }
1097
1098    // -- WongWang --
1099    #[test]
1100    fn ww_reset_clears() {
1101        let mut n = WongWangUnit::new(42);
1102        for _ in 0..1000 {
1103            n.step(0.02, 0.0);
1104        }
1105        n.reset();
1106        assert!((n.s1 - 0.1).abs() < 1e-10);
1107        assert!((n.s2 - 0.1).abs() < 1e-10);
1108    }
1109    #[test]
1110    fn ww_bounded() {
1111        let mut n = WongWangUnit::new(42);
1112        for _ in 0..5000 {
1113            n.step(1.0, 0.0);
1114        }
1115        assert!(n.s1.is_finite());
1116        assert!(n.s2.is_finite());
1117    }
1118    #[test]
1119    fn ww_nan_no_panic() {
1120        WongWangUnit::new(42).step(f64::NAN, 0.0);
1121    }
1122
1123    // -- ErmentroutKopellPopulation --
1124    #[test]
1125    fn ek_pop_reset_clears() {
1126        let mut n = ErmentroutKopellPopulation::new();
1127        for _ in 0..500 {
1128            n.step(0.0);
1129        }
1130        n.reset();
1131        assert!((n.r - 0.1).abs() < 1e-10);
1132        assert!((n.v - (-2.0)).abs() < 1e-10);
1133    }
1134    #[test]
1135    fn ek_pop_moderate_stable() {
1136        let mut n = ErmentroutKopellPopulation::new();
1137        for _ in 0..5000 {
1138            n.step(1.0);
1139        }
1140        assert!(n.r.is_finite());
1141        assert!(n.v.is_finite());
1142    }
1143    #[test]
1144    fn ek_pop_nan_no_panic() {
1145        ErmentroutKopellPopulation::new().step(f64::NAN);
1146    }
1147
1148    // -- Wendling --
1149    #[test]
1150    fn wendling_reset_clears() {
1151        let mut n = WendlingNeuron::new();
1152        for _ in 0..1000 {
1153            n.step(220.0);
1154        }
1155        n.reset();
1156        assert!(n.y.iter().all(|&x| x == 0.0));
1157    }
1158    #[test]
1159    fn wendling_bounded() {
1160        let mut n = WendlingNeuron::new();
1161        for _ in 0..5000 {
1162            n.step(1e3);
1163        }
1164        assert!(n.y.iter().all(|x| x.is_finite()));
1165    }
1166    #[test]
1167    fn wendling_nan_no_panic() {
1168        WendlingNeuron::new().step(f64::NAN);
1169    }
1170
1171    // -- LarterBreakspear --
1172    #[test]
1173    fn lb_reset_clears() {
1174        let mut n = LarterBreakspearNeuron::new();
1175        for _ in 0..500 {
1176            n.step(0.0);
1177        }
1178        n.reset();
1179        assert!((n.v - (-0.5)).abs() < 1e-10);
1180        assert!((n.w - 0.0).abs() < 1e-10);
1181        assert!((n.z - 0.0).abs() < 1e-10);
1182    }
1183    #[test]
1184    fn lb_bounded() {
1185        let mut n = LarterBreakspearNeuron::new();
1186        for _ in 0..5000 {
1187            n.step(10.0);
1188        }
1189        assert!(n.v.is_finite());
1190    }
1191    #[test]
1192    fn lb_nan_no_panic() {
1193        LarterBreakspearNeuron::new().step(f64::NAN);
1194    }
1195}