Skip to main content

sc_neurocore_engine/neurons/simple_spiking/
pernarowski.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 — Pernarowski Neuron Model
8
9//! Pernarowski beta-cell bursting dynamics.
10
11/// Pernarowski 1994 three-state pancreatic beta-cell burster.
12#[derive(Clone, Debug)]
13pub struct PernarowskiNeuron {
14    pub v: f64,
15    pub w: f64,
16    pub z: f64,
17    pub alpha: f64,
18    pub beta: f64,
19    pub eps1: f64,
20    pub eps2: f64,
21    pub gamma: f64,
22    pub dt: f64,
23    pub v_threshold: f64,
24}
25
26impl PernarowskiNeuron {
27    pub fn new() -> Self {
28        Self {
29            v: -1.0,
30            w: 0.0,
31            z: 0.0,
32            alpha: 0.1,
33            beta: 0.5,
34            eps1: 0.1,
35            eps2: 0.001,
36            gamma: 0.5,
37            dt: 0.1,
38            v_threshold: 0.5,
39        }
40    }
41    fn is_valid(&self) -> bool {
42        self.v.is_finite()
43            && self.w.is_finite()
44            && self.z.is_finite()
45            && self.alpha.is_finite()
46            && self.beta.is_finite()
47            && self.eps1.is_finite()
48            && self.eps1 > 0.0
49            && self.eps2.is_finite()
50            && self.eps2 > 0.0
51            && self.gamma.is_finite()
52            && self.gamma > 0.0
53            && self.dt.is_finite()
54            && self.dt > 0.0
55            && self.v_threshold.is_finite()
56    }
57    fn derivatives(&self, v: f64, w: f64, z: f64, current: f64) -> Option<(f64, f64, f64)> {
58        if !(v.is_finite() && w.is_finite() && z.is_finite() && current.is_finite()) {
59            return None;
60        }
61        let dv = v - v.powi(3) / 3.0 - w - z + current;
62        let dw = self.eps1 * (v - self.gamma * w + self.alpha);
63        let dz = self.eps2 * (self.beta * (v + 0.7) - z);
64        if dv.is_finite() && dw.is_finite() && dz.is_finite() {
65            Some((dv, dw, dz))
66        } else {
67            None
68        }
69    }
70    fn rk4_candidate(&self, current: f64) -> Option<(f64, f64, f64)> {
71        let dt = self.dt;
72        let (k1v, k1w, k1z) = self.derivatives(self.v, self.w, self.z, current)?;
73        let (k2v, k2w, k2z) = self.derivatives(
74            self.v + 0.5 * dt * k1v,
75            self.w + 0.5 * dt * k1w,
76            self.z + 0.5 * dt * k1z,
77            current,
78        )?;
79        let (k3v, k3w, k3z) = self.derivatives(
80            self.v + 0.5 * dt * k2v,
81            self.w + 0.5 * dt * k2w,
82            self.z + 0.5 * dt * k2z,
83            current,
84        )?;
85        let (k4v, k4w, k4z) = self.derivatives(
86            self.v + dt * k3v,
87            self.w + dt * k3w,
88            self.z + dt * k3z,
89            current,
90        )?;
91        let v = self.v + dt * (k1v + 2.0 * k2v + 2.0 * k3v + k4v) / 6.0;
92        let w = self.w + dt * (k1w + 2.0 * k2w + 2.0 * k3w + k4w) / 6.0;
93        let z = self.z + dt * (k1z + 2.0 * k2z + 2.0 * k3z + k4z) / 6.0;
94        if v.is_finite() && w.is_finite() && z.is_finite() {
95            Some((v, w, z))
96        } else {
97            None
98        }
99    }
100    fn try_step(&mut self, current: f64) -> Option<i32> {
101        if !self.is_valid() || !current.is_finite() {
102            return None;
103        }
104        let v_prev = self.v;
105        let (v, w, z) = self.rk4_candidate(current)?;
106        self.v = v;
107        self.w = w;
108        self.z = z;
109        Some(if self.v >= self.v_threshold && v_prev < self.v_threshold {
110            1
111        } else {
112            0
113        })
114    }
115    pub fn step(&mut self, current: f64) -> i32 {
116        self.try_step(current).unwrap_or(0)
117    }
118    /// Run `n_steps` RK4 updates under a constant input, returning the `v` trace
119    /// and the upward-`v_threshold`-crossing spike count. Uses the same RK4
120    /// candidate path as `step`, so the
121    /// trace is bit-identical to the per-step path and to the Python reference
122    /// (the cubic uses `v.powi(3)` = `v*v*v`, matching the Python `v*v*v`; no
123    /// transcendental functions). The final state is left in `self.v` / `self.w`
124    /// / `self.z`.
125    pub fn simulate(&mut self, n_steps: usize, current: f64) -> (Vec<f64>, i64) {
126        self.try_simulate(n_steps, current).unwrap_or_default()
127    }
128    /// Run one failure-atomic batch, returning `None` on any invalid stage.
129    pub fn try_simulate(&mut self, n_steps: usize, current: f64) -> Option<(Vec<f64>, i64)> {
130        let mut candidate = self.clone();
131        let mut trace = Vec::with_capacity(n_steps);
132        let mut spikes: i64 = 0;
133        for _ in 0..n_steps {
134            let spiked = candidate.try_step(current)?;
135            trace.push(candidate.v);
136            spikes += spiked as i64;
137        }
138        *self = candidate;
139        Some((trace, spikes))
140    }
141    pub fn reset(&mut self) {
142        self.v = -1.0;
143        self.w = 0.0;
144        self.z = 0.0;
145    }
146}
147impl Default for PernarowskiNeuron {
148    fn default() -> Self {
149        Self::new()
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn default_matches_constructor_state() {
159        let default = PernarowskiNeuron::default();
160        let constructed = PernarowskiNeuron::new();
161        assert_eq!(default.v, constructed.v);
162    }
163
164    #[test]
165    fn simulate_matches_repeated_step() {
166        let mut simulated = PernarowskiNeuron::new();
167        let mut repeated = PernarowskiNeuron::new();
168        let (trace, spikes) = simulated.simulate(2_000, 1.0);
169        let mut expected_trace = Vec::with_capacity(2_000);
170        let mut expected_spikes = 0_i64;
171        for _ in 0..2_000 {
172            if repeated.step(1.0) == 1 {
173                expected_spikes += 1;
174            }
175            expected_trace.push(repeated.v);
176        }
177        assert_eq!(trace, expected_trace);
178        assert_eq!(spikes, expected_spikes);
179    }
180
181    #[test]
182    fn pernarowski_fires() {
183        let mut n = PernarowskiNeuron::new();
184        let t: i32 = (0..2000).map(|_| n.step(1.0)).sum();
185        assert!(t > 0);
186    }
187
188    #[test]
189    fn pernarowski_reset_clears_state() {
190        let mut n = PernarowskiNeuron::new();
191        for _ in 0..500 {
192            n.step(1.0);
193        }
194        n.reset();
195        assert!((n.v - (-1.0)).abs() < 1e-10);
196    }
197
198    #[test]
199    fn pernarowski_bounded() {
200        let mut n = PernarowskiNeuron::new();
201        for _ in 0..2000 {
202            n.step(50.0);
203        }
204        assert!(n.v.is_finite());
205    }
206
207    #[test]
208    fn pernarowski_slow_z() {
209        let mut n = PernarowskiNeuron::new();
210        let z0 = n.z;
211        for _ in 0..2000 {
212            n.step(1.0);
213        }
214        assert!((n.z - z0).abs() > 1e-6, "slow z should evolve");
215    }
216
217    #[test]
218    fn pernarowski_matches_rk4_candidate() {
219        let mut n = PernarowskiNeuron::new();
220        n.v = -0.8;
221        n.w = 0.2;
222        n.z = -0.1;
223        let current = 0.5;
224        let dt = n.dt;
225        let rhs = |v: f64, w: f64, z: f64| {
226            (
227                v - v.powi(3) / 3.0 - w - z + current,
228                n.eps1 * (v - n.gamma * w + n.alpha),
229                n.eps2 * (n.beta * (v + 0.7) - z),
230            )
231        };
232        let (k1v, k1w, k1z) = rhs(n.v, n.w, n.z);
233        let (k2v, k2w, k2z) = rhs(
234            n.v + 0.5 * dt * k1v,
235            n.w + 0.5 * dt * k1w,
236            n.z + 0.5 * dt * k1z,
237        );
238        let (k3v, k3w, k3z) = rhs(
239            n.v + 0.5 * dt * k2v,
240            n.w + 0.5 * dt * k2w,
241            n.z + 0.5 * dt * k2z,
242        );
243        let (k4v, k4w, k4z) = rhs(n.v + dt * k3v, n.w + dt * k3w, n.z + dt * k3z);
244        let expected = (
245            n.v + dt * (k1v + 2.0 * k2v + 2.0 * k3v + k4v) / 6.0,
246            n.w + dt * (k1w + 2.0 * k2w + 2.0 * k3w + k4w) / 6.0,
247            n.z + dt * (k1z + 2.0 * k2z + 2.0 * k3z + k4z) / 6.0,
248        );
249
250        assert_eq!(n.step(current), 0);
251        assert!((n.v - expected.0).abs() < 1e-14);
252        assert!((n.w - expected.1).abs() < 1e-14);
253        assert!((n.z - expected.2).abs() < 1e-14);
254    }
255
256    #[test]
257    fn pernarowski_invalid_input_preserves_state() {
258        let mut n = PernarowskiNeuron::new();
259        let before = (n.v, n.w, n.z);
260        assert_eq!(n.step(f64::NAN), 0);
261        assert_eq!((n.v, n.w, n.z), before);
262    }
263
264    #[test]
265    fn pernarowski_overflow_candidate_preserves_state() {
266        let mut n = PernarowskiNeuron::new();
267        n.v = 1.0e160;
268        let before = (n.v, n.w, n.z);
269        assert_eq!(n.step(0.5), 0);
270        assert_eq!((n.v, n.w, n.z), before);
271    }
272
273    #[test]
274    fn try_simulate_rejects_overflow_without_mutation() {
275        let mut neuron = PernarowskiNeuron {
276            v: 1.0e103,
277            ..Default::default()
278        };
279        let before = (neuron.v, neuron.w, neuron.z);
280        assert!(neuron.try_simulate(2, 0.5).is_none());
281        assert_eq!((neuron.v, neuron.w, neuron.z), before);
282    }
283
284    #[test]
285    fn pernarowski_nan_no_panic() {
286        PernarowskiNeuron::new().step(f64::NAN);
287    }
288
289    #[test]
290    fn pernarowski_negative_no_crash() {
291        let mut n = PernarowskiNeuron::new();
292        for _ in 0..500 {
293            n.step(-5.0);
294        }
295        assert!(n.v.is_finite());
296    }
297}