Skip to main content

sc_neurocore_engine/neurons/trivial/
escape_rate.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 — Escape-Rate Neuron
8
9/// Escape-rate neuron — stochastic IF with exponential hazard. Gerstner 2000.
10#[derive(Clone, Debug)]
11pub struct EscapeRateNeuron {
12    pub v: f64,
13    pub v_rest: f64,
14    pub v_reset: f64,
15    pub v_threshold: f64,
16    pub tau_m: f64,
17    pub rho_0: f64,
18    pub delta_u: f64,
19    pub resistance: f64,
20    pub dt: f64,
21    pub rng_state: u16,
22    pub initial_seed: u16,
23}
24
25impl EscapeRateNeuron {
26    pub fn new(seed: u64) -> Self {
27        let narrowed = (seed & u64::from(u16::MAX)) as u16;
28        let initial_seed = if narrowed == 0 { 0xACE1 } else { narrowed };
29        Self {
30            v: -70.0,
31            v_rest: -70.0,
32            v_reset: -70.0,
33            v_threshold: -50.0,
34            tau_m: 10.0,
35            rho_0: 0.001,
36            delta_u: 3.0,
37            resistance: 1.0,
38            dt: 1.0,
39            rng_state: initial_seed,
40            initial_seed,
41        }
42    }
43
44    pub fn valid(&self) -> bool {
45        self.v.is_finite()
46            && self.v_rest.is_finite()
47            && self.v_reset.is_finite()
48            && self.v_threshold.is_finite()
49            && self.tau_m.is_finite()
50            && self.tau_m > 0.0
51            && self.rho_0.is_finite()
52            && self.rho_0 > 0.0
53            && self.delta_u.is_finite()
54            && self.delta_u > 0.0
55            && self.resistance.is_finite()
56            && self.resistance > 0.0
57            && self.dt.is_finite()
58            && self.dt > 0.0
59            && self.rng_state != 0
60    }
61
62    pub fn try_step(&mut self, current: f64) -> Result<i32, &'static str> {
63        if !self.v.is_finite()
64            || !self.v_rest.is_finite()
65            || !self.v_reset.is_finite()
66            || !self.v_threshold.is_finite()
67            || !self.tau_m.is_finite()
68            || self.tau_m <= 0.0
69            || !self.rho_0.is_finite()
70            || self.rho_0 <= 0.0
71            || !self.delta_u.is_finite()
72            || self.delta_u <= 0.0
73            || !self.resistance.is_finite()
74            || self.resistance <= 0.0
75            || !self.dt.is_finite()
76            || self.dt <= 0.0
77            || self.rng_state == 0
78            || !current.is_finite()
79        {
80            return Err("invalid escape-rate state or input");
81        }
82        let v_inf = self.v_rest + self.resistance * current;
83        let decay = (-self.dt / self.tau_m).exp();
84        let next_v = v_inf + (self.v - v_inf) * decay;
85        if !v_inf.is_finite() || !decay.is_finite() || !next_v.is_finite() {
86            return Err("non-finite escape-rate membrane candidate");
87        }
88        let hazard = self.rho_0
89            * ((next_v - self.v_threshold) / self.delta_u)
90                .clamp(-700.0, 700.0)
91                .exp()
92            * self.dt;
93        if !hazard.is_finite() || hazard < 0.0 {
94            return Err("non-finite escape hazard");
95        }
96        let p_spike = -(-hazard).exp_m1();
97        if !p_spike.is_finite() || !(0.0..=1.0).contains(&p_spike) {
98            return Err("invalid escape probability");
99        }
100        let mut sample = self.rng_state;
101        for _ in 0..8 {
102            let feedback = (sample ^ (sample >> 2) ^ (sample >> 3) ^ (sample >> 5)) & 1;
103            sample = (sample >> 1) | (feedback << 15);
104        }
105        let threshold = if p_spike <= 0.0 {
106            0_u32
107        } else if p_spike >= 1.0 {
108            65_536_u32
109        } else {
110            (p_spike * 65_535.0).floor() as u32 + 1
111        };
112        self.rng_state = sample;
113        if u32::from(sample) < threshold {
114            self.v = self.v_reset;
115            Ok(1)
116        } else {
117            self.v = next_v;
118            Ok(0)
119        }
120    }
121
122    pub fn step(&mut self, current: f64) -> i32 {
123        self.try_step(current).unwrap_or(0)
124    }
125
126    pub fn reset(&mut self) {
127        self.v = self.v_rest;
128        self.rng_state = self.initial_seed;
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn escape_rate_stochastic() {
138        let mut n = EscapeRateNeuron::new(42);
139        let total: i32 = (0..1000).map(|_| n.step(30.0)).sum();
140        assert!(total > 0);
141    }
142    #[test]
143    fn escape_rate_exact_flow_matches_closed_form() {
144        let mut n = EscapeRateNeuron::new(42);
145        n.v = -65.0;
146        n.dt = 5.0;
147        n.rho_0 = 1.0e-12;
148        let current = 10.0;
149        let v0 = n.v;
150        let v_inf = n.v_rest + n.resistance * current;
151        let euler = v0 + (-(v0 - n.v_rest) + n.resistance * current) / n.tau_m * n.dt;
152        let expected = v_inf + (v0 - v_inf) * (-n.dt / n.tau_m).exp();
153
154        assert_eq!(n.step(current), 0);
155        assert!((n.v - expected).abs() < 1e-14);
156        assert!((n.v - euler).abs() > 1e-3);
157    }
158    #[test]
159    fn escape_rate_reset_clears_state() {
160        let mut n = EscapeRateNeuron::new(42);
161        for _ in 0..100 {
162            n.step(30.0);
163        }
164        n.reset();
165        assert!((n.v - n.v_rest).abs() < 1e-10);
166    }
167    #[test]
168    fn escape_rate_bounded() {
169        let mut n = EscapeRateNeuron::new(42);
170        for _ in 0..1000 {
171            n.step(1e4);
172        }
173        assert!(n.v.is_finite());
174    }
175    #[test]
176    fn escape_rate_nan_no_panic() {
177        let mut n = EscapeRateNeuron::new(42);
178        let before = n.v;
179        assert_eq!(n.step(f64::NAN), 0);
180        assert_eq!(n.v, before);
181    }
182    #[test]
183    fn escape_rate_invalid_state_does_not_mutate() {
184        let mut n = EscapeRateNeuron::new(42);
185        n.v = -65.0;
186        n.tau_m = 0.0;
187        assert_eq!(n.step(1.0), 0);
188        assert_eq!(n.v, -65.0);
189    }
190    #[test]
191    fn escape_rate_seed_varies() {
192        let mut n1 = EscapeRateNeuron::new(1);
193        let mut n2 = EscapeRateNeuron::new(999);
194        let t1: i32 = (0..1000).map(|_| n1.step(30.0)).sum();
195        let t2: i32 = (0..1000).map(|_| n2.step(30.0)).sum();
196        assert!(t1 > 0 && t2 > 0);
197    }
198}