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_complete(
86 &mut self,
87 n_steps: usize,
88 current: f64,
89 ) -> Result<(Vec<f64>, Vec<f64>, i64), &'static str> {
90 let mut candidate = self.clone();
91 let mut x_trace = Vec::with_capacity(n_steps);
92 let mut y_trace = Vec::with_capacity(n_steps);
93 let mut spikes = 0_i64;
94 for _ in 0..n_steps {
95 spikes += i64::from(candidate.try_step(current)?);
96 x_trace.push(candidate.x);
97 y_trace.push(candidate.y);
98 }
99 self.x = candidate.x;
100 self.y = candidate.y;
101 Ok((x_trace, y_trace, spikes))
102 }
103
104 pub fn simulate(
109 &mut self,
110 n_steps: usize,
111 current: f64,
112 ) -> Result<(Vec<f64>, i64), &'static str> {
113 let (x_trace, _y_trace, spikes) = self.simulate_complete(n_steps, current)?;
114 Ok((x_trace, spikes))
115 }
116
117 pub fn reset(&mut self) {
118 self.x = 0.0;
119 self.y = 0.0;
120 }
121}
122impl Default for ChialvoMapNeuron {
123 fn default() -> Self {
124 Self::new()
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn chialvo_matches_independent_source_step() {
134 let mut neuron = ChialvoMapNeuron {
135 x: 0.2,
136 y: 0.7,
137 ..Default::default()
138 };
139 let x = neuron.x;
140 let y = neuron.y;
141 let expected_x = x * x * (y - x).exp() + neuron.k + 0.01;
142 let expected_y = neuron.a * y - neuron.b * x + neuron.c;
143 assert_eq!(neuron.try_step(0.01), Ok(0));
144 assert_eq!(neuron.x, expected_x);
145 assert_eq!(neuron.y, expected_y);
146 }
147
148 #[test]
149 fn chialvo_matches_python_golden_event_counts() {
150 for (current, expected) in [(-0.05, 0_i64), (0.0, 26), (0.01, 30), (0.1, 0), (1.0, 1)] {
151 let mut neuron = ChialvoMapNeuron::new();
152 let (_trace, spikes) = neuron
153 .simulate(1000, current)
154 .expect("finite source regime");
155 assert_eq!(spikes, expected, "current={current}");
156 }
157 }
158
159 #[test]
160 fn chialvo_rejects_non_finite_input_without_mutation() {
161 let mut neuron = ChialvoMapNeuron::new();
162 let initial = (neuron.x, neuron.y);
163 assert!(neuron.try_step(f64::NAN).is_err());
164 assert_eq!((neuron.x, neuron.y), initial);
165
166 neuron.y = f64::INFINITY;
167 assert!(neuron.try_step(0.0).is_err());
168 }
169
170 #[test]
171 fn chialvo_reset_preserves_parameters() {
172 let mut neuron = ChialvoMapNeuron {
173 x: 2.0,
174 y: -1.0,
175 a: 0.8,
176 b: 0.4,
177 c: 0.2,
178 k: 0.03,
179 x_threshold: 0.75,
180 };
181 neuron.reset();
182 assert_eq!((neuron.x, neuron.y), (0.0, 0.0));
183 assert_eq!(
184 (neuron.a, neuron.b, neuron.c, neuron.k, neuron.x_threshold),
185 (0.8, 0.4, 0.2, 0.03, 0.75)
186 );
187 }
188
189 #[test]
190 fn chialvo_batch_failure_is_atomic_and_complete_trace_is_aligned() {
191 let mut neuron = ChialvoMapNeuron::new();
192 let initial = (neuron.x, neuron.y);
193 assert!(neuron.simulate_complete(2, 1.0e308).is_err());
194 assert_eq!((neuron.x, neuron.y), initial);
195
196 let (x_trace, y_trace, spikes) = neuron
197 .simulate_complete(3, 0.0)
198 .expect("finite source regime");
199 assert_eq!(x_trace.len(), 3);
200 assert_eq!(y_trace.len(), 3);
201 assert_eq!(spikes, 0);
202 assert_eq!((neuron.x, neuron.y), (x_trace[2], y_trace[2]));
203 }
204}