sc_neurocore_engine/neurons/trivial/
escape_rate.rs1#[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}