sc_neurocore_engine/neurons/
chialvo_map.rs1#[derive(Clone, Debug)]
13pub struct ChialvoMapNeuron {
14 pub x: f64,
15 pub y: f64,
16 pub a: f64,
17 pub b: f64,
18 pub c: f64,
19 pub k: f64,
20 pub x_threshold: f64,
21}
22
23impl ChialvoMapNeuron {
24 pub fn new() -> Self {
25 Self {
26 x: 0.0,
27 y: 0.0,
28 a: 0.89,
29 b: 0.6,
30 c: 0.28,
31 k: 0.04,
32 x_threshold: 1.0,
33 }
34 }
35 fn is_valid(&self) -> bool {
36 self.x.is_finite()
37 && self.y.is_finite()
38 && self.a.is_finite()
39 && self.b.is_finite()
40 && self.c.is_finite()
41 && self.k.is_finite()
42 && self.x_threshold.is_finite()
43 }
44
45 fn safe_exp(value: f64) -> f64 {
46 value.clamp(-500.0, 500.0).exp()
47 }
48
49 pub fn try_step(&mut self, current: f64) -> Result<i32, &'static str> {
51 if !self.is_valid() {
52 return Err("invalid Chialvo map runtime state");
53 }
54 if !current.is_finite() {
55 return Err("invalid Chialvo map current");
56 }
57
58 let x_prev = self.x;
59 let x_squared = self.x * self.x;
60 let exponential = Self::safe_exp(self.y - self.x);
61 let x_new = x_squared * exponential + self.k + current;
62 let y_new = self.a * self.y - self.b * self.x + self.c;
63 if !x_new.is_finite() || !y_new.is_finite() {
64 return Err("invalid Chialvo map candidate state");
65 }
66 self.x = x_new;
67 self.y = y_new;
68 Ok(if x_prev < self.x_threshold && self.x >= self.x_threshold {
69 1
70 } else {
71 0
72 })
73 }
74
75 pub fn step(&mut self, current: f64) -> i32 {
78 self.try_step(current).unwrap_or(0)
79 }
80
81 pub fn simulate(
83 &mut self,
84 n_steps: usize,
85 current: f64,
86 ) -> Result<(Vec<f64>, i64), &'static str> {
87 let mut trace = Vec::with_capacity(n_steps);
88 let mut spikes = 0_i64;
89 for _ in 0..n_steps {
90 spikes += i64::from(self.try_step(current)?);
91 trace.push(self.x);
92 }
93 Ok((trace, spikes))
94 }
95
96 pub fn reset(&mut self) {
97 self.x = 0.0;
98 self.y = 0.0;
99 }
100}
101impl Default for ChialvoMapNeuron {
102 fn default() -> Self {
103 Self::new()
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn chialvo_matches_independent_source_step() {
113 let mut neuron = ChialvoMapNeuron {
114 x: 0.2,
115 y: 0.7,
116 ..Default::default()
117 };
118 let x = neuron.x;
119 let y = neuron.y;
120 let expected_x = x * x * (y - x).exp() + neuron.k + 0.01;
121 let expected_y = neuron.a * y - neuron.b * x + neuron.c;
122 assert_eq!(neuron.try_step(0.01), Ok(0));
123 assert_eq!(neuron.x, expected_x);
124 assert_eq!(neuron.y, expected_y);
125 }
126
127 #[test]
128 fn chialvo_matches_python_golden_event_counts() {
129 for (current, expected) in [(-0.05, 0_i64), (0.0, 26), (0.01, 30), (0.1, 0), (1.0, 1)] {
130 let mut neuron = ChialvoMapNeuron::new();
131 let (_trace, spikes) = neuron
132 .simulate(1000, current)
133 .expect("finite source regime");
134 assert_eq!(spikes, expected, "current={current}");
135 }
136 }
137
138 #[test]
139 fn chialvo_rejects_non_finite_input_without_mutation() {
140 let mut neuron = ChialvoMapNeuron::new();
141 let initial = (neuron.x, neuron.y);
142 assert!(neuron.try_step(f64::NAN).is_err());
143 assert_eq!((neuron.x, neuron.y), initial);
144
145 neuron.y = f64::INFINITY;
146 assert!(neuron.try_step(0.0).is_err());
147 }
148
149 #[test]
150 fn chialvo_reset_preserves_parameters() {
151 let mut neuron = ChialvoMapNeuron {
152 x: 2.0,
153 y: -1.0,
154 a: 0.8,
155 b: 0.4,
156 c: 0.2,
157 k: 0.03,
158 x_threshold: 0.75,
159 };
160 neuron.reset();
161 assert_eq!((neuron.x, neuron.y), (0.0, 0.0));
162 assert_eq!(
163 (neuron.a, neuron.b, neuron.c, neuron.k, neuron.x_threshold),
164 (0.8, 0.4, 0.2, 0.03, 0.75)
165 );
166 }
167}