1use std::error::Error;
14use std::fmt;
15
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18pub enum AlphaError {
19 NonFiniteConfiguration,
21 InvalidScale,
23 NonFiniteInput,
25 NonFiniteCandidate,
27}
28
29impl fmt::Display for AlphaError {
30 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
31 let message = match self {
32 Self::NonFiniteConfiguration => "alpha state and parameters must be finite",
33 Self::InvalidScale => {
34 "alpha tau_v/tau_exc/tau_inh/dt must be positive and v_threshold must exceed v_rest"
35 }
36 Self::NonFiniteInput => "alpha input must contain only finite values",
37 Self::NonFiniteCandidate => "alpha exact-flow candidate must remain finite",
38 };
39 formatter.write_str(message)
40 }
41}
42
43impl Error for AlphaError {}
44
45#[derive(Clone, Debug)]
47pub struct AlphaNeuron {
48 pub v: f64,
50 pub a_exc: f64,
52 pub i_exc: f64,
54 pub a_inh: f64,
56 pub i_inh: f64,
58 pub v_rest: f64,
60 pub v_threshold: f64,
62 pub tau_v: f64,
64 pub tau_exc: f64,
66 pub tau_inh: f64,
68 pub dt: f64,
70}
71
72impl AlphaNeuron {
73 pub fn new() -> Self {
75 Self {
76 v: 0.0,
77 a_exc: 0.0,
78 i_exc: 0.0,
79 a_inh: 0.0,
80 i_inh: 0.0,
81 v_rest: 0.0,
82 v_threshold: 1.0,
83 tau_v: 20.0,
84 tau_exc: 5.0,
85 tau_inh: 10.0,
86 dt: 1.0,
87 }
88 }
89
90 #[allow(clippy::too_many_arguments)]
92 pub fn with_parameters(
93 v: f64,
94 a_exc: f64,
95 i_exc: f64,
96 a_inh: f64,
97 i_inh: f64,
98 v_rest: f64,
99 v_threshold: f64,
100 tau_v: f64,
101 tau_exc: f64,
102 tau_inh: f64,
103 dt: f64,
104 ) -> Result<Self, AlphaError> {
105 let neuron = Self {
106 v,
107 a_exc,
108 i_exc,
109 a_inh,
110 i_inh,
111 v_rest,
112 v_threshold,
113 tau_v,
114 tau_exc,
115 tau_inh,
116 dt,
117 };
118 neuron.validate()?;
119 Ok(neuron)
120 }
121
122 fn validate(&self) -> Result<(), AlphaError> {
123 if ![
124 self.v,
125 self.a_exc,
126 self.i_exc,
127 self.a_inh,
128 self.i_inh,
129 self.v_rest,
130 self.v_threshold,
131 self.tau_v,
132 self.tau_exc,
133 self.tau_inh,
134 self.dt,
135 ]
136 .into_iter()
137 .all(f64::is_finite)
138 {
139 return Err(AlphaError::NonFiniteConfiguration);
140 }
141 if self.tau_v <= 0.0
142 || self.tau_exc <= 0.0
143 || self.tau_inh <= 0.0
144 || self.dt <= 0.0
145 || self.v_threshold <= self.v_rest
146 {
147 return Err(AlphaError::InvalidScale);
148 }
149 Ok(())
150 }
151
152 fn filter_candidates(
153 rise_state: f64,
154 current_state: f64,
155 drive: f64,
156 tau: f64,
157 dt: f64,
158 decay: f64,
159 ) -> Result<(f64, f64), AlphaError> {
160 let steady_state = tau * drive;
161 let rise_delta = rise_state - steady_state;
162 let current_delta = current_state - steady_state;
163 let rise_next = steady_state + rise_delta * decay;
164 let current_next = steady_state + decay * (current_delta + rise_delta * dt / tau);
165 if !rise_next.is_finite() || !current_next.is_finite() {
166 return Err(AlphaError::NonFiniteCandidate);
167 }
168 Ok((rise_next, current_next))
169 }
170
171 fn drive_contribution(
172 current_delta: f64,
173 rise_delta: f64,
174 tau_drive: f64,
175 dt: f64,
176 rates: [f64; 2],
177 decays: [f64; 2],
178 ) -> Result<f64, AlphaError> {
179 let [rate_v, rate_drive] = rates;
180 let [decay_v, decay_drive] = decays;
181 let contribution = if (rate_v - rate_drive).abs() <= 1.0e-14 {
182 rate_v * decay_v * (current_delta * dt + rise_delta * dt * dt / (2.0 * tau_drive))
183 } else {
184 let rate_delta = rate_v - rate_drive;
185 let first_order = current_delta * (decay_drive - decay_v) / rate_delta;
186 let second_order = rise_delta / tau_drive
187 * (decay_drive * (rate_delta * dt - 1.0) + decay_v)
188 / (rate_delta * rate_delta);
189 rate_v * (first_order + second_order)
190 };
191 if !contribution.is_finite() {
192 return Err(AlphaError::NonFiniteCandidate);
193 }
194 Ok(contribution)
195 }
196
197 #[inline]
199 pub fn try_step(&mut self, exc_current: f64, inh_current: f64) -> Result<i32, AlphaError> {
200 self.validate()?;
201 if !exc_current.is_finite() || !inh_current.is_finite() {
202 return Err(AlphaError::NonFiniteInput);
203 }
204 let rate_v = 1.0 / self.tau_v;
205 let rate_exc = 1.0 / self.tau_exc;
206 let rate_inh = 1.0 / self.tau_inh;
207 let decay_v = (-self.dt / self.tau_v).exp();
208 let decay_exc = (-self.dt / self.tau_exc).exp();
209 let decay_inh = (-self.dt / self.tau_inh).exp();
210 let (a_exc_next, i_exc_next) = Self::filter_candidates(
211 self.a_exc,
212 self.i_exc,
213 exc_current,
214 self.tau_exc,
215 self.dt,
216 decay_exc,
217 )?;
218 let (a_inh_next, i_inh_next) = Self::filter_candidates(
219 self.a_inh,
220 self.i_inh,
221 inh_current,
222 self.tau_inh,
223 self.dt,
224 decay_inh,
225 )?;
226 let exc_steady = self.tau_exc * exc_current;
227 let inh_steady = self.tau_inh * inh_current;
228 let v_steady = self.v_rest + exc_steady - inh_steady;
229 let v_next = v_steady
230 + (self.v - v_steady) * decay_v
231 + Self::drive_contribution(
232 self.i_exc - exc_steady,
233 self.a_exc - exc_steady,
234 self.tau_exc,
235 self.dt,
236 [rate_v, rate_exc],
237 [decay_v, decay_exc],
238 )?
239 - Self::drive_contribution(
240 self.i_inh - inh_steady,
241 self.a_inh - inh_steady,
242 self.tau_inh,
243 self.dt,
244 [rate_v, rate_inh],
245 [decay_v, decay_inh],
246 )?;
247 if !v_next.is_finite() {
248 return Err(AlphaError::NonFiniteCandidate);
249 }
250 self.a_exc = a_exc_next;
251 self.i_exc = i_exc_next;
252 self.a_inh = a_inh_next;
253 self.i_inh = i_inh_next;
254 if v_next >= self.v_threshold {
255 self.v = self.v_rest;
256 return Ok(1);
257 }
258 self.v = v_next;
259 Ok(0)
260 }
261
262 #[inline]
264 pub fn step(&mut self, exc_current: f64, inh_current: f64) -> i32 {
265 self.try_step(exc_current, inh_current).unwrap_or(0)
266 }
267
268 pub fn reset(&mut self) {
270 self.v = self.v_rest;
271 self.a_exc = 0.0;
272 self.i_exc = 0.0;
273 self.a_inh = 0.0;
274 self.i_inh = 0.0;
275 }
276}
277
278impl Default for AlphaNeuron {
279 fn default() -> Self {
280 Self::new()
281 }
282}
283
284pub struct AlphaTrace {
286 pub v: Vec<f64>,
288 pub a_exc: Vec<f64>,
290 pub i_exc: Vec<f64>,
292 pub a_inh: Vec<f64>,
294 pub i_inh: Vec<f64>,
296 pub spikes: Vec<f64>,
298 pub final_state: [f64; 5],
301 pub spike_count: usize,
303}
304
305#[allow(clippy::too_many_arguments)]
307pub fn simulate(
308 v: f64,
309 a_exc: f64,
310 i_exc: f64,
311 a_inh: f64,
312 i_inh: f64,
313 v_rest: f64,
314 v_threshold: f64,
315 tau_v: f64,
316 tau_exc: f64,
317 tau_inh: f64,
318 dt: f64,
319 exc_current: &[f64],
320 inh_current: &[f64],
321) -> Result<AlphaTrace, AlphaError> {
322 if exc_current.len() != inh_current.len() {
323 return Err(AlphaError::NonFiniteInput);
324 }
325 let mut neuron = AlphaNeuron::with_parameters(
326 v,
327 a_exc,
328 i_exc,
329 a_inh,
330 i_inh,
331 v_rest,
332 v_threshold,
333 tau_v,
334 tau_exc,
335 tau_inh,
336 dt,
337 )?;
338 if !exc_current.iter().all(|value| value.is_finite())
339 || !inh_current.iter().all(|value| value.is_finite())
340 {
341 return Err(AlphaError::NonFiniteInput);
342 }
343 let steps = exc_current.len();
344 let mut v_trace = Vec::with_capacity(steps);
345 let mut a_exc_trace = Vec::with_capacity(steps);
346 let mut i_exc_trace = Vec::with_capacity(steps);
347 let mut a_inh_trace = Vec::with_capacity(steps);
348 let mut i_inh_trace = Vec::with_capacity(steps);
349 let mut spikes = Vec::with_capacity(steps);
350 let mut spike_count = 0usize;
351 for index in 0..steps {
352 let spike = neuron.try_step(exc_current[index], inh_current[index])?;
353 spike_count += spike as usize;
354 v_trace.push(neuron.v);
355 a_exc_trace.push(neuron.a_exc);
356 i_exc_trace.push(neuron.i_exc);
357 a_inh_trace.push(neuron.a_inh);
358 i_inh_trace.push(neuron.i_inh);
359 spikes.push(f64::from(spike));
360 }
361 Ok(AlphaTrace {
362 v: v_trace,
363 a_exc: a_exc_trace,
364 i_exc: i_exc_trace,
365 a_inh: a_inh_trace,
366 i_inh: i_inh_trace,
367 spikes,
368 final_state: [
369 neuron.v,
370 neuron.a_exc,
371 neuron.i_exc,
372 neuron.a_inh,
373 neuron.i_inh,
374 ],
375 spike_count,
376 })
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382
383 #[test]
384 fn defaults_match_catalogue_model_family() {
385 let neuron = AlphaNeuron::new();
386 assert_eq!(
387 (
388 neuron.v,
389 neuron.a_exc,
390 neuron.i_exc,
391 neuron.a_inh,
392 neuron.i_inh,
393 neuron.v_rest,
394 neuron.v_threshold,
395 neuron.tau_v,
396 neuron.tau_exc,
397 neuron.tau_inh,
398 neuron.dt,
399 ),
400 (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 20.0, 5.0, 10.0, 1.0)
401 );
402 }
403
404 #[test]
405 fn filter_matches_exact_alpha_cascade() {
406 let decay = (-0.5_f64 / 5.0).exp();
407 let (rise_next, current_next) =
408 AlphaNeuron::filter_candidates(0.25, 0.1, 2.0, 5.0, 0.5, decay).unwrap();
409 let steady = 5.0 * 2.0;
410 let expected_rise = steady + (0.25 - steady) * decay;
411 let expected_current = steady + decay * ((0.1 - steady) + (0.25 - steady) * 0.5 / 5.0);
412 assert!((rise_next - expected_rise).abs() < 1.0e-12);
413 assert!((current_next - expected_current).abs() < 1.0e-12);
414 }
415
416 #[test]
417 fn drive_contribution_handles_equal_time_constants() {
418 let rate = 1.0 / 20.0;
419 let decay = (-0.5_f64 / 20.0).exp();
420 let exact =
421 AlphaNeuron::drive_contribution(0.3, 0.2, 20.0, 0.5, [rate, rate], [decay, decay])
422 .unwrap();
423 let expected = rate * decay * (0.3 * 0.5 + 0.2 * 0.5 * 0.5 / (2.0 * 20.0));
424 assert!((exact - expected).abs() < 1.0e-12);
425 }
426
427 #[test]
428 fn spike_resets_only_the_membrane() {
429 let mut neuron = AlphaNeuron {
430 v: 0.9,
431 a_exc: 0.4,
432 i_exc: 0.6,
433 a_inh: 0.2,
434 i_inh: 0.1,
435 v_threshold: 0.5,
436 ..AlphaNeuron::new()
437 };
438 let (a_exc_before, i_exc_before, a_inh_before, i_inh_before) =
439 (neuron.a_exc, neuron.i_exc, neuron.a_inh, neuron.i_inh);
440 assert_eq!(neuron.try_step(0.0, 0.0), Ok(1));
441 assert_eq!(neuron.v, 0.0);
442 let decay_exc = (-1.0_f64 / 5.0).exp();
443 let decay_inh = (-1.0_f64 / 10.0).exp();
444 assert!((neuron.a_exc - a_exc_before * decay_exc).abs() < 1.0e-12);
445 assert!(
446 (neuron.i_exc - decay_exc * (i_exc_before + a_exc_before * 1.0 / 5.0)).abs() < 1.0e-12
447 );
448 assert!((neuron.a_inh - a_inh_before * decay_inh).abs() < 1.0e-12);
449 assert!(
450 (neuron.i_inh - decay_inh * (i_inh_before + a_inh_before * 1.0 / 10.0)).abs() < 1.0e-12
451 );
452 }
453
454 #[test]
455 fn invalid_step_is_atomic() {
456 let mut neuron = AlphaNeuron::new();
457 neuron.v = 0.25;
458 neuron.a_exc = 0.5;
459 let before = (
460 neuron.v,
461 neuron.a_exc,
462 neuron.i_exc,
463 neuron.a_inh,
464 neuron.i_inh,
465 );
466 assert_eq!(
467 neuron.try_step(f64::NAN, 0.0),
468 Err(AlphaError::NonFiniteInput)
469 );
470 assert_eq!(
471 (
472 neuron.v,
473 neuron.a_exc,
474 neuron.i_exc,
475 neuron.a_inh,
476 neuron.i_inh
477 ),
478 before
479 );
480 }
481
482 #[test]
483 fn invalid_configuration_is_rejected() {
484 assert!(matches!(
485 AlphaNeuron::with_parameters(0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.5, 20.0, 5.0, 10.0, 1.0),
486 Err(AlphaError::InvalidScale)
487 ));
488 assert!(matches!(
489 AlphaNeuron::with_parameters(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 5.0, 10.0, 1.0),
490 Err(AlphaError::InvalidScale)
491 ));
492 }
493
494 #[test]
495 fn batch_matches_scalar_and_empty_preserves_initial_state() {
496 let empty = simulate(
497 0.1,
498 0.2,
499 0.3,
500 0.4,
501 0.5,
502 0.0,
503 1.0,
504 20.0,
505 5.0,
506 10.0,
507 1.0,
508 &[],
509 &[],
510 )
511 .unwrap();
512 assert!(empty.v.is_empty() && empty.spikes.is_empty());
513 assert_eq!(empty.final_state, [0.1, 0.2, 0.3, 0.4, 0.5]);
514
515 let exc = [0.5, 0.6, 0.7, 0.8];
516 let inh = [0.1, 0.2, 0.1, 0.2];
517 let batch = simulate(
518 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 20.0, 5.0, 10.0, 1.0, &exc, &inh,
519 )
520 .unwrap();
521 let mut scalar = AlphaNeuron::new();
522 let mut count = 0;
523 for index in 0..exc.len() {
524 count += scalar.try_step(exc[index], inh[index]).unwrap() as usize;
525 }
526 assert_eq!(
527 batch.final_state,
528 [
529 scalar.v,
530 scalar.a_exc,
531 scalar.i_exc,
532 scalar.a_inh,
533 scalar.i_inh
534 ]
535 );
536 assert_eq!(batch.spike_count, count);
537 }
538
539 #[test]
540 fn reset_restores_documented_rest_state_not_configuration() {
541 let mut neuron = AlphaNeuron::new();
542 neuron.v = 0.4;
543 neuron.a_exc = 0.3;
544 neuron.i_inh = 0.2;
545 neuron.reset();
546 assert_eq!(
547 (
548 neuron.v,
549 neuron.a_exc,
550 neuron.i_exc,
551 neuron.a_inh,
552 neuron.i_inh
553 ),
554 (0.0, 0.0, 0.0, 0.0, 0.0)
555 );
556 assert_eq!(
557 (
558 neuron.v_threshold,
559 neuron.tau_v,
560 neuron.tau_exc,
561 neuron.tau_inh,
562 neuron.dt
563 ),
564 (1.0, 20.0, 5.0, 10.0, 1.0)
565 );
566 }
567}