1use super::super::biophysical::safe_rate;
10
11#[derive(Clone, Debug)]
30pub struct DCNNeuron {
31 pub v: f64,
32 pub h: f64, pub n: f64, pub p: f64, pub s: f64, pub r: f64, pub ca: f64, pub g_na: f64,
40 pub g_nap: f64, pub g_k: f64,
42 pub g_t: f64, pub g_ahp: f64, pub g_h: f64, pub g_l: f64,
46 pub e_na: f64,
48 pub e_k: f64,
49 pub e_ca: f64,
50 pub e_h: f64,
51 pub e_l: f64,
52 pub c_m: f64,
53 pub phi: f64,
54 pub tau_ca: f64,
55 pub kd_ahp: f64,
56 pub dt: f64,
57 pub v_threshold: f64,
58 pub gain: f64,
59}
60
61impl Default for DCNNeuron {
62 fn default() -> Self {
63 Self::new()
64 }
65}
66
67impl DCNNeuron {
68 pub fn new() -> Self {
69 Self {
70 v: -60.0,
71 h: 0.6,
72 n: 0.32,
73 p: 0.01, s: 0.8, r: 0.1, ca: 0.05, g_na: 35.0,
78 g_nap: 0.5, g_k: 9.0,
80 g_t: 0.1, g_ahp: 2.0, g_h: 0.02, g_l: 0.2, e_na: 55.0,
85 e_k: -90.0,
86 e_ca: 120.0,
87 e_h: -40.0,
88 e_l: -65.0,
89 c_m: 1.0,
90 phi: 5.0,
91 tau_ca: 150.0,
92 kd_ahp: 0.5,
93 dt: 0.5,
94 v_threshold: -20.0,
95 gain: 1.0,
96 }
97 }
98
99 pub fn step(&mut self, current: f64) -> i32 {
100 if !self.is_valid() || !current.is_finite() {
101 return 0;
102 }
103 let input = self.gain * current;
104 let sub_steps = 20;
105 let sub_dt = self.dt / sub_steps as f64;
106 let mut fired = 0i32;
107 let (mut v, mut h, mut n, mut p, mut s, mut r, mut ca) =
108 (self.v, self.h, self.n, self.p, self.s, self.r, self.ca);
109
110 for _ in 0..sub_steps {
111 let alpha_m = safe_rate(0.1, 35.0, v, 10.0, 1.0);
113 let beta_m = 4.0 * (-(v + 60.0) / 18.0).exp();
114 let m_inf = alpha_m / (alpha_m + beta_m);
115
116 let alpha_h = 0.07 * (-(v + 58.0) / 20.0).exp();
117 let beta_h = 1.0 / (1.0 + (-(v + 28.0) / 10.0).exp());
118
119 let alpha_n = safe_rate(0.01, 34.0, v, 10.0, 0.1);
121 let beta_n = 0.125 * (-(v + 44.0) / 80.0).exp();
122
123 let p_inf = 1.0 / (1.0 + (-(v + 48.0) / 5.0).exp());
125 let tau_p = 5.0 + 15.0 / (1.0 + ((v + 48.0) / 10.0).powi(2)).max(0.01);
126
127 let m_t_inf = 1.0 / (1.0 + (-(v + 52.0) / 5.0).exp());
129 let s_inf = 1.0 / (1.0 + ((v + 60.0) / 6.5).exp());
130 let tau_s = 20.0 + 50.0 / (1.0 + ((v + 65.0) / 10.0).exp());
131
132 let r_inf = 1.0 / (1.0 + ((v + 80.0) / 10.0).exp());
134 let tau_r = 100.0 + 200.0 / (1.0 + ((v + 70.0) / 10.0).exp());
135
136 h = dcn_exact_hh_gate(h, alpha_h, beta_h, self.phi, sub_dt);
140 n = dcn_exact_hh_gate(n, alpha_n, beta_n, self.phi, sub_dt);
141 p = dcn_exact_relax(p, p_inf, tau_p, sub_dt);
142 s = dcn_exact_relax(s, s_inf, tau_s, sub_dt);
143 r = dcn_exact_relax(r, r_inf, tau_r, sub_dt);
144
145 let i_t = self.g_t * m_t_inf.powi(2) * s * (v - self.e_ca);
147 let ca_entry = if i_t < 0.0 { -i_t * 0.001 } else { 0.0 };
148 ca = dcn_exact_relax(ca, ca_entry * self.tau_ca, self.tau_ca, sub_dt).max(0.0);
149
150 let ahp_inf = ca.powi(2) / (ca.powi(2) + self.kd_ahp.powi(2));
152
153 let g_na_eff = self.g_na * m_inf.powi(3) * h;
156 let g_nap_eff = self.g_nap * p;
157 let g_k_eff = self.g_k * n.powi(4);
158 let g_t_eff = self.g_t * m_t_inf.powi(2) * s;
159 let g_ahp_eff = self.g_ahp * ahp_inf;
160 let g_h_eff = self.g_h * r;
161 v = dcn_exact_voltage_step(
162 v,
163 input,
164 self.c_m,
165 sub_dt,
166 &[
167 (g_na_eff, self.e_na),
168 (g_nap_eff, self.e_na),
169 (g_k_eff, self.e_k),
170 (g_t_eff, self.e_ca),
171 (g_ahp_eff, self.e_k),
172 (g_h_eff, self.e_h),
173 (self.g_l, self.e_l),
174 ],
175 );
176
177 if v >= self.v_threshold {
178 fired = 1;
179 v = -60.0;
180 s *= 0.5; ca += 0.5; }
183 }
184
185 if ![v, h, n, p, s, r, ca].iter().all(|value| value.is_finite()) {
186 return 0;
187 }
188 self.v = v.clamp(-100.0, 60.0);
189 self.h = h.clamp(0.0, 1.0);
190 self.n = n.clamp(0.0, 1.0);
191 self.p = p.clamp(0.0, 1.0);
192 self.s = s.clamp(0.0, 1.0);
193 self.r = r.clamp(0.0, 1.0);
194 self.ca = ca.max(0.0);
195
196 fired
197 }
198
199 pub fn reset(&mut self) {
200 *self = Self::new();
201 }
202
203 fn is_valid(&self) -> bool {
204 [
205 self.v,
206 self.h,
207 self.n,
208 self.p,
209 self.s,
210 self.r,
211 self.ca,
212 self.g_na,
213 self.g_nap,
214 self.g_k,
215 self.g_t,
216 self.g_ahp,
217 self.g_h,
218 self.g_l,
219 self.e_na,
220 self.e_k,
221 self.e_ca,
222 self.e_h,
223 self.e_l,
224 self.c_m,
225 self.phi,
226 self.tau_ca,
227 self.kd_ahp,
228 self.dt,
229 self.v_threshold,
230 self.gain,
231 ]
232 .iter()
233 .all(|value| value.is_finite())
234 && [self.h, self.n, self.p, self.s, self.r]
235 .iter()
236 .all(|gate| (0.0..=1.0).contains(gate))
237 && self.ca >= 0.0
238 && (-100.0..=60.0).contains(&self.v)
239 && [
240 self.g_na, self.g_nap, self.g_k, self.g_t, self.g_ahp, self.g_h, self.g_l,
241 ]
242 .iter()
243 .all(|g| *g >= 0.0)
244 && self.c_m > 0.0
245 && self.phi > 0.0
246 && self.tau_ca > 0.0
247 && self.kd_ahp > 0.0
248 && self.dt > 0.0
249 && self.gain >= 0.0
250 }
251}
252
253fn dcn_exact_relax(value: f64, target: f64, tau: f64, dt: f64) -> f64 {
254 target + (value - target) * (-dt / tau).exp()
255}
256
257fn dcn_exact_hh_gate(value: f64, alpha: f64, beta: f64, phi: f64, dt: f64) -> f64 {
258 let rate = phi * (alpha + beta);
259 let target = alpha / (alpha + beta);
260 target + (value - target) * (-rate * dt).exp()
261}
262
263fn dcn_exact_voltage_step(
264 v: f64,
265 input_current: f64,
266 c_m: f64,
267 dt: f64,
268 conductances: &[(f64, f64)],
269) -> f64 {
270 let g_total: f64 = conductances.iter().map(|(g, _)| *g).sum();
271 if g_total <= 0.0 {
272 return v + dt * input_current / c_m;
273 }
274 let reversal_drive: f64 = conductances.iter().map(|(g, e_rev)| g * e_rev).sum();
275 let v_inf = (input_current + reversal_drive) / g_total;
276 v_inf + (v - v_inf) * (-dt * g_total / c_m).exp()
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282
283 #[test]
286 fn dcn_fires_with_input() {
287 let mut n = DCNNeuron::new();
288 let mut spikes = 0;
289 for _ in 0..2_000 {
290 spikes += n.step(5.0);
291 }
292 assert!(
293 spikes > 3,
294 "DCN must fire with excitatory input, got {spikes}"
295 );
296 }
297
298 #[test]
299 fn dcn_spontaneous_activity() {
300 let mut n = DCNNeuron::new();
303 let mut spikes = 0;
304 for _ in 0..20_000 {
305 spikes += n.step(0.0);
306 }
307 let mut no_nap = DCNNeuron::new();
310 no_nap.g_nap = 0.0;
311 let mut spikes_no = 0;
312 for _ in 0..20_000 {
313 spikes_no += no_nap.step(0.0);
314 }
315 assert!(
316 spikes >= spikes_no,
317 "INaP should contribute to spontaneous firing: with={spikes}, without={spikes_no}"
318 );
319 }
320
321 #[test]
322 fn dcn_rebound_burst() {
323 let mut n = DCNNeuron::new();
325 for _ in 0..2000 {
327 n.step(-5.0);
328 }
329 assert!(
330 n.s > 0.5,
331 "T-type must de-inactivate during hyperpolarisation, s={}",
332 n.s
333 );
334
335 let mut spikes = 0;
337 for _ in 0..200 {
338 spikes += n.step(3.0);
339 }
340 let mut n2 = DCNNeuron::new();
342 n2.s = 0.05; let mut spikes2 = 0;
344 for _ in 0..200 {
345 spikes2 += n2.step(3.0);
346 }
347 assert!(
348 spikes >= spikes2,
349 "De-inactivated T-type should facilitate rebound: rebound={spikes} vs inact={spikes2}"
350 );
351 }
352
353 #[test]
354 fn dcn_ih_depolarises() {
355 let mut with_ih = DCNNeuron::new();
357 with_ih.v = -80.0;
358 let mut no_ih = DCNNeuron::new();
359 no_ih.v = -80.0;
360 no_ih.g_h = 0.0;
361
362 for _ in 0..1000 {
363 with_ih.step(0.0);
364 no_ih.step(0.0);
365 }
366 assert!(
367 with_ih.v > no_ih.v,
368 "Ih should depolarise from hyperpolarised state: Ih={:.1} vs no_Ih={:.1}",
369 with_ih.v,
370 no_ih.v
371 );
372 }
373
374 #[test]
375 fn dcn_gate_and_calcium_kinetics_use_closed_form_relaxation() {
376 let mut n = DCNNeuron::new();
377 n.g_na = 0.0;
378 n.g_nap = 0.0;
379 n.g_k = 0.0;
380 n.g_t = 0.0;
381 n.g_ahp = 0.0;
382 n.g_h = 0.0;
383 n.g_l = 0.0;
384 n.gain = 0.0;
385 let (v0, h0, n0, p0, s0, r0, ca0) = (n.v, n.h, n.n, n.p, n.s, n.r, n.ca);
386 let alpha_h = 0.07 * (-(v0 + 58.0) / 20.0).exp();
387 let beta_h = 1.0 / (1.0 + (-(v0 + 28.0) / 10.0).exp());
388 let alpha_n = safe_rate(0.01, 34.0, v0, 10.0, 0.1);
389 let beta_n = 0.125 * (-(v0 + 44.0) / 80.0).exp();
390 let p_inf = 1.0 / (1.0 + (-(v0 + 48.0) / 5.0).exp());
391 let tau_p = 5.0 + 15.0 / (1.0 + ((v0 + 48.0) / 10.0).powi(2)).max(0.01);
392 let s_inf = 1.0 / (1.0 + ((v0 + 60.0) / 6.5).exp());
393 let tau_s = 20.0 + 50.0 / (1.0 + ((v0 + 65.0) / 10.0).exp());
394 let r_inf = 1.0 / (1.0 + ((v0 + 80.0) / 10.0).exp());
395 let tau_r = 100.0 + 200.0 / (1.0 + ((v0 + 70.0) / 10.0).exp());
396
397 n.step(0.0);
398
399 assert_close(n.v, v0);
400 assert_close(n.h, dcn_exact_hh_gate(h0, alpha_h, beta_h, n.phi, n.dt));
401 assert_close(n.n, dcn_exact_hh_gate(n0, alpha_n, beta_n, n.phi, n.dt));
402 assert_close(n.p, dcn_exact_relax(p0, p_inf, tau_p, n.dt));
403 assert_close(n.s, dcn_exact_relax(s0, s_inf, tau_s, n.dt));
404 assert_close(n.r, dcn_exact_relax(r0, r_inf, tau_r, n.dt));
405 assert_close(n.ca, dcn_exact_relax(ca0, 0.0, n.tau_ca, n.dt));
406 }
407
408 #[test]
409 fn dcn_negative_input_no_crash() {
410 let mut n = DCNNeuron::new();
411 for _ in 0..10_000 {
412 n.step(-100.0);
413 }
414 assert!(n.v.is_finite());
415 assert!(n.v >= -100.0);
416 }
417
418 #[test]
419 fn dcn_nan_input_stays_finite() {
420 let mut n = DCNNeuron::new();
421 let before = n.clone();
422 n.step(f64::NAN);
423 assert!(n.v.is_finite());
424 assert_eq!(n.v, before.v);
425 assert_eq!(n.ca, before.ca);
426 }
427
428 #[test]
429 fn dcn_extreme_input_bounded() {
430 let mut n = DCNNeuron::new();
431 for _ in 0..1000 {
432 n.step(1e6);
433 }
434 assert!(n.v.is_finite() && n.v <= 60.0);
435 }
436
437 #[test]
438 fn dcn_corrupted_state_preserved_on_step() {
439 let mut n = DCNNeuron::new();
440 n.h = -0.1;
441 let before_v = n.v;
442 let before_ca = n.ca;
443 assert_eq!(n.step(10.0), 0);
444 assert_eq!(n.v, before_v);
445 assert_eq!(n.ca, before_ca);
446 }
447
448 #[test]
449 fn dcn_reset_clears_state() {
450 let mut n = DCNNeuron::new();
451 for _ in 0..1000 {
452 n.step(10.0);
453 }
454 n.reset();
455 assert_eq!(n.v, -60.0);
456 assert_eq!(n.s, 0.8);
457 assert_eq!(n.r, 0.1);
458 }
459
460 #[test]
461 fn dcn_gates_bounded() {
462 let mut n = DCNNeuron::new();
463 for _ in 0..10_000 {
464 n.step(10.0);
465 }
466 for (name, val) in [("h", n.h), ("n", n.n), ("p", n.p), ("s", n.s), ("r", n.r)] {
467 assert!((0.0..=1.0).contains(&val), "{name} out of bounds: {val}");
468 }
469 assert!(n.ca >= 0.0, "Ca²⁺ must be non-negative: {}", n.ca);
470 }
471
472 #[test]
473 fn dcn_nap_increases_excitability() {
474 let mut with_nap = DCNNeuron::new();
476 let mut no_nap = DCNNeuron::new();
477 no_nap.g_nap = 0.0;
478 let mut spikes_with = 0;
479 let mut spikes_no = 0;
480 for _ in 0..5_000 {
481 spikes_with += with_nap.step(3.0);
482 spikes_no += no_nap.step(3.0);
483 }
484 assert!(
485 spikes_with >= spikes_no,
486 "INaP should increase excitability: with={spikes_with}, without={spikes_no}"
487 );
488 }
489
490 #[test]
491 fn dcn_ahp_limits_rate() {
492 let mut with_ahp = DCNNeuron::new();
494 let mut no_ahp = DCNNeuron::new();
495 no_ahp.g_ahp = 0.0;
496 let mut spikes_with = 0;
497 let mut spikes_no = 0;
498 for _ in 0..5_000 {
499 spikes_with += with_ahp.step(8.0);
500 spikes_no += no_ahp.step(8.0);
501 }
502 assert!(
503 spikes_no >= spikes_with,
504 "AHP removal should increase firing: with={spikes_with}, without={spikes_no}"
505 );
506 }
507
508 #[test]
509 fn dcn_ca_rises_during_spiking() {
510 let mut n = DCNNeuron::new();
511 let ca_init = n.ca;
512 for _ in 0..5_000 {
513 n.step(10.0);
514 }
515 assert!(
516 n.ca > ca_init,
517 "Ca²⁺ must rise during spiking: init={ca_init}, now={}",
518 n.ca
519 );
520 }
521
522 #[test]
523 fn dcn_has_seven_currents() {
524 let n = DCNNeuron::new();
526 assert!(n.g_na > 0.0, "Na_t missing");
527 assert!(n.g_nap > 0.0, "Na_p missing");
528 assert!(n.g_k > 0.0, "K_dr missing");
529 assert!(n.g_t > 0.0, "Ca_T missing");
530 assert!(n.g_ahp > 0.0, "AHP missing");
531 assert!(n.g_h > 0.0, "Ih missing");
532 assert!(n.g_l > 0.0, "Leak missing");
533 }
534
535 #[test]
536 fn dcn_performance_1k_steps() {
537 let start = std::time::Instant::now();
538 let mut n = DCNNeuron::new();
539 for _ in 0..1_000 {
540 std::hint::black_box(n.step(5.0));
541 }
542 let elapsed = start.elapsed();
543 assert!(
544 elapsed.as_millis() < 200,
545 "1k steps must complete in <200ms"
546 );
547 }
548
549 fn assert_close(observed: f64, expected: f64) {
550 assert!(
551 (observed - expected).abs() <= 1.0e-12,
552 "observed {:.17e}, expected {:.17e}",
553 observed,
554 expected,
555 );
556 }
557
558 #[test]
559 fn dcn_default_matches_constructor_contract() {
560 let default = DCNNeuron::default();
561 let constructed = DCNNeuron::new();
562 assert_eq!(default.v, constructed.v);
563 assert_eq!(default.s, constructed.s);
564 assert_eq!(default.ca, constructed.ca);
565 assert_eq!(default.dt, constructed.dt);
566 }
567}