sc_neurocore_engine/neurons/simple_spiking/
chay.rs1#[derive(Clone, Debug)]
13pub struct ChayNeuron {
14 pub v: f64,
15 pub n: f64,
16 pub ca: f64,
17 pub g_ca: f64,
18 pub g_k: f64,
19 pub g_kca: f64,
20 pub g_l: f64,
21 pub e_ca: f64,
22 pub e_k: f64,
23 pub e_l: f64,
24 pub rho: f64,
25 pub alpha_ca: f64,
26 pub k_ca: f64,
27 pub dt: f64,
28 pub v_threshold: f64,
29}
30
31impl ChayNeuron {
32 pub fn new() -> Self {
33 Self {
34 v: -50.0,
35 n: 0.1,
36 ca: 0.1,
37 g_ca: 25.0,
38 g_k: 1400.0,
39 g_kca: 12.0,
40 g_l: 7.0,
41 e_ca: 100.0,
42 e_k: -75.0,
43 e_l: -40.0,
44 rho: 0.00015,
45 alpha_ca: 0.002,
46 k_ca: 0.04,
47 dt: 0.02,
48 v_threshold: -20.0,
49 }
50 }
51 pub fn step(&mut self, current: f64) -> i32 {
52 if !current.is_finite() || !self.dt.is_finite() || self.dt <= 0.0 {
53 return 0;
54 }
55
56 let v_initial = self.v;
57 let mut v = self.v;
58 let mut n = self.n;
59 let mut ca = self.ca;
60 let substeps = (self.dt / 0.001_f64).ceil().max(1.0) as usize;
61 let h = self.dt / substeps as f64;
62 let mut crossed = false;
63
64 for _ in 0..substeps {
65 let m_inf = 1.0 / (1.0 + (-(v + 25.0) / 8.0).clamp(-700.0, 700.0).exp());
66 let n_inf = 1.0 / (1.0 + (-(v + 18.0) / 14.0).clamp(-700.0, 700.0).exp());
67 let d = (v + 18.0).abs().max(0.01);
68 let tau_n = 1.0 / (0.01 * d);
69 let ca_denominator = ca + 1.0;
70 if ca_denominator <= 0.0 {
71 return 0;
72 }
73 let kca_act = ca / ca_denominator;
74 let i_ca = self.g_ca * m_inf * (v - self.e_ca);
75 let i_k = self.g_k * n * (v - self.e_k);
76 let i_kca = self.g_kca * kca_act * (v - self.e_k);
77 let i_l = self.g_l * (v - self.e_l);
78
79 let v_next = v + (-i_ca - i_k - i_kca - i_l + current) * h;
80 let n_next = n + (n_inf - n) / tau_n.max(0.01) * h;
81 let ca_next = ca + self.rho * (-self.alpha_ca * i_ca - self.k_ca * ca) * h;
82 if !v_next.is_finite()
83 || !n_next.is_finite()
84 || !ca_next.is_finite()
85 || !(-200.0..=200.0).contains(&v_next)
86 || !(0.0..=1.0).contains(&n_next)
87 || !(0.0..=100.0).contains(&ca_next)
88 {
89 return 0;
90 }
91 crossed = crossed || (v_next >= self.v_threshold && v < self.v_threshold);
92 v = v_next;
93 n = n_next;
94 ca = ca_next;
95 }
96
97 self.v = v;
98 self.n = n;
99 self.ca = ca;
100 if crossed && v_initial < self.v_threshold {
101 1
102 } else {
103 0
104 }
105 }
106 pub fn reset(&mut self) {
107 self.v = -50.0;
108 self.n = 0.1;
109 self.ca = 0.1;
110 }
111}
112impl Default for ChayNeuron {
113 fn default() -> Self {
114 Self::new()
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn default_matches_constructor_state() {
124 let default = ChayNeuron::default();
125 let constructed = ChayNeuron::new();
126 assert_eq!(default.v, constructed.v);
127 }
128
129 #[test]
130 fn chay_drive_changes_state_without_leaving_physical_bounds() {
131 let mut rest = ChayNeuron::new();
132 let mut driven = ChayNeuron::new();
133 for _ in 0..500 {
134 rest.step(0.0);
135 driven.step(5.0);
136 }
137 assert!(driven.v > rest.v);
138 assert!((0.0..=1.0).contains(&driven.n));
139 assert!(driven.ca >= 0.0);
140 }
141
142 #[test]
143 fn chay_reset_clears_state() {
144 let mut n = ChayNeuron::new();
145 for _ in 0..1000 {
146 n.step(20.0);
147 }
148 n.reset();
149 assert!((n.v - (-50.0)).abs() < 1e-10);
150 }
151
152 #[test]
153 fn chay_bounded() {
154 let mut n = ChayNeuron::new();
155 for _ in 0..5000 {
156 n.step(200.0);
157 }
158 assert!(n.v.is_finite());
159 }
160
161 #[test]
162 fn chay_ca_nonneg() {
163 let mut n = ChayNeuron::new();
164 for _ in 0..5000 {
165 n.step(20.0);
166 }
167 assert!(n.ca >= 0.0, "Ca²⁺ must be non-negative");
168 }
169
170 #[test]
171 fn chay_nan_no_panic() {
172 ChayNeuron::new().step(f64::NAN);
173 }
174
175 #[test]
176 fn chay_negative_no_crash() {
177 let mut n = ChayNeuron::new();
178 for _ in 0..500 {
179 n.step(-10.0);
180 }
181 assert!(n.v.is_finite());
182 }
183}