1#[derive(Clone, Debug)]
12pub struct HillTononiNeuron {
14 pub v: f64,
15 pub theta: f64,
16 pub d_k: f64,
17 pub m_h: f64,
18 pub m_t: f64,
19 pub h_t: f64,
20 pub spike_timer: f64,
21 pub g_na_l: f64,
22 pub g_k_l: f64,
23 pub g_na_p: f64,
24 pub g_dk: f64,
25 pub g_h: f64,
26 pub g_t: f64,
27 pub e_na: f64,
28 pub e_k: f64,
29 pub e_na_p: f64,
30 pub e_dk: f64,
31 pub e_h: f64,
32 pub e_t: f64,
33 pub n_na_p: f64,
34 pub n_t: f64,
35 pub tau_m: f64,
36 pub theta_eq: f64,
37 pub tau_theta: f64,
38 pub g_spike: f64,
39 pub t_spike: f64,
40 pub tau_spike: f64,
41 pub tau_d: f64,
42 pub d_influx_peak: f64,
43 pub d_threshold: f64,
44 pub d_slope: f64,
45 pub d_eq: f64,
46 pub d_half: f64,
47 pub dt: f64,
48}
49
50impl HillTononiNeuron {
51 pub fn new() -> Self {
53 Self {
54 v: -70.0,
55 theta: -51.0,
56 d_k: 0.001,
57 m_h: 0.2871859013825026,
58 m_t: 0.1450215950687922,
59 h_t: 0.03732688734412946,
60 spike_timer: 0.0,
61 g_na_l: 0.2,
62 g_k_l: 1.0,
63 g_na_p: 0.5,
64 g_dk: 0.5,
65 g_h: 0.0,
66 g_t: 0.0,
67 e_na: 30.0,
68 e_k: -90.0,
69 e_na_p: 30.0,
70 e_dk: -90.0,
71 e_h: -40.0,
72 e_t: 0.0,
73 n_na_p: 3.0,
74 n_t: 2.0,
75 tau_m: 16.0,
76 theta_eq: -51.0,
77 tau_theta: 2.0,
78 g_spike: 1.0,
79 t_spike: 2.0,
80 tau_spike: 1.75,
81 tau_d: 1250.0,
82 d_influx_peak: 0.025,
83 d_threshold: -10.0,
84 d_slope: 5.0,
85 d_eq: 0.001,
86 d_half: 0.25,
87 dt: 0.25,
88 }
89 }
90
91 fn m_h_inf(v: f64) -> f64 {
92 1.0 / (1.0 + ((v + 75.0) / 5.5).exp())
93 }
94
95 fn tau_m_h(v: f64) -> f64 {
96 1.0 / ((-14.59 - 0.086 * v).exp() + (-1.87 + 0.0701 * v).exp())
97 }
98
99 fn m_t_inf(v: f64) -> f64 {
100 1.0 / (1.0 + (-(v + 59.0) / 6.2).exp())
101 }
102
103 fn tau_m_t(v: f64) -> f64 {
104 0.22 / ((-(v + 132.0) / 16.7).exp() + ((v + 16.8) / 18.2).exp()) + 0.13
105 }
106
107 fn h_t_inf(v: f64) -> f64 {
108 1.0 / (1.0 + ((v + 83.0) / 4.0).exp())
109 }
110
111 fn tau_h_t(v: f64) -> f64 {
112 8.2 + (56.6 + 0.27 * ((v + 115.2) / 5.0).exp()) / (1.0 + ((v + 86.0) / 3.2).exp())
113 }
114
115 fn d_k_inf(&self, v: f64) -> f64 {
116 let influx = self.d_influx_peak / (1.0 + (-(v - self.d_threshold) / self.d_slope).exp());
117 self.tau_d * influx + self.d_eq
118 }
119
120 fn derivatives(&self, state: [f64; 6], current: f64, spike_active: bool) -> [f64; 6] {
121 let [v, theta, d_k, m_h, m_t, h_t] = state;
122 let m_na_p = 1.0 / (1.0 + (-(v + 55.7) / 7.7).exp());
123 let d_activation = 1.0 / (1.0 + (self.d_half / d_k.max(1e-15)).powf(3.5));
124 let i_na_l = -self.g_na_l * (v - self.e_na);
125 let i_k_l = -self.g_k_l * (v - self.e_k);
126 let i_na_p = -self.g_na_p * m_na_p.powf(self.n_na_p) * (v - self.e_na_p);
127 let i_dk = -self.g_dk * d_activation * (v - self.e_dk);
128 let i_h = -self.g_h * m_h * (v - self.e_h);
129 let i_t = -self.g_t * m_t.powf(self.n_t) * h_t * (v - self.e_t);
130 let i_spike = if spike_active {
131 -self.g_spike * (v - self.e_k) / self.tau_spike
132 } else {
133 0.0
134 };
135 [
136 (i_na_l + i_k_l + i_na_p + i_dk + i_h + i_t + current) / self.tau_m + i_spike,
137 -(theta - self.theta_eq) / self.tau_theta,
138 (self.d_k_inf(v) - d_k) / self.tau_d,
139 (Self::m_h_inf(v) - m_h) / Self::tau_m_h(v),
140 (Self::m_t_inf(v) - m_t) / Self::tau_m_t(v),
141 (Self::h_t_inf(v) - h_t) / Self::tau_h_t(v),
142 ]
143 }
144
145 fn shifted(state: [f64; 6], slope: [f64; 6], scale: f64) -> [f64; 6] {
146 std::array::from_fn(|index| state[index] + scale * slope[index])
147 }
148
149 fn candidate(&self, state: [f64; 6], current: f64, spike_active: bool) -> [f64; 6] {
150 let k1 = self.derivatives(state, current, spike_active);
151 let k2 = self.derivatives(
152 Self::shifted(state, k1, 0.5 * self.dt),
153 current,
154 spike_active,
155 );
156 let k3 = self.derivatives(
157 Self::shifted(state, k2, 0.5 * self.dt),
158 current,
159 spike_active,
160 );
161 let k4 = self.derivatives(Self::shifted(state, k3, self.dt), current, spike_active);
162 std::array::from_fn(|index| {
163 state[index]
164 + self.dt * (k1[index] + 2.0 * k2[index] + 2.0 * k3[index] + k4[index]) / 6.0
165 })
166 }
167
168 fn configuration_is_valid(&self) -> bool {
169 let values = [
170 self.v,
171 self.theta,
172 self.d_k,
173 self.m_h,
174 self.m_t,
175 self.h_t,
176 self.spike_timer,
177 self.g_na_l,
178 self.g_k_l,
179 self.g_na_p,
180 self.g_dk,
181 self.g_h,
182 self.g_t,
183 self.e_na,
184 self.e_k,
185 self.e_na_p,
186 self.e_dk,
187 self.e_h,
188 self.e_t,
189 self.n_na_p,
190 self.n_t,
191 self.tau_m,
192 self.theta_eq,
193 self.tau_theta,
194 self.g_spike,
195 self.t_spike,
196 self.tau_spike,
197 self.tau_d,
198 self.d_influx_peak,
199 self.d_threshold,
200 self.d_slope,
201 self.d_eq,
202 self.d_half,
203 self.dt,
204 ];
205 values.iter().all(|value| value.is_finite())
206 && self.d_k >= 0.0
207 && self.spike_timer >= 0.0
208 && [
209 self.g_na_l,
210 self.g_k_l,
211 self.g_na_p,
212 self.g_dk,
213 self.g_h,
214 self.g_t,
215 self.g_spike,
216 self.d_influx_peak,
217 self.d_eq,
218 ]
219 .iter()
220 .all(|value| *value >= 0.0)
221 && [
222 self.n_na_p,
223 self.n_t,
224 self.tau_m,
225 self.tau_theta,
226 self.t_spike,
227 self.tau_spike,
228 self.tau_d,
229 self.d_slope,
230 self.d_half,
231 self.dt,
232 ]
233 .iter()
234 .all(|value| *value > 0.0)
235 }
236
237 pub fn try_step(&mut self, current: f64) -> Result<i32, &'static str> {
239 if !current.is_finite() || !self.configuration_is_valid() {
240 return Err("Hill-Tononi configuration and current must be finite and physical");
241 }
242 let refractory = self.spike_timer > 0.0;
243 let state = [self.v, self.theta, self.d_k, self.m_h, self.m_t, self.h_t];
244 let mut next = self.candidate(state, current, refractory);
245 if !next.iter().all(|value| value.is_finite()) || next[2] < 0.0 {
246 return Err("Hill-Tononi candidate must be finite and physical");
247 }
248 let mut timer = (self.spike_timer - self.dt).max(0.0);
249 let spike = !refractory && next[0] >= next[1];
250 if spike {
251 next[0] = self.e_na;
252 next[1] = self.e_na;
253 timer = self.t_spike;
254 }
255 [self.v, self.theta, self.d_k, self.m_h, self.m_t, self.h_t] = next;
256 self.spike_timer = timer;
257 Ok(i32::from(spike))
258 }
259
260 pub fn step(&mut self, current: f64) -> i32 {
262 self.try_step(current).unwrap_or(0)
263 }
264
265 pub fn reset(&mut self) {
267 self.v = -70.0;
268 self.theta = -51.0;
269 self.d_k = 0.001;
270 self.m_h = Self::m_h_inf(self.v);
271 self.m_t = Self::m_t_inf(self.v);
272 self.h_t = Self::h_t_inf(self.v);
273 self.spike_timer = 0.0;
274 }
275}
276
277impl Default for HillTononiNeuron {
278 fn default() -> Self {
279 Self::new()
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 #[test]
288 fn source_defaults_and_python_anchor() {
289 let mut neuron = HillTononiNeuron::new();
290 assert_eq!(
291 [neuron.v, neuron.theta, neuron.d_k, neuron.dt],
292 [-70.0, -51.0, 0.001, 0.25]
293 );
294 assert_eq!(neuron.step(12.0), 0);
295 let expected = [
296 -69.81228106951788,
297 -51.0,
298 0.0010000391293823398,
299 0.2871847356365222,
300 0.1451785200593081,
301 0.037318086618308974,
302 ];
303 let observed = [
304 neuron.v,
305 neuron.theta,
306 neuron.d_k,
307 neuron.m_h,
308 neuron.m_t,
309 neuron.h_t,
310 ];
311 for (actual, target) in observed.into_iter().zip(expected) {
312 assert!((actual - target).abs() < 2e-12, "{actual} != {target}");
313 }
314 }
315
316 #[test]
317 fn spike_sets_dynamic_threshold_and_pulse() {
318 let mut neuron = HillTononiNeuron::new();
319 neuron.v = -50.0;
320 neuron.theta = -51.0;
321 assert_eq!(neuron.try_step(0.0), Ok(1));
322 assert_eq!(
323 [neuron.v, neuron.theta, neuron.spike_timer],
324 [30.0, 30.0, 2.0]
325 );
326 assert_eq!(neuron.try_step(0.0), Ok(0));
327 assert_eq!(neuron.spike_timer, 1.75);
328 assert!(neuron.v < 30.0);
329 }
330
331 #[test]
332 fn invalid_step_is_atomic() {
333 let mut neuron = HillTononiNeuron::new();
334 let before = [
335 neuron.v,
336 neuron.theta,
337 neuron.d_k,
338 neuron.m_h,
339 neuron.m_t,
340 neuron.h_t,
341 ];
342 assert!(neuron.try_step(f64::NAN).is_err());
343 assert_eq!(
344 [
345 neuron.v,
346 neuron.theta,
347 neuron.d_k,
348 neuron.m_h,
349 neuron.m_t,
350 neuron.h_t
351 ],
352 before
353 );
354 }
355}