sc_neurocore_engine/neurons/channels/
persistent_na.rs1use crate::neurons::biophysical::safe_rate;
10
11#[derive(Clone, Debug)]
24pub struct PersistentNaNeuron {
25 pub v: f64,
26 pub h: f64, pub n: f64, pub p: f64, pub g_na: f64, pub g_nap: f64, pub g_k: f64, pub g_l: f64,
34 pub e_na: f64,
36 pub e_k: f64,
37 pub e_l: f64,
38 pub c_m: f64,
39 pub phi: f64,
40 pub dt: f64,
41 pub v_threshold: f64,
42 pub gain: f64,
43}
44
45impl Default for PersistentNaNeuron {
46 fn default() -> Self {
47 Self::new()
48 }
49}
50
51impl PersistentNaNeuron {
52 pub fn new() -> Self {
53 Self {
54 v: -65.0,
55 h: 0.6,
56 n: 0.32,
57 p: 0.0,
58 g_na: 35.0,
59 g_nap: 0.15, g_k: 9.0,
61 g_l: 0.3, e_na: 55.0,
63 e_k: -90.0,
64 e_l: -65.0,
65 c_m: 1.0,
66 phi: 5.0,
67 dt: 0.5,
68 v_threshold: -20.0,
69 gain: 1.0,
70 }
71 }
72
73 fn valid(&self) -> bool {
74 let finite = [
75 self.v,
76 self.h,
77 self.n,
78 self.p,
79 self.g_na,
80 self.g_nap,
81 self.g_k,
82 self.g_l,
83 self.e_na,
84 self.e_k,
85 self.e_l,
86 self.c_m,
87 self.phi,
88 self.dt,
89 self.v_threshold,
90 self.gain,
91 ]
92 .into_iter()
93 .all(f64::is_finite);
94 finite
95 && (-100.0..=60.0).contains(&self.v)
96 && [self.h, self.n, self.p]
97 .into_iter()
98 .all(|gate| (0.0..=1.0).contains(&gate))
99 && (0.0..=200.0).contains(&self.g_na)
100 && (0.0..=20.0).contains(&self.g_nap)
101 && (0.0..=100.0).contains(&self.g_k)
102 && (0.0..=5.0).contains(&self.g_l)
103 && (30.0..=70.0).contains(&self.e_na)
104 && (-100.0..=-70.0).contains(&self.e_k)
105 && (-80.0..=-40.0).contains(&self.e_l)
106 && (0.5..=2.0).contains(&self.c_m)
107 && (0.5..=10.0).contains(&self.phi)
108 && self.dt > 0.0
109 && self.dt <= 1.0
110 && (-20.0..=20.0).contains(&self.v_threshold)
111 && (0.0..=10.0).contains(&self.gain)
112 }
113
114 pub fn try_step(&mut self, current: f64) -> Result<i32, &'static str> {
115 if !current.is_finite() {
116 return Err("current must be finite");
117 }
118 if !self.valid() {
119 return Err("PersistentNa state and parameters must satisfy the public bounds");
120 }
121
122 let mut candidate = self.clone();
123 let input = candidate.gain * current;
124 let sub_steps = 50;
125 let sub_dt = candidate.dt / sub_steps as f64;
126 let mut fired = 0i32;
127
128 for _ in 0..sub_steps {
129 let v = candidate.v;
130
131 let alpha_m = safe_rate(0.1, 35.0, v, 10.0, 1.0);
133 let beta_m = 4.0 * (-(v + 60.0) / 18.0).exp();
134 let m_inf = alpha_m / (alpha_m + beta_m);
135
136 let alpha_h = 0.07 * (-(v + 58.0) / 20.0).exp();
137 let beta_h = 1.0 / (1.0 + (-(v + 28.0) / 10.0).exp());
138
139 let alpha_n = safe_rate(0.01, 34.0, v, 10.0, 0.1);
140 let beta_n = 0.125 * (-(v + 44.0) / 80.0).exp();
141
142 let p_inf = 1.0 / (1.0 + (-(v + 48.0) / 5.0).exp());
145 let tau_p = 10.0 + 40.0 / (1.0 + ((v + 48.0) / 10.0).powi(2));
146
147 candidate.h +=
149 sub_dt * candidate.phi * (alpha_h * (1.0 - candidate.h) - beta_h * candidate.h);
150 candidate.n +=
151 sub_dt * candidate.phi * (alpha_n * (1.0 - candidate.n) - beta_n * candidate.n);
152 candidate.p += sub_dt * (p_inf - candidate.p) / tau_p;
153
154 let i_na = candidate.g_na * m_inf.powi(3) * candidate.h * (v - candidate.e_na);
156 let i_nap = candidate.g_nap * candidate.p * (v - candidate.e_na);
157 let i_k = candidate.g_k * candidate.n.powi(4) * (v - candidate.e_k);
158 let i_l = candidate.g_l * (v - candidate.e_l);
159
160 let dv = (-i_na - i_nap - i_k - i_l + input) / candidate.c_m;
161 candidate.v += sub_dt * dv;
162 if ![candidate.v, candidate.h, candidate.n, candidate.p]
163 .into_iter()
164 .all(f64::is_finite)
165 {
166 return Err("PersistentNa candidate state became non-finite");
167 }
168
169 if candidate.v >= candidate.v_threshold {
170 fired = 1;
171 candidate.v = -65.0;
172 }
173 }
174
175 candidate.v = candidate.v.clamp(-100.0, 60.0);
176 candidate.h = candidate.h.clamp(0.0, 1.0);
177 candidate.n = candidate.n.clamp(0.0, 1.0);
178 candidate.p = candidate.p.clamp(0.0, 1.0);
179 *self = candidate;
180
181 Ok(fired)
182 }
183
184 pub fn step(&mut self, current: f64) -> i32 {
185 self.try_step(current).unwrap_or(0)
186 }
187
188 pub fn reset(&mut self) {
189 self.v = -65.0;
190 self.h = 0.6;
191 self.n = 0.32;
192 self.p = 0.0;
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
203 fn nap_fires_with_input() {
204 let mut n = PersistentNaNeuron::new();
205 let mut spikes = 0;
206 for _ in 0..2_000 {
207 spikes += n.step(2.0);
208 }
209 assert!(spikes > 5, "NaP neuron must fire with input, got {spikes}");
210 }
211
212 #[test]
213 fn nap_subthreshold_oscillations() {
214 let mut n = PersistentNaNeuron::new();
218 let mut spikes_inhibited = 0;
219 for _ in 0..10_000 {
220 spikes_inhibited += n.step(-2.0);
221 }
222 assert_eq!(
223 spikes_inhibited, 0,
224 "INaP neuron must be silent with inhibitory input, got {spikes_inhibited}"
225 );
226 }
227
228 #[test]
229 fn nap_lowers_threshold() {
230 let mut with_nap = PersistentNaNeuron::new();
232 let mut no_nap = PersistentNaNeuron::new();
233 no_nap.g_nap = 0.0;
234
235 let input = 1.0;
237 let mut spikes_nap = 0;
238 let mut spikes_no = 0;
239 for _ in 0..10_000 {
240 spikes_nap += with_nap.step(input);
241 spikes_no += no_nap.step(input);
242 }
243 assert!(
244 spikes_nap >= spikes_no,
245 "INaP must lower effective threshold: NaP={spikes_nap} vs none={spikes_no}"
246 );
247 }
248
249 #[test]
250 fn nap_p_gate_activates_at_subthreshold() {
251 let mut n = PersistentNaNeuron::new();
253 n.v = -50.0;
254 for _ in 0..1000 {
256 let _ = n.step(0.0);
258 }
259 assert!(
262 n.p > 0.01,
263 "p gate must activate at subthreshold voltages, p={}",
264 n.p
265 );
266 }
267
268 #[test]
269 fn nap_increases_firing_rate() {
270 let mut low = PersistentNaNeuron::new();
272 low.g_nap = 0.2;
273 let mut high = PersistentNaNeuron::new();
274 high.g_nap = 1.5;
275
276 let input = 1.5;
277 let mut spikes_low = 0;
278 let mut spikes_high = 0;
279 for _ in 0..10_000 {
280 spikes_low += low.step(input);
281 spikes_high += high.step(input);
282 }
283 assert!(
284 spikes_high >= spikes_low,
285 "Higher g_nap must increase firing: high={spikes_high} vs low={spikes_low}"
286 );
287 }
288
289 #[test]
290 fn nap_negative_input_no_crash() {
291 let mut n = PersistentNaNeuron::new();
292 for _ in 0..10_000 {
293 n.step(-100.0);
294 }
295 assert!(n.v.is_finite());
296 assert!(n.v >= -100.0);
297 }
298
299 #[test]
300 fn nap_nan_input_is_rejected_atomically() {
301 let mut n = PersistentNaNeuron::new();
302 let before = n.clone();
303 assert!(n.try_step(f64::NAN).is_err());
304 assert_eq!(n.v, before.v);
305 assert_eq!(n.h, before.h);
306 assert_eq!(n.n, before.n);
307 assert_eq!(n.p, before.p);
308 }
309
310 #[test]
311 fn nap_invalid_configuration_is_rejected_atomically() {
312 let mut n = PersistentNaNeuron::new();
313 n.c_m = 0.0;
314 let before = n.clone();
315 assert!(n.try_step(1.0).is_err());
316 assert_eq!(n.v, before.v);
317 assert_eq!(n.c_m, before.c_m);
318 }
319
320 #[test]
321 fn nap_extreme_input_bounded() {
322 let mut n = PersistentNaNeuron::new();
323 for _ in 0..1000 {
324 n.step(1e6);
325 }
326 assert!(n.v.is_finite() && n.v <= 60.0);
327 }
328
329 #[test]
330 fn nap_reset_clears_state() {
331 let mut n = PersistentNaNeuron::new();
332 n.g_nap = 0.3;
333 for _ in 0..1000 {
334 n.step(10.0);
335 }
336 n.reset();
337 assert_eq!(n.v, -65.0);
338 assert_eq!(n.p, 0.0);
339 assert_eq!(n.h, 0.6);
340 assert_eq!(n.g_nap, 0.3);
341 }
342
343 #[test]
344 fn nap_gates_bounded() {
345 let mut n = PersistentNaNeuron::new();
346 for _ in 0..10_000 {
347 n.step(10.0);
348 }
349 assert!(n.h >= 0.0 && n.h <= 1.0);
350 assert!(n.n >= 0.0 && n.n <= 1.0);
351 assert!(n.p >= 0.0 && n.p <= 1.0);
352 }
353
354 #[test]
355 fn nap_performance_1k_steps() {
356 let start = std::time::Instant::now();
357 let mut n = PersistentNaNeuron::new();
358 for _ in 0..1_000 {
359 std::hint::black_box(n.step(5.0));
360 }
361 let elapsed = start.elapsed();
362 assert!(
363 elapsed.as_millis() < 200,
364 "1k steps must complete in <200ms"
365 );
366 }
367}