sc_neurocore_engine/neurons/biophysical/
mihalas_niebur.rs1#[derive(Clone, Debug)]
18pub struct MihalasNieburNeuron {
19 pub v: f64,
20 pub theta: f64,
21 pub i1: f64,
22 pub i2: f64,
23 pub v_rest: f64,
24 pub v_reset: f64,
25 pub theta_reset: f64,
26 pub theta_inf: f64,
27 pub leak_rate: f64,
28 pub threshold_voltage_coupling: f64,
29 pub threshold_decay_rate: f64,
30 pub current_decay_rate_1: f64,
31 pub current_decay_rate_2: f64,
32 pub current_retention_1: f64,
33 pub current_retention_2: f64,
34 pub current_jump_1: f64,
35 pub current_jump_2: f64,
36 pub dt: f64,
37}
38
39impl MihalasNieburNeuron {
40 pub fn new() -> Self {
42 Self {
43 v: -0.07,
44 theta: -0.05,
45 i1: 0.0,
46 i2: 0.0,
47 v_rest: -0.07,
48 v_reset: -0.07,
49 theta_reset: -0.06,
50 theta_inf: -0.05,
51 leak_rate: 0.05,
52 threshold_voltage_coupling: 0.005,
53 threshold_decay_rate: 0.01,
54 current_decay_rate_1: 0.2,
55 current_decay_rate_2: 0.02,
56 current_retention_1: 0.0,
57 current_retention_2: 1.0,
58 current_jump_1: 0.0,
59 current_jump_2: 0.0,
60 dt: 0.1,
61 }
62 }
63
64 fn finite(values: &[f64]) -> bool {
65 values.iter().all(|value| value.is_finite())
66 }
67
68 fn valid(&self) -> bool {
69 Self::finite(&[
70 self.v,
71 self.theta,
72 self.i1,
73 self.i2,
74 self.v_rest,
75 self.v_reset,
76 self.theta_reset,
77 self.theta_inf,
78 self.leak_rate,
79 self.threshold_voltage_coupling,
80 self.threshold_decay_rate,
81 self.current_decay_rate_1,
82 self.current_decay_rate_2,
83 self.current_retention_1,
84 self.current_retention_2,
85 self.current_jump_1,
86 self.current_jump_2,
87 self.dt,
88 ]) && self.leak_rate > 0.0
89 && self.threshold_decay_rate > 0.0
90 && self.current_decay_rate_1 > 0.0
91 && self.current_decay_rate_2 > 0.0
92 && self.dt > 0.0
93 && self.theta_reset > self.v_reset
94 }
95
96 fn derivatives(&self, state: [f64; 4], current: f64) -> [f64; 4] {
97 [
98 current + state[2] + state[3] - self.leak_rate * (state[0] - self.v_rest),
99 self.threshold_voltage_coupling * (state[0] - self.v_rest)
100 - self.threshold_decay_rate * (state[1] - self.theta_inf),
101 -self.current_decay_rate_1 * state[2],
102 -self.current_decay_rate_2 * state[3],
103 ]
104 }
105
106 fn add_scaled(state: [f64; 4], slope: [f64; 4], scale: f64) -> [f64; 4] {
107 [
108 state[0] + scale * slope[0],
109 state[1] + scale * slope[1],
110 state[2] + scale * slope[2],
111 state[3] + scale * slope[3],
112 ]
113 }
114
115 fn candidate(&self, current: f64) -> Option<(Self, i32)> {
116 if !self.valid() || !current.is_finite() {
117 return None;
118 }
119 let state = [self.v, self.theta, self.i1, self.i2];
120 let half_dt = 0.5 * self.dt;
121 let k1 = self.derivatives(state, current);
122 let k2 = self.derivatives(Self::add_scaled(state, k1, half_dt), current);
123 let k3 = self.derivatives(Self::add_scaled(state, k2, half_dt), current);
124 let k4 = self.derivatives(Self::add_scaled(state, k3, self.dt), current);
125 let values = [
126 state[0] + self.dt * (k1[0] + 2.0 * k2[0] + 2.0 * k3[0] + k4[0]) / 6.0,
127 state[1] + self.dt * (k1[1] + 2.0 * k2[1] + 2.0 * k3[1] + k4[1]) / 6.0,
128 state[2] + self.dt * (k1[2] + 2.0 * k2[2] + 2.0 * k3[2] + k4[2]) / 6.0,
129 state[3] + self.dt * (k1[3] + 2.0 * k2[3] + 2.0 * k3[3] + k4[3]) / 6.0,
130 ];
131 if !Self::finite(&values) {
132 return None;
133 }
134 let mut next = Self {
135 v: values[0],
136 theta: values[1],
137 i1: values[2],
138 i2: values[3],
139 ..self.clone()
140 };
141 let event = i32::from(next.v >= next.theta);
142 if event == 1 {
143 next.i1 = self.current_retention_1 * next.i1 + self.current_jump_1;
144 next.i2 = self.current_retention_2 * next.i2 + self.current_jump_2;
145 next.v = self.v_reset;
146 next.theta = self.theta_reset.max(next.theta);
147 }
148 next.valid().then_some((next, event))
149 }
150
151 pub fn try_step(&mut self, current: f64) -> Option<i32> {
153 let (candidate, event) = self.candidate(current)?;
154 *self = candidate;
155 Some(event)
156 }
157
158 pub fn step(&mut self, current: f64) -> i32 {
160 self.try_step(current).unwrap_or(0)
161 }
162
163 pub fn try_simulate(&mut self, n_steps: usize, current: f64) -> Option<(Vec<f64>, i64)> {
165 let mut candidate = self.clone();
166 let mut trace = Vec::with_capacity(n_steps);
167 let mut events = 0_i64;
168 for _ in 0..n_steps {
169 events += i64::from(candidate.try_step(current)?);
170 trace.push(candidate.v);
171 }
172 *self = candidate;
173 Some((trace, events))
174 }
175
176 pub fn reset(&mut self) {
178 self.v = self.v_rest;
179 self.theta = self.theta_inf;
180 self.i1 = 0.0;
181 self.i2 = 0.0;
182 }
183}
184
185impl Default for MihalasNieburNeuron {
186 fn default() -> Self {
187 Self::new()
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn paper_profile_adapts_under_constant_drive() {
197 let mut neuron = MihalasNieburNeuron::new();
198 let (_, events) = neuron
199 .try_simulate(2500, 0.002)
200 .expect("valid source profile");
201 assert_eq!(events, 13);
202 assert!(neuron.theta > neuron.theta_inf);
203 }
204
205 #[test]
206 fn event_uses_published_reset_map() {
207 let mut neuron = MihalasNieburNeuron::new();
208 neuron.v = -0.049;
209 neuron.i1 = 0.003;
210 neuron.i2 = -0.001;
211 neuron.current_retention_1 = 0.25;
212 neuron.current_retention_2 = 0.5;
213 neuron.current_jump_1 = 0.004;
214 neuron.current_jump_2 = 0.002;
215 assert_eq!(neuron.step(0.02), 1);
216 assert_eq!(neuron.v, neuron.v_reset);
217 assert!(neuron.theta >= neuron.theta_reset);
218 assert!(neuron.i1 > neuron.current_jump_1);
219 assert!(neuron.i2 > 0.0);
220 }
221
222 #[test]
223 fn invalid_batch_is_failure_atomic() {
224 let mut neuron = MihalasNieburNeuron::new();
225 let before = neuron.clone();
226 assert!(neuron.try_simulate(4, f64::NAN).is_none());
227 assert_eq!(neuron.v, before.v);
228 assert_eq!(neuron.theta, before.theta);
229 }
230}