1use super::super::biophysical::safe_rate;
10
11#[derive(Clone, Debug)]
28pub struct StellateCell {
29 pub v: f64,
30 pub h: f64, pub n: f64, pub p: f64, pub g_na: f64,
35 pub g_k: f64,
36 pub g_kv3: f64,
37 pub g_l: f64,
38 pub e_na: f64,
40 pub e_k: f64,
41 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 StellateCell {
50 fn default() -> Self {
51 Self::new()
52 }
53}
54
55impl StellateCell {
56 pub fn new() -> Self {
57 Self {
58 v: -65.0,
59 h: 0.6,
60 n: 0.32,
61 p: 0.0,
62 g_na: 35.0,
63 g_k: 9.0,
64 g_kv3: 3.0, g_l: 0.1,
66 e_na: 55.0,
67 e_k: -90.0,
68 e_l: -65.0,
69 c_m: 0.5, phi: 5.0,
71 dt: 0.5,
72 v_threshold: -20.0,
73 gain: 1.0,
74 }
75 }
76
77 #[inline]
78 fn safe_exp(value: f64) -> f64 {
79 value.clamp(-60.0, 60.0).exp()
80 }
81
82 #[inline]
83 fn boltz(v: f64, vh: f64, k: f64) -> f64 {
84 let z = -(v - vh) / k;
85 if z > 60.0 {
86 0.0
87 } else if z < -60.0 {
88 1.0
89 } else {
90 1.0 / (1.0 + z.exp())
91 }
92 }
93
94 #[inline]
95 fn exact_relax(value: f64, target: f64, tau: f64, dt: f64) -> f64 {
96 if tau <= f64::EPSILON {
97 target.clamp(0.0, 1.0)
98 } else {
99 (target + (value - target) * (-dt / tau).exp()).clamp(0.0, 1.0)
100 }
101 }
102
103 #[inline]
104 fn exact_hh_gate(value: f64, alpha: f64, beta: f64, phi: f64, dt: f64) -> f64 {
105 let total = alpha + beta;
106 if total <= f64::EPSILON {
107 value.clamp(0.0, 1.0)
108 } else {
109 let steady = alpha / total;
110 (steady + (value - steady) * (-phi * total * dt).exp()).clamp(0.0, 1.0)
111 }
112 }
113
114 #[inline]
115 fn exact_voltage_step(
116 v: f64,
117 c_m: f64,
118 input: f64,
119 conductances: [(f64, f64); 4],
120 dt: f64,
121 ) -> f64 {
122 let g_total = conductances.iter().map(|(g, _)| *g).sum::<f64>();
123 let drive = input
124 + conductances
125 .iter()
126 .map(|(g, reversal)| g * reversal)
127 .sum::<f64>();
128 if g_total <= f64::EPSILON {
129 v + dt * drive / c_m
130 } else {
131 let v_inf = drive / g_total;
132 let tau = c_m / g_total;
133 v_inf + (v - v_inf) * (-dt / tau).exp()
134 }
135 }
136
137 fn is_valid(&self) -> bool {
138 [
139 self.v,
140 self.h,
141 self.n,
142 self.p,
143 self.g_na,
144 self.g_k,
145 self.g_kv3,
146 self.g_l,
147 self.e_na,
148 self.e_k,
149 self.e_l,
150 self.c_m,
151 self.phi,
152 self.dt,
153 self.v_threshold,
154 self.gain,
155 ]
156 .iter()
157 .all(|value| value.is_finite())
158 && (-100.0..=60.0).contains(&self.v)
159 && [self.h, self.n, self.p]
160 .iter()
161 .all(|gate| (0.0..=1.0).contains(gate))
162 && [self.g_na, self.g_k, self.g_kv3, self.g_l]
163 .iter()
164 .all(|conductance| *conductance >= 0.0)
165 && self.c_m > 0.0
166 && self.phi > 0.0
167 && self.dt > 0.0
168 && self.gain >= 0.0
169 }
170
171 pub fn step(&mut self, current: f64) -> i32 {
172 if !self.is_valid() || !current.is_finite() {
173 return 0;
174 }
175
176 let input = self.gain * current;
177 let sub_steps = 50;
178 let sub_dt = self.dt / sub_steps as f64;
179 let mut fired = 0i32;
180 let mut v = self.v;
181 let mut h = self.h;
182 let mut n = self.n;
183 let mut p = self.p;
184
185 for _ in 0..sub_steps {
186 let alpha_m = safe_rate(0.1, 35.0, v, 10.0, 1.0);
188 let beta_m = 4.0 * Self::safe_exp(-(v + 60.0) / 18.0);
189 let m_inf = alpha_m / (alpha_m + beta_m);
190
191 let alpha_h = 0.07 * Self::safe_exp(-(v + 58.0) / 20.0);
192 let beta_h = Self::boltz(v, -28.0, 10.0);
193
194 let alpha_n = safe_rate(0.01, 34.0, v, 10.0, 0.1);
195 let beta_n = 0.125 * Self::safe_exp(-(v + 44.0) / 80.0);
196
197 let p_inf = Self::boltz(v, -10.0, 10.0);
199 let tau_p = 1.0 + 4.0 / (1.0 + Self::safe_exp((v + 20.0) / 15.0));
200
201 h = Self::exact_hh_gate(h, alpha_h, beta_h, self.phi, sub_dt);
203 n = Self::exact_hh_gate(n, alpha_n, beta_n, self.phi, sub_dt);
204 p = Self::exact_relax(p, p_inf, tau_p, sub_dt);
205
206 let g_na = self.g_na * m_inf.powi(3) * h;
208 let g_k = self.g_k * n.powi(4);
209 let g_kv3 = self.g_kv3 * p.powi(2);
210 let g_l = self.g_l;
211
212 v = Self::exact_voltage_step(
213 v,
214 self.c_m,
215 input,
216 [
217 (g_na, self.e_na),
218 (g_k, self.e_k),
219 (g_kv3, self.e_k),
220 (g_l, self.e_l),
221 ],
222 sub_dt,
223 )
224 .clamp(-100.0, 60.0);
225 if ![v, h, n, p].iter().all(|value| value.is_finite()) {
226 return 0;
227 }
228
229 if v >= self.v_threshold {
230 fired = 1;
231 v = -65.0;
232 }
233 }
234
235 self.v = v;
236 self.h = h;
237 self.n = n;
238 self.p = p;
239
240 fired
241 }
242
243 pub fn reset(&mut self) {
244 *self = Self::new();
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[test]
255 fn stellate_fires_with_input() {
256 let mut n = StellateCell::new();
257 let mut spikes = 0;
258 for _ in 0..2_000 {
259 spikes += n.step(2.0);
260 }
261 assert!(
262 spikes > 5,
263 "Stellate cell must fire with input, got {spikes}"
264 );
265 }
266
267 #[test]
268 fn stellate_silent_without_input() {
269 let mut n = StellateCell::new();
270 let mut spikes = 0;
271 for _ in 0..10_000 {
272 spikes += n.step(0.0);
273 }
274 assert_eq!(
275 spikes, 0,
276 "Stellate cell must be silent without input, got {spikes}"
277 );
278 }
279
280 #[test]
281 fn stellate_high_frequency() {
282 let mut n = StellateCell::new();
284 let mut spikes = 0;
285 for _ in 0..2_000 {
286 spikes += n.step(20.0);
287 }
288 assert!(
290 spikes > 50,
291 "FS stellate should fire at high rate, got {spikes}"
292 );
293 }
294
295 #[test]
296 fn stellate_minimal_adaptation() {
297 let mut n = StellateCell::new();
299 let input = 10.0;
300 let mut spikes_early = 0;
301 for _ in 0..2000 {
302 spikes_early += n.step(input);
303 }
304 let mut spikes_late = 0;
305 for _ in 0..2000 {
306 spikes_late += n.step(input);
307 }
308 let diff = (spikes_early - spikes_late).abs();
310 assert!(
311 diff < 20,
312 "FS should have minimal adaptation: early={spikes_early}, late={spikes_late}"
313 );
314 }
315
316 #[test]
317 fn stellate_kv3_narrows_spikes() {
318 let mut with_kv3 = StellateCell::new();
320 let mut no_kv3 = StellateCell::new();
321 no_kv3.g_kv3 = 0.0;
322
323 let mut spikes_kv3 = 0;
324 let mut spikes_no = 0;
325 for _ in 0..2000 {
326 spikes_kv3 += with_kv3.step(15.0);
327 spikes_no += no_kv3.step(15.0);
328 }
329 assert!(spikes_kv3 > 0, "With Kv3.1 must fire, got {spikes_kv3}");
331 assert!(
332 spikes_no >= 0,
333 "No-Kv3.1 baseline must not panic, got {spikes_no}"
334 );
335 }
336
337 #[test]
338 fn stellate_negative_input_no_crash() {
339 let mut n = StellateCell::new();
340 for _ in 0..10_000 {
341 n.step(-100.0);
342 }
343 assert!(n.v.is_finite());
344 assert!(n.v >= -100.0);
345 }
346
347 #[test]
348 fn stellate_nan_input_stays_finite() {
349 let mut n = StellateCell::new();
350 let before = n.clone();
351 n.step(f64::NAN);
352 assert!(n.v.is_finite());
353 assert_eq!(n.v, before.v);
354 assert_eq!(n.h, before.h);
355 assert_eq!(n.n, before.n);
356 assert_eq!(n.p, before.p);
357 }
358
359 #[test]
360 fn stellate_corrupted_state_preserved_on_step() {
361 let mut n = StellateCell::new();
362 n.h = -0.1;
363 let before = n.clone();
364 assert_eq!(n.step(8.0), 0);
365 assert_eq!(n.v, before.v);
366 assert_eq!(n.h, before.h);
367 assert_eq!(n.n, before.n);
368 assert_eq!(n.p, before.p);
369 }
370
371 #[test]
372 fn stellate_invalid_voltage_preserved_on_step() {
373 let mut n = StellateCell::new();
374 n.v = 60.1;
375 let before = n.clone();
376 assert_eq!(n.step(8.0), 0);
377 assert_eq!(n.v, before.v);
378 assert_eq!(n.h, before.h);
379 assert_eq!(n.n, before.n);
380 assert_eq!(n.p, before.p);
381 }
382
383 #[test]
384 fn stellate_closed_form_gate_kinetics() {
385 let mut n = StellateCell::new();
386 n.g_na = 0.0;
387 n.g_k = 0.0;
388 n.g_kv3 = 0.0;
389 n.g_l = 0.0;
390 n.gain = 0.0;
391
392 let alpha_h = 0.07 * StellateCell::safe_exp(-(n.v + 58.0) / 20.0);
393 let beta_h = StellateCell::boltz(n.v, -28.0, 10.0);
394 let alpha_n = safe_rate(0.01, 34.0, n.v, 10.0, 0.1);
395 let beta_n = 0.125 * StellateCell::safe_exp(-(n.v + 44.0) / 80.0);
396 let p_inf = StellateCell::boltz(n.v, -10.0, 10.0);
397 let tau_p = 1.0 + 4.0 / (1.0 + StellateCell::safe_exp((n.v + 20.0) / 15.0));
398
399 let expected_h = exact_hh_gate_stellate(n.h, alpha_h, beta_h, n.phi, n.dt);
400 let expected_n = exact_hh_gate_stellate(n.n, alpha_n, beta_n, n.phi, n.dt);
401 let expected_p = exact_relax_stellate(n.p, p_inf, tau_p, n.dt);
402 let expected_v = n.v;
403
404 assert_eq!(n.step(0.0), 0);
405 assert_close_stellate(n.v, expected_v, 1e-12);
406 assert_close_stellate(n.h, expected_h, 1e-12);
407 assert_close_stellate(n.n, expected_n, 1e-12);
408 assert_close_stellate(n.p, expected_p, 1e-12);
409 }
410
411 fn exact_relax_stellate(value: f64, target: f64, tau: f64, dt: f64) -> f64 {
412 if tau <= f64::EPSILON {
413 target.clamp(0.0, 1.0)
414 } else {
415 (target + (value - target) * (-dt / tau).exp()).clamp(0.0, 1.0)
416 }
417 }
418
419 fn exact_hh_gate_stellate(value: f64, alpha: f64, beta: f64, phi: f64, dt: f64) -> f64 {
420 let total = alpha + beta;
421 if total <= f64::EPSILON {
422 value.clamp(0.0, 1.0)
423 } else {
424 let steady = alpha / total;
425 (steady + (value - steady) * (-phi * total * dt).exp()).clamp(0.0, 1.0)
426 }
427 }
428
429 fn assert_close_stellate(actual: f64, expected: f64, tolerance: f64) {
430 assert!(
431 (actual - expected).abs() <= tolerance,
432 "actual={actual:.16e} expected={expected:.16e} tolerance={tolerance:.3e}"
433 );
434 }
435
436 #[test]
437 fn stellate_extreme_input_bounded() {
438 let mut n = StellateCell::new();
439 for _ in 0..1000 {
440 n.step(1e6);
441 }
442 assert!(n.v.is_finite() && n.v <= 60.0);
443 }
444
445 #[test]
446 fn stellate_reset_clears_state() {
447 let mut n = StellateCell::new();
448 for _ in 0..1000 {
449 n.step(20.0);
450 }
451 n.reset();
452 assert_eq!(n.v, -65.0);
453 assert_eq!(n.p, 0.0);
454 }
455
456 #[test]
457 fn stellate_gates_bounded() {
458 let mut n = StellateCell::new();
459 for _ in 0..10_000 {
460 n.step(15.0);
461 }
462 assert!(n.h >= 0.0 && n.h <= 1.0);
463 assert!(n.n >= 0.0 && n.n <= 1.0);
464 assert!(n.p >= 0.0 && n.p <= 1.0);
465 }
466
467 #[test]
468 fn stellate_performance_1k_steps() {
469 let start = std::time::Instant::now();
470 let mut n = StellateCell::new();
471 for _ in 0..1_000 {
472 std::hint::black_box(n.step(10.0));
473 }
474 let elapsed = start.elapsed();
475 assert!(
476 elapsed.as_millis() < 200,
477 "1k steps must complete in <200ms, took {}ms",
478 elapsed.as_millis()
479 );
480 }
481
482 #[test]
483 fn stellate_default_matches_constructor_contract() {
484 let default = StellateCell::default();
485 let constructed = StellateCell::new();
486 assert_eq!(default.v, constructed.v);
487 assert_eq!(default.h, constructed.h);
488 assert_eq!(default.p, constructed.p);
489 assert_eq!(default.dt, constructed.dt);
490 }
491}