sc_neurocore_engine/neurons/channels/
ih.rs1use crate::neurons::biophysical::safe_rate;
10
11#[derive(Clone, Debug)]
27pub struct IhNeuron {
28 pub v: f64,
29 pub h: f64, pub n: f64, pub r: f64, pub g_na: f64,
34 pub g_k: f64,
35 pub g_h: f64, pub g_l: f64,
37 pub e_na: f64,
39 pub e_k: f64,
40 pub e_h: f64, pub e_l: f64,
42 pub c_m: f64,
43 pub phi: f64,
44 pub dt: f64,
45 pub v_threshold: f64,
46 pub gain: f64,
47}
48
49impl Default for IhNeuron {
50 fn default() -> Self {
51 Self::new()
52 }
53}
54
55impl IhNeuron {
56 pub fn new() -> Self {
57 Self {
58 v: -65.0,
59 h: 0.6,
60 n: 0.32,
61 r: 0.1,
62 g_na: 35.0,
63 g_k: 9.0,
64 g_h: 0.15,
65 g_l: 0.2,
66 e_na: 55.0,
67 e_k: -90.0,
68 e_h: -40.0,
69 e_l: -65.0,
70 c_m: 1.0,
71 phi: 5.0,
72 dt: 0.5,
73 v_threshold: -20.0,
74 gain: 1.0,
75 }
76 }
77
78 fn valid(&self) -> bool {
79 let finite = [
80 self.v,
81 self.h,
82 self.n,
83 self.r,
84 self.g_na,
85 self.g_k,
86 self.g_h,
87 self.g_l,
88 self.e_na,
89 self.e_k,
90 self.e_h,
91 self.e_l,
92 self.c_m,
93 self.phi,
94 self.dt,
95 self.v_threshold,
96 self.gain,
97 ]
98 .into_iter()
99 .all(f64::is_finite);
100 finite
101 && (-100.0..=60.0).contains(&self.v)
102 && [self.h, self.n, self.r]
103 .into_iter()
104 .all(|gate| (0.0..=1.0).contains(&gate))
105 && (0.0..=200.0).contains(&self.g_na)
106 && (0.0..=100.0).contains(&self.g_k)
107 && (0.0..=5.0).contains(&self.g_h)
108 && (0.0..=5.0).contains(&self.g_l)
109 && (30.0..=70.0).contains(&self.e_na)
110 && (-100.0..=-70.0).contains(&self.e_k)
111 && (-50.0..=0.0).contains(&self.e_h)
112 && (-80.0..=-40.0).contains(&self.e_l)
113 && (0.5..=2.0).contains(&self.c_m)
114 && (0.5..=10.0).contains(&self.phi)
115 && self.dt > 0.0
116 && self.dt <= 1.0
117 && (-20.0..=20.0).contains(&self.v_threshold)
118 && (0.0..=10.0).contains(&self.gain)
119 }
120
121 pub fn try_step(&mut self, current: f64) -> Result<i32, &'static str> {
122 if !current.is_finite() {
123 return Err("current must be finite");
124 }
125 if !self.valid() {
126 return Err("Ih state and parameters must satisfy the public bounds");
127 }
128
129 let mut candidate = self.clone();
130 let input = candidate.gain * current;
131 let sub_steps = 50;
132 let sub_dt = candidate.dt / sub_steps as f64;
133 let mut fired = 0i32;
134
135 for _ in 0..sub_steps {
136 let v = candidate.v;
137
138 let alpha_m = safe_rate(0.1, 35.0, v, 10.0, 1.0);
140 let beta_m = 4.0 * (-(v + 60.0) / 18.0).exp();
141 let m_inf = alpha_m / (alpha_m + beta_m);
142
143 let alpha_h = 0.07 * (-(v + 58.0) / 20.0).exp();
144 let beta_h = 1.0 / (1.0 + (-(v + 28.0) / 10.0).exp());
145
146 let alpha_n = safe_rate(0.01, 34.0, v, 10.0, 0.1);
147 let beta_n = 0.125 * (-(v + 44.0) / 80.0).exp();
148
149 let r_inf = 1.0 / (1.0 + ((v + 80.0) / 10.0).exp());
152 let tau_r = 100.0 + 200.0 / (1.0 + ((v + 70.0) / 10.0).exp());
153
154 candidate.h +=
156 sub_dt * candidate.phi * (alpha_h * (1.0 - candidate.h) - beta_h * candidate.h);
157 candidate.n +=
158 sub_dt * candidate.phi * (alpha_n * (1.0 - candidate.n) - beta_n * candidate.n);
159 candidate.r += sub_dt * (r_inf - candidate.r) / tau_r;
160
161 let i_na = candidate.g_na * m_inf.powi(3) * candidate.h * (v - candidate.e_na);
163 let i_k = candidate.g_k * candidate.n.powi(4) * (v - candidate.e_k);
164 let i_h = candidate.g_h * candidate.r * (v - candidate.e_h);
165 let i_l = candidate.g_l * (v - candidate.e_l);
166
167 let dv = (-i_na - i_k - i_h - i_l + input) / candidate.c_m;
168 candidate.v += sub_dt * dv;
169 if ![candidate.v, candidate.h, candidate.n, candidate.r]
170 .into_iter()
171 .all(f64::is_finite)
172 {
173 return Err("Ih candidate state became non-finite");
174 }
175
176 if candidate.v >= candidate.v_threshold {
177 fired = 1;
178 candidate.v = -65.0;
179 }
180 }
181
182 candidate.v = candidate.v.clamp(-100.0, 60.0);
183 candidate.h = candidate.h.clamp(0.0, 1.0);
184 candidate.n = candidate.n.clamp(0.0, 1.0);
185 candidate.r = candidate.r.clamp(0.0, 1.0);
186 *self = candidate;
187
188 Ok(fired)
189 }
190
191 pub fn step(&mut self, current: f64) -> i32 {
192 self.try_step(current).unwrap_or(0)
193 }
194
195 pub fn reset(&mut self) {
196 self.v = -65.0;
197 self.h = 0.6;
198 self.n = 0.32;
199 self.r = 0.1;
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
210 fn ih_fires_with_input() {
211 let mut n = IhNeuron::new();
212 let mut spikes = 0;
213 for _ in 0..2_000 {
214 spikes += n.step(2.0);
215 }
216 assert!(spikes > 5, "Ih neuron must fire with input, got {spikes}");
217 }
218
219 #[test]
220 fn ih_silent_without_input() {
221 let mut n = IhNeuron::new();
222 let mut spikes = 0;
223 for _ in 0..10_000 {
224 spikes += n.step(0.0);
225 }
226 assert_eq!(
227 spikes, 0,
228 "Ih neuron must be silent without input, got {spikes}"
229 );
230 }
231
232 #[test]
233 fn ih_sag_potential() {
234 let mut with_ih = IhNeuron::new();
236 let mut no_ih = IhNeuron::new();
237 no_ih.g_h = 0.0;
238
239 for _ in 0..4000 {
241 with_ih.step(-3.0);
242 no_ih.step(-3.0);
243 }
244 assert!(
246 with_ih.v > no_ih.v,
247 "Ih sag must depolarise from hyperpolarisation: Ih={:.1} vs no_Ih={:.1}",
248 with_ih.v,
249 no_ih.v
250 );
251 }
252
253 #[test]
254 fn ih_r_gate_activates_on_hyperpolarisation() {
255 let mut n = IhNeuron::new();
256 let r_before = n.r;
257 for _ in 0..4000 {
259 n.step(-5.0);
260 }
261 assert!(
262 n.r > r_before,
263 "r gate must increase during hyperpolarisation, r={}",
264 n.r
265 );
266 }
267
268 #[test]
269 fn ih_rebound_excitation() {
270 let mut n = IhNeuron::new();
272 for _ in 0..4000 {
274 n.step(-3.0);
275 }
276 let r_after_hyp = n.r;
277 assert!(
278 r_after_hyp > 0.2,
279 "r must build up during hyperpolarisation, r={r_after_hyp}"
280 );
281
282 let mut rebound_spikes = 0;
284 for _ in 0..500 {
285 rebound_spikes += n.step(1.5);
286 }
287
288 let mut n2 = IhNeuron::new();
290 let mut direct_spikes = 0;
291 for _ in 0..500 {
292 direct_spikes += n2.step(1.5);
293 }
294
295 assert!(
296 rebound_spikes >= direct_spikes,
297 "Rebound should facilitate firing: rebound={rebound_spikes} vs direct={direct_spikes}"
298 );
299 }
300
301 #[test]
302 fn ih_negative_input_no_crash() {
303 let mut n = IhNeuron::new();
304 for _ in 0..10_000 {
305 n.step(-100.0);
306 }
307 assert!(n.v.is_finite());
308 assert!(n.v >= -100.0);
309 }
310
311 #[test]
312 fn ih_nan_input_is_rejected_atomically() {
313 let mut n = IhNeuron::new();
314 let before = n.clone();
315 assert!(n.try_step(f64::NAN).is_err());
316 assert_eq!(n.v, before.v);
317 assert_eq!(n.h, before.h);
318 assert_eq!(n.n, before.n);
319 assert_eq!(n.r, before.r);
320 }
321
322 #[test]
323 fn ih_invalid_configuration_is_rejected_atomically() {
324 let mut n = IhNeuron::new();
325 n.c_m = 0.0;
326 let before = n.clone();
327 assert!(n.try_step(1.0).is_err());
328 assert_eq!(n.v, before.v);
329 assert_eq!(n.c_m, before.c_m);
330 }
331
332 #[test]
333 fn ih_extreme_input_bounded() {
334 let mut n = IhNeuron::new();
335 for _ in 0..1000 {
336 n.step(1e6);
337 }
338 assert!(n.v.is_finite() && n.v <= 60.0);
339 }
340
341 #[test]
342 fn ih_reset_clears_state() {
343 let mut n = IhNeuron::new();
344 n.g_h = 0.3;
345 for _ in 0..1000 {
346 n.step(10.0);
347 }
348 n.reset();
349 assert_eq!(n.v, -65.0);
350 assert_eq!(n.r, 0.1);
351 assert_eq!(n.g_h, 0.3);
352 }
353
354 #[test]
355 fn ih_gates_bounded() {
356 let mut n = IhNeuron::new();
357 for _ in 0..10_000 {
358 n.step(10.0);
359 }
360 assert!(n.h >= 0.0 && n.h <= 1.0);
361 assert!(n.n >= 0.0 && n.n <= 1.0);
362 assert!(n.r >= 0.0 && n.r <= 1.0);
363 }
364
365 #[test]
366 fn ih_performance_1k_steps() {
367 let start = std::time::Instant::now();
368 let mut n = IhNeuron::new();
369 for _ in 0..1_000 {
370 std::hint::black_box(n.step(2.0));
371 }
372 let elapsed = start.elapsed();
373 assert!(
374 elapsed.as_millis() < 200,
375 "1k steps must complete in <200ms"
376 );
377 }
378}