1use super::super::biophysical::safe_rate;
10
11#[derive(Clone, Debug)]
38pub struct GolgiCell {
39 pub v: f64,
40 pub m: f64, pub h: f64, pub p_na: f64, pub n: f64, pub a: f64, pub b: f64, pub w: f64, pub m_t: f64, pub s: f64, pub c_n: f64, pub r: f64, pub ca: f64, pub g_na_t: f64,
54 pub g_na_p: f64,
55 pub g_kdr: f64,
56 pub g_ka: f64,
57 pub g_km: f64,
58 pub g_cat: f64,
59 pub g_can: f64,
60 pub g_bk: f64,
61 pub g_sk: f64,
62 pub g_h: f64,
63 pub g_l: f64,
64 pub e_na: f64,
66 pub e_k: f64,
67 pub e_ca: f64,
68 pub e_h: f64,
69 pub e_l: f64,
70 pub c_m: f64,
71 pub tau_ca: f64,
72 pub kd_bk: f64,
73 pub kd_sk: f64,
74 pub dt: f64,
75 pub sub_steps: usize,
76 pub gain: f64,
77}
78
79impl Default for GolgiCell {
80 fn default() -> Self {
81 Self::new()
82 }
83}
84
85impl GolgiCell {
86 pub fn new() -> Self {
87 Self {
88 v: -60.0,
89 m: 0.02,
90 h: 0.85,
91 p_na: 0.01,
92 n: 0.05,
93 a: 0.1,
94 b: 0.8,
95 w: 0.01,
96 m_t: 0.01,
97 s: 0.9,
98 c_n: 0.01,
99 r: 0.1,
100 ca: 0.05,
101 g_na_t: 48.0, g_na_p: 0.2, g_kdr: 16.0,
104 g_ka: 8.0, g_km: 1.0, g_cat: 0.5, g_can: 1.0, g_bk: 3.0, g_sk: 1.0, g_h: 0.1, g_l: 0.05,
112 e_na: 55.0,
113 e_k: -90.0,
114 e_ca: 120.0,
115 e_h: -40.0,
116 e_l: -55.0, c_m: 1.0,
118 tau_ca: 200.0,
119 kd_bk: 1.0,
120 kd_sk: 0.5,
121 dt: 0.5,
122 sub_steps: 10,
123 gain: 1.0,
124 }
125 }
126
127 #[inline]
128 fn boltz(v: f64, vh: f64, k: f64) -> f64 {
129 let x = (v - vh) / k;
130 if x >= 0.0 {
131 1.0 / (1.0 + (-x).exp())
132 } else {
133 let ex = x.exp();
134 ex / (1.0 + ex)
135 }
136 }
137
138 #[inline]
139 fn voltage_valid(value: f64) -> bool {
140 value.is_finite() && (-100.0..=60.0).contains(&value)
141 }
142
143 #[inline]
144 fn probability(value: f64) -> bool {
145 value.is_finite() && (0.0..=1.0).contains(&value)
146 }
147
148 #[inline]
149 fn gate_alpha_beta(previous: f64, alpha: f64, beta: f64, phi: f64, dt: f64) -> Option<f64> {
150 let total = phi * (alpha + beta);
151 if !previous.is_finite()
152 || !alpha.is_finite()
153 || !beta.is_finite()
154 || !total.is_finite()
155 || !dt.is_finite()
156 || total <= 0.0
157 {
158 return None;
159 }
160 let steady = alpha / (alpha + beta);
161 Some((steady + (previous - steady) * (-total * dt).exp()).clamp(0.0, 1.0))
162 }
163
164 #[inline]
165 fn gate_inf(previous: f64, steady: f64, tau: f64, dt: f64) -> Option<f64> {
166 if !previous.is_finite()
167 || !steady.is_finite()
168 || !tau.is_finite()
169 || !dt.is_finite()
170 || tau <= 0.0
171 {
172 return None;
173 }
174 Some((steady + (previous - steady) * (-dt / tau).exp()).clamp(0.0, 1.0))
175 }
176
177 #[inline]
178 fn calcium_exact(previous: f64, entry: f64, tau: f64, dt: f64) -> Option<f64> {
179 if !previous.is_finite()
180 || !entry.is_finite()
181 || !tau.is_finite()
182 || !dt.is_finite()
183 || tau <= 0.0
184 || previous < 0.0
185 {
186 return None;
187 }
188 let steady = entry * tau;
189 let value = steady + (previous - steady) * (-dt / tau).exp();
190 value.is_finite().then_some(value.max(0.0))
191 }
192
193 fn valid_state(&self) -> bool {
194 Self::voltage_valid(self.v)
195 && [
196 self.m, self.h, self.p_na, self.n, self.a, self.b, self.w, self.m_t, self.s,
197 self.c_n, self.r,
198 ]
199 .into_iter()
200 .all(Self::probability)
201 && [
202 self.g_na_t,
203 self.g_na_p,
204 self.g_kdr,
205 self.g_ka,
206 self.g_km,
207 self.g_cat,
208 self.g_can,
209 self.g_bk,
210 self.g_sk,
211 self.g_h,
212 self.g_l,
213 ]
214 .into_iter()
215 .all(|g| g.is_finite() && g >= 0.0)
216 && self.ca.is_finite()
217 && self.ca >= 0.0
218 && self.e_na.is_finite()
219 && self.e_k.is_finite()
220 && self.e_ca.is_finite()
221 && self.e_h.is_finite()
222 && self.e_l.is_finite()
223 && self.c_m.is_finite()
224 && self.tau_ca.is_finite()
225 && self.kd_bk.is_finite()
226 && self.kd_sk.is_finite()
227 && self.dt.is_finite()
228 && self.gain.is_finite()
229 && self.c_m > 0.0
230 && self.tau_ca > 0.0
231 && self.kd_bk > 0.0
232 && self.kd_sk > 0.0
233 && self.dt > 0.0
234 && self.sub_steps > 0
235 && self.gain >= 0.0
236 }
237
238 pub fn step(&mut self, current: f64) -> i32 {
239 if !current.is_finite() || !self.valid_state() {
240 return 0;
241 }
242
243 let input = self.gain * current;
244 let dt_sub = self.dt / self.sub_steps as f64;
245 let v_prev = self.v;
246 let mut next = self.clone();
247
248 for _ in 0..self.sub_steps {
249 let v = next.v;
250
251 let alpha_m = safe_rate(0.1, 35.0, v, 10.0, 1.0);
253 let beta_m = 4.0 * (-(v + 60.0) / 18.0).exp();
254 let alpha_h = 0.07 * (-(v + 58.0) / 20.0).exp();
255 let beta_h = 1.0 / (1.0 + (-(v + 28.0) / 10.0).exp());
256 let Some(m) = Self::gate_alpha_beta(next.m, alpha_m, beta_m, 5.0, dt_sub) else {
257 return 0;
258 };
259 let Some(h) = Self::gate_alpha_beta(next.h, alpha_h, beta_h, 5.0, dt_sub) else {
260 return 0;
261 };
262
263 let pna_inf = Self::boltz(v, -48.0, 5.0);
265 let tau_pna = 5.0 + 20.0 / (1.0 + ((v + 48.0) / 10.0).powi(2)).max(0.01);
266 let Some(p_na) = Self::gate_inf(next.p_na, pna_inf, tau_pna, dt_sub) else {
267 return 0;
268 };
269
270 let alpha_n = safe_rate(0.01, 34.0, v, 10.0, 0.1);
272 let beta_n = 0.125 * (-(v + 44.0) / 80.0).exp();
273 let Some(n) = Self::gate_alpha_beta(next.n, alpha_n, beta_n, 5.0, dt_sub) else {
274 return 0;
275 };
276
277 let a_inf = Self::boltz(v, -27.0, 16.0);
279 let b_inf = Self::boltz(v, -80.0, -6.0);
280 let Some(a) = Self::gate_inf(next.a, a_inf, 2.0, dt_sub) else {
281 return 0;
282 };
283 let Some(b) = Self::gate_inf(next.b, b_inf, 15.0, dt_sub) else {
284 return 0;
285 };
286
287 let w_inf = Self::boltz(v, -35.0, 10.0);
289 let tau_w = 100.0 / (3.3 * ((v + 35.0) / 20.0).exp() + (-(v + 35.0) / 20.0).exp());
290 let Some(w) = Self::gate_inf(next.w, w_inf, tau_w, dt_sub) else {
291 return 0;
292 };
293
294 let mt_inf = Self::boltz(v, -52.0, 5.0);
296 let s_inf = Self::boltz(v, -60.0, -6.5);
297 let tau_s = 20.0 + 50.0 / (1.0 + ((v + 65.0) / 10.0).powi(2)).max(0.01);
298 let Some(m_t) = Self::gate_inf(next.m_t, mt_inf, 1.0, dt_sub) else {
299 return 0;
300 };
301 let Some(s) = Self::gate_inf(next.s, s_inf, tau_s, dt_sub) else {
302 return 0;
303 };
304
305 let cn_inf = Self::boltz(v, -20.0, 5.0);
307 let tau_cn = 2.0 + 10.0 / (1.0 + ((v + 20.0) / 10.0).powi(2)).max(0.01);
308 let Some(c_n) = Self::gate_inf(next.c_n, cn_inf, tau_cn, dt_sub) else {
309 return 0;
310 };
311
312 let r_inf = Self::boltz(v, -80.0, -10.0);
314 let tau_r = 50.0 + 200.0 / (1.0 + ((v + 80.0) / 20.0).powi(2)).max(0.01);
315 let Some(r) = Self::gate_inf(next.r, r_inf, tau_r, dt_sub) else {
316 return 0;
317 };
318
319 let g_cat = self.g_cat * m_t.powi(2) * s;
321 let g_can = self.g_can * c_n.powi(2);
322 let i_cat = g_cat * (v - self.e_ca);
323 let i_can = g_can * (v - self.e_ca);
324 let ca_entry = if i_cat + i_can < 0.0 {
325 -(i_cat + i_can) * 0.001
326 } else {
327 0.0
328 };
329 let Some(ca) = Self::calcium_exact(next.ca, ca_entry, self.tau_ca, dt_sub) else {
330 return 0;
331 };
332
333 let ca2 = ca * ca;
336 let kd2 = self.kd_bk * self.kd_bk;
337 let bk_v = Self::boltz(v, 100.0 - 120.0 * ca2 / (ca2 + kd2), 15.0);
338 let sk_inf = ca2 / (ca2 + self.kd_sk.powi(2));
340
341 let g_na = self.g_na_t * m.powi(3) * h + self.g_na_p * p_na;
343 let g_k = self.g_kdr * n.powi(4)
344 + self.g_ka * a.powi(3) * b
345 + self.g_km * w
346 + self.g_bk * bk_v
347 + self.g_sk * sk_inf;
348 let g_ca = g_cat + g_can;
349 let g_h = self.g_h * r;
350 let g_total = g_na + g_k + g_ca + g_h + self.g_l;
351 if !g_total.is_finite() || g_total <= 0.0 {
352 return 0;
353 }
354 let steady_v = (input
355 + g_na * self.e_na
356 + g_k * self.e_k
357 + g_ca * self.e_ca
358 + g_h * self.e_h
359 + self.g_l * self.e_l)
360 / g_total;
361 let v_next = steady_v + (v - steady_v) * (-(g_total / self.c_m) * dt_sub).exp();
362 if !Self::voltage_valid(v_next) || !ca.is_finite() || ca < 0.0 {
363 return 0;
364 }
365
366 next.v = v_next;
367 next.m = m;
368 next.h = h;
369 next.p_na = p_na;
370 next.n = n;
371 next.a = a;
372 next.b = b;
373 next.w = w;
374 next.m_t = m_t;
375 next.s = s;
376 next.c_n = c_n;
377 next.r = r;
378 next.ca = ca;
379 }
380
381 *self = next;
382
383 if self.v >= 0.0 && v_prev < 0.0 {
385 1
386 } else {
387 0
388 }
389 }
390
391 pub fn reset(&mut self) {
392 *self = Self::new();
393 }
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399
400 #[test]
403 fn golgi_fires_with_input() {
404 let mut n = GolgiCell::new();
405 let mut spikes = 0;
406 for _ in 0..10_000 {
407 spikes += n.step(15.0);
408 }
409 assert!(
410 spikes > 10,
411 "Golgi cell must fire with excitatory input, got {spikes}"
412 );
413 }
414
415 #[test]
416 fn golgi_spontaneous_firing() {
417 let mut n = GolgiCell::new();
419 let _spikes: i32 = (0..20_000).map(|_| n.step(0.0)).sum();
420 let mut n2 = GolgiCell::new();
423 let mut spikes_small = 0;
424 for _ in 0..20_000 {
425 spikes_small += n2.step(0.5);
426 }
427 assert!(
428 spikes_small > 0,
429 "Golgi cell should fire with minimal input (near-threshold), got {spikes_small}"
430 );
431 }
432
433 #[test]
434 fn golgi_ahp_reduces_rate_at_high_drive() {
435 let mut with_ahp = GolgiCell::new();
437 let mut no_ahp = GolgiCell::new();
438 no_ahp.g_bk = 0.0;
439 no_ahp.g_sk = 0.0;
440 let mut spikes_with = 0;
441 let mut spikes_no = 0;
442 for _ in 0..10_000 {
443 spikes_with += with_ahp.step(10.0);
444 spikes_no += no_ahp.step(10.0);
445 }
446 assert!(
447 spikes_no >= spikes_with,
448 "AHP removal should increase firing: with={spikes_with}, without={spikes_no}"
449 );
450 }
451
452 #[test]
453 fn golgi_ka_is_transient() {
454 let mut with_a = GolgiCell::new();
457 let mut no_a = GolgiCell::new();
458 no_a.g_ka = 0.0;
459 let mut spikes_with = 0;
460 let mut spikes_no = 0;
461 for _ in 0..10_000 {
462 spikes_with += with_a.step(5.0);
463 spikes_no += no_a.step(5.0);
464 }
465 assert!(spikes_with > 0, "Must fire with K_A");
467 assert!(
469 spikes_with != spikes_no,
470 "K_A should affect firing rate: with={spikes_with}, without={spikes_no}"
471 );
472 }
473
474 #[test]
475 fn golgi_ca_accumulates_during_spiking() {
476 let mut n = GolgiCell::new();
477 let ca_init = n.ca;
478 for _ in 0..5000 {
479 n.step(10.0);
480 }
481 assert!(
482 n.ca > ca_init,
483 "Ca²⁺ must rise during spiking: init={ca_init}, now={}",
484 n.ca
485 );
486 }
487
488 #[test]
489 fn golgi_negative_input_no_crash() {
490 let mut n = GolgiCell::new();
491 for _ in 0..10_000 {
492 n.step(-100.0);
493 }
494 assert!(n.v.is_finite(), "Must stay finite with negative input");
495 assert!(n.v >= -100.0);
496 }
497
498 #[test]
499 fn golgi_nan_input_stays_finite() {
500 let mut n = GolgiCell::new();
501 n.step(f64::NAN);
502 assert!(n.v.is_finite(), "NaN input must not corrupt state");
503 }
504
505 #[test]
506 fn golgi_extreme_input_bounded() {
507 let mut n = GolgiCell::new();
508 for _ in 0..1000 {
509 n.step(1e6);
510 }
511 assert!(
512 n.v.is_finite() && n.v <= 60.0,
513 "Extreme input must stay bounded"
514 );
515 }
516
517 #[test]
518 fn golgi_reset_clears_state() {
519 let mut n = GolgiCell::new();
520 for _ in 0..5000 {
521 n.step(10.0);
522 }
523 n.reset();
524 let fresh = GolgiCell::new();
525 assert_eq!(n.v, fresh.v);
526 assert_eq!(n.ca, fresh.ca);
527 assert_eq!(n.m, fresh.m);
528 assert_eq!(n.h, fresh.h);
529 assert_eq!(n.p_na, fresh.p_na);
530 assert_eq!(n.w, fresh.w);
531 assert_eq!(n.r, fresh.r);
532 }
533
534 #[test]
535 fn golgi_gates_bounded() {
536 let mut n = GolgiCell::new();
537 for _ in 0..10_000 {
538 n.step(15.0);
539 }
540 for (name, val) in [
542 ("m", n.m),
543 ("h", n.h),
544 ("p_na", n.p_na),
545 ("n", n.n),
546 ("a", n.a),
547 ("b", n.b),
548 ("w", n.w),
549 ("m_t", n.m_t),
550 ("s", n.s),
551 ("c_n", n.c_n),
552 ("r", n.r),
553 ] {
554 assert!((0.0..=1.0).contains(&val), "{name} out of bounds: {val}");
555 }
556 assert!(n.ca >= 0.0, "Ca²⁺ must be non-negative: {}", n.ca);
557 }
558
559 #[test]
560 fn golgi_has_eleven_currents() {
561 let n = GolgiCell::new();
563 assert!(n.g_na_t > 0.0, "Na_t missing");
564 assert!(n.g_na_p > 0.0, "Na_p missing");
565 assert!(n.g_kdr > 0.0, "K_dr missing");
566 assert!(n.g_ka > 0.0, "K_A missing");
567 assert!(n.g_km > 0.0, "K_M missing");
568 assert!(n.g_cat > 0.0, "Ca_T missing");
569 assert!(n.g_can > 0.0, "Ca_N missing");
570 assert!(n.g_bk > 0.0, "BK missing");
571 assert!(n.g_sk > 0.0, "SK missing");
572 assert!(n.g_h > 0.0, "Ih missing");
573 assert!(n.g_l > 0.0, "Leak missing");
574 }
575
576 #[test]
577 fn golgi_persistent_na_depolarises() {
578 let mut with_nap = GolgiCell::new();
580 let mut no_nap = GolgiCell::new();
581 no_nap.g_na_p = 0.0;
582 let mut spikes_with = 0;
583 let mut spikes_no = 0;
584 for _ in 0..10_000 {
585 spikes_with += with_nap.step(2.0);
586 spikes_no += no_nap.step(2.0);
587 }
588 assert!(
589 spikes_with >= spikes_no,
590 "Na_p should increase excitability: with={spikes_with} vs without={spikes_no}"
591 );
592 }
593
594 #[test]
595 fn golgi_km_modulates_firing_pattern() {
596 let mut with_km = GolgiCell::new();
601 let mut no_km = GolgiCell::new();
602 no_km.g_km = 0.0;
603 let mut spikes_with = 0;
604 let mut spikes_no = 0;
605 for _ in 0..10_000 {
606 spikes_with += with_km.step(10.0);
607 spikes_no += no_km.step(10.0);
608 }
609 assert!(spikes_with > 0, "Golgi cell with K_M should fire");
610 assert!(spikes_no > 0, "Golgi cell without K_M should fire");
611 assert!(
612 spikes_with != spikes_no,
613 "K_M should measurably modulate firing: with_km={spikes_with}, without={spikes_no}"
614 );
615 }
616
617 #[test]
618 fn golgi_ih_sag() {
619 let mut with_h = GolgiCell::new();
621 let mut no_h = GolgiCell::new();
622 no_h.g_h = 0.0;
623 for _ in 0..10_000 {
625 with_h.step(-1.0);
626 no_h.step(-1.0);
627 }
628 assert!(
630 with_h.v > no_h.v,
631 "Ih should cause sag (less hyperpolarised): with_h={:.1} vs no_h={:.1}",
632 with_h.v,
633 no_h.v
634 );
635 }
636
637 #[test]
638 fn golgi_bk_fast_ahp() {
639 let mut with_bk = GolgiCell::new();
641 let mut no_bk = GolgiCell::new();
642 no_bk.g_bk = 0.0;
643 let mut spikes_with = 0;
645 let mut spikes_no = 0;
646 for _ in 0..10_000 {
647 spikes_with += with_bk.step(10.0);
648 spikes_no += no_bk.step(10.0);
649 }
650 assert!(
652 spikes_with > 0 && spikes_no > 0,
653 "Both should fire: with_bk={spikes_with}, no_bk={spikes_no}"
654 );
655 }
656
657 #[test]
658 fn golgi_sk_slow_adaptation() {
659 let mut with_sk = GolgiCell::new();
661 let mut no_sk = GolgiCell::new();
662 no_sk.g_sk = 0.0;
663 let mut spikes_with = 0;
664 let mut spikes_no = 0;
665 for _ in 0..20_000 {
666 spikes_with += with_sk.step(8.0);
667 spikes_no += no_sk.step(8.0);
668 }
669 assert!(
670 spikes_no >= spikes_with,
671 "SK removal should increase firing: with_sk={spikes_with}, no_sk={spikes_no}"
672 );
673 }
674
675 #[test]
676 fn golgi_performance_1k_steps() {
677 let start = std::time::Instant::now();
678 let mut n = GolgiCell::new();
679 for _ in 0..1_000 {
680 std::hint::black_box(n.step(5.0));
681 }
682 let elapsed = start.elapsed();
683 assert!(
684 elapsed.as_millis() < 50,
685 "1k steps must complete in <50ms, took {}ms",
686 elapsed.as_millis()
687 );
688 }
689
690 #[test]
691 fn golgi_default_matches_constructor_contract() {
692 let default = GolgiCell::default();
693 let constructed = GolgiCell::new();
694 assert_eq!(default.v, constructed.v);
695 assert_eq!(default.ca, constructed.ca);
696 assert_eq!(default.g_na_t, constructed.g_na_t);
697 assert_eq!(default.sub_steps, constructed.sub_steps);
698 }
699}