sc_neurocore_engine/neurons/biophysical/
glif.rs1#[derive(Clone, Debug)]
13pub struct GLIFNeuron {
14 pub v: f64,
15 pub theta_spike: f64,
16 pub i_asc1: f64,
17 pub i_asc2: f64,
18 pub theta_voltage: f64,
19 pub refractory_remaining: f64,
20 pub e_l: f64,
21 pub capacitance: f64,
22 pub resistance: f64,
23 pub theta_inf: f64,
24 pub b_spike: f64,
25 pub b_voltage: f64,
26 pub a_voltage: f64,
27 pub k_asc1: f64,
28 pub k_asc2: f64,
29 pub f_v: f64,
30 pub delta_v: f64,
31 pub delta_theta_spike: f64,
32 pub f_asc1: f64,
33 pub f_asc2: f64,
34 pub delta_i_asc1: f64,
35 pub delta_i_asc2: f64,
36 pub refractory_period: f64,
37 pub dt: f64,
38}
39
40impl GLIFNeuron {
41 pub fn new() -> Self {
43 Self {
44 v: -70.0,
45 theta_spike: 0.0,
46 i_asc1: 0.0,
47 i_asc2: 0.0,
48 theta_voltage: 0.0,
49 refractory_remaining: 0.0,
50 e_l: -70.0,
51 capacitance: 10.0,
52 resistance: 1.0,
53 theta_inf: -50.0,
54 b_spike: 0.01,
55 b_voltage: 0.01,
56 a_voltage: 0.0001,
57 k_asc1: 0.1,
58 k_asc2: 0.005,
59 f_v: 0.0,
60 delta_v: 0.0,
61 delta_theta_spike: 2.0,
62 f_asc1: 1.0,
63 f_asc2: 1.0,
64 delta_i_asc1: 1.0,
65 delta_i_asc2: 0.5,
66 refractory_period: 2.0,
67 dt: 1.0,
68 }
69 }
70
71 fn finite(values: &[f64]) -> bool {
72 values.iter().all(|value| value.is_finite())
73 }
74
75 fn valid(&self) -> bool {
76 Self::finite(&[
77 self.v,
78 self.theta_spike,
79 self.i_asc1,
80 self.i_asc2,
81 self.theta_voltage,
82 self.refractory_remaining,
83 self.e_l,
84 self.capacitance,
85 self.resistance,
86 self.theta_inf,
87 self.b_spike,
88 self.b_voltage,
89 self.a_voltage,
90 self.k_asc1,
91 self.k_asc2,
92 self.f_v,
93 self.delta_v,
94 self.delta_theta_spike,
95 self.f_asc1,
96 self.f_asc2,
97 self.delta_i_asc1,
98 self.delta_i_asc2,
99 self.refractory_period,
100 self.dt,
101 ]) && self.capacitance > 0.0
102 && self.resistance > 0.0
103 && self.b_spike > 0.0
104 && self.b_voltage > 0.0
105 && self.k_asc1 > 0.0
106 && self.k_asc2 > 0.0
107 && self.dt > 0.0
108 && self.refractory_remaining >= 0.0
109 && self.refractory_period >= 0.0
110 }
111
112 fn decay(rate: f64, dt: f64) -> f64 {
113 (-rate * dt).exp()
114 }
115
116 fn exponential_convolution(decay_rate: f64, forcing_rate: f64, dt: f64) -> f64 {
117 let difference = decay_rate - forcing_rate;
118 let scale = 1.0_f64.max(decay_rate.abs()).max(forcing_rate.abs());
119 if difference.abs() <= 1e-12 * scale {
120 dt * (-decay_rate * dt).exp()
121 } else {
122 ((-forcing_rate * dt).exp() - (-decay_rate * dt).exp()) / difference
123 }
124 }
125
126 fn candidate(&self, current: f64) -> Option<(Self, i32)> {
127 if !self.valid() || !current.is_finite() {
128 return None;
129 }
130 if self.refractory_remaining > 0.0 {
131 let mut next = self.clone();
132 next.refractory_remaining = (self.refractory_remaining - self.dt).max(0.0);
133 return Some((next, 0));
134 }
135
136 let total_current = current + self.i_asc1 + self.i_asc2;
137 let membrane_rate = 1.0 / (self.resistance * self.capacitance);
138 let membrane_decay = Self::decay(membrane_rate, self.dt);
139 let equilibrium_offset = self.resistance * total_current;
140 let voltage_offset = self.v - self.e_l;
141 let next_offset =
142 equilibrium_offset + (voltage_offset - equilibrium_offset) * membrane_decay;
143 let next_v = self.e_l + next_offset;
144 let next_theta_spike = self.theta_spike * Self::decay(self.b_spike, self.dt);
145 let next_i_asc1 = self.i_asc1 * Self::decay(self.k_asc1, self.dt);
146 let next_i_asc2 = self.i_asc2 * Self::decay(self.k_asc2, self.dt);
147 let threshold_forcing = equilibrium_offset * (1.0 - Self::decay(self.b_voltage, self.dt))
148 / self.b_voltage
149 + (voltage_offset - equilibrium_offset)
150 * Self::exponential_convolution(self.b_voltage, membrane_rate, self.dt);
151 let next_theta_voltage = self.theta_voltage * Self::decay(self.b_voltage, self.dt)
152 + self.a_voltage * threshold_forcing;
153 let mut next = Self {
154 v: next_v,
155 theta_spike: next_theta_spike,
156 i_asc1: next_i_asc1,
157 i_asc2: next_i_asc2,
158 theta_voltage: next_theta_voltage,
159 refractory_remaining: 0.0,
160 ..self.clone()
161 };
162 if !next.valid() {
163 return None;
164 }
165 if next.v <= self.theta_inf + next.theta_spike + next.theta_voltage {
166 return Some((next, 0));
167 }
168 next.v = self.e_l + self.f_v * (next.v - self.e_l) - self.delta_v;
169 next.theta_spike += self.delta_theta_spike;
170 next.i_asc1 = self.f_asc1 * next.i_asc1 + self.delta_i_asc1;
171 next.i_asc2 = self.f_asc2 * next.i_asc2 + self.delta_i_asc2;
172 next.refractory_remaining = self.refractory_period;
173 next.valid().then_some((next, 1))
174 }
175
176 pub fn try_step(&mut self, current: f64) -> Option<i32> {
178 let (candidate, event) = self.candidate(current)?;
179 *self = candidate;
180 Some(event)
181 }
182
183 pub fn step(&mut self, current: f64) -> i32 {
185 self.try_step(current).unwrap_or(0)
186 }
187
188 pub fn try_simulate(&mut self, n_steps: usize, current: f64) -> Option<(Vec<f64>, i64)> {
190 let mut candidate = self.clone();
191 let mut trace = Vec::with_capacity(n_steps);
192 let mut events = 0_i64;
193 for _ in 0..n_steps {
194 events += i64::from(candidate.try_step(current)?);
195 trace.push(candidate.v);
196 }
197 *self = candidate;
198 Some((trace, events))
199 }
200
201 pub fn reset(&mut self) {
203 self.v = self.e_l;
204 self.theta_spike = 0.0;
205 self.i_asc1 = 0.0;
206 self.i_asc2 = 0.0;
207 self.theta_voltage = 0.0;
208 self.refractory_remaining = 0.0;
209 }
210}
211
212impl Default for GLIFNeuron {
213 fn default() -> Self {
214 Self::new()
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[test]
223 fn default_profile_has_source_five_state_contract() {
224 let neuron = GLIFNeuron::new();
225 assert!(neuron.valid());
226 assert_eq!(
227 neuron.theta_inf + neuron.theta_spike + neuron.theta_voltage,
228 -50.0
229 );
230 }
231
232 #[test]
233 fn reset_rules_and_refractory_state_are_explicit() {
234 let mut neuron = GLIFNeuron::new();
235 neuron.v = -50.0;
236 assert_eq!(neuron.try_step(40.0), Some(1));
237 assert_eq!(neuron.v, -70.0);
238 assert_eq!(neuron.theta_spike, 2.0);
239 assert_eq!(neuron.i_asc1, 1.0);
240 assert_eq!(neuron.i_asc2, 0.5);
241 assert_eq!(neuron.refractory_remaining, 2.0);
242 }
243
244 #[test]
245 fn invalid_batch_is_failure_atomic() {
246 let mut neuron = GLIFNeuron::new();
247 let before = neuron.clone();
248 assert!(neuron.try_simulate(4, f64::NAN).is_none());
249 assert_eq!(neuron.v, before.v);
250 assert_eq!(neuron.theta_spike, before.theta_spike);
251 }
252
253 #[test]
254 fn source_profile_regimes_are_pinned() {
255 for (current, expected) in [(0.0, 0), (22.0, 22), (30.0, 49), (50.0, 80)] {
256 let mut neuron = GLIFNeuron::new();
257 let (_, events) = neuron.try_simulate(1000, current).expect("valid profile");
258 assert_eq!(events, expected, "current={current}");
259 }
260 }
261}