sc_neurocore_engine/neurons/
aihara_map.rs1#[derive(Clone, Debug)]
13pub struct AiharaMapNeuron {
14 pub y: f64,
15 pub k: f64,
16 pub alpha: f64,
17 pub bias: f64,
18 pub epsilon: f64,
19}
20
21impl Default for AiharaMapNeuron {
22 fn default() -> Self {
23 Self::new()
24 }
25}
26
27impl AiharaMapNeuron {
28 pub fn new() -> Self {
29 Self {
30 y: 0.1,
31 k: 0.7,
32 alpha: 1.0,
33 bias: 0.3968,
34 epsilon: 0.01,
35 }
36 }
37
38 fn logistic(value: f64, epsilon: f64) -> f64 {
39 let argument = value / epsilon;
40 if argument >= 0.0 {
41 1.0 / (1.0 + (-argument).exp())
42 } else {
43 let exponential = argument.exp();
44 exponential / (1.0 + exponential)
45 }
46 }
47
48 fn valid(&self) -> bool {
49 [self.y, self.k, self.alpha, self.bias, self.epsilon]
50 .iter()
51 .all(|value| value.is_finite())
52 && (0.0..1.0).contains(&self.k)
53 && self.alpha > 0.0
54 && self.epsilon > 0.0
55 }
56
57 pub fn output(&self) -> f64 {
59 Self::logistic(self.y, self.epsilon)
60 }
61
62 pub fn try_step(&mut self, current: f64) -> Result<i32, AiharaMapError> {
64 if !self.valid() {
65 return Err(AiharaMapError::InvalidConfiguration);
66 }
67 if !current.is_finite() {
68 return Err(AiharaMapError::NonFiniteInput);
69 }
70 let next_y = self.k * self.y - self.alpha * self.output() + self.bias + current;
71 if !next_y.is_finite() {
72 return Err(AiharaMapError::NonFiniteCandidate);
73 }
74 let event = i32::from(Self::logistic(next_y, self.epsilon) >= 0.5);
75 self.y = next_y;
76 Ok(event)
77 }
78
79 pub fn step(&mut self, current: f64) -> i32 {
81 self.try_step(current).unwrap_or(0)
82 }
83
84 pub fn reset(&mut self) {
85 self.y = 0.1;
86 }
87}
88
89#[derive(Clone, Copy, Debug, PartialEq, Eq)]
90pub enum AiharaMapError {
91 InvalidConfiguration,
92 NonFiniteInput,
93 NonFiniteCandidate,
94 StepLimitExceeded,
95}
96
97impl std::fmt::Display for AiharaMapError {
98 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 formatter.write_str(match self {
100 Self::InvalidConfiguration => "invalid Aihara state or parameters",
101 Self::NonFiniteInput => "current must contain only finite values",
102 Self::NonFiniteCandidate => "Aihara map candidate must be finite",
103 Self::StepLimitExceeded => "current exceeds the signed-32-bit step limit",
104 })
105 }
106}
107
108impl std::error::Error for AiharaMapError {}
109
110#[derive(Clone, Debug)]
111pub struct AiharaMapBatchResult {
112 pub y: Vec<f64>,
113 pub x: Vec<f64>,
114 pub spikes: Vec<u8>,
115 pub y_final: f64,
116 pub x_final: f64,
117 pub spike_count: usize,
118}
119
120pub fn simulate_aihara_map(
122 y: f64,
123 k: f64,
124 alpha: f64,
125 bias: f64,
126 epsilon: f64,
127 current: &[f64],
128) -> Result<AiharaMapBatchResult, AiharaMapError> {
129 if current.len() > i32::MAX as usize {
130 return Err(AiharaMapError::StepLimitExceeded);
131 }
132 let mut neuron = AiharaMapNeuron {
133 y,
134 k,
135 alpha,
136 bias,
137 epsilon,
138 };
139 if !neuron.valid() {
140 return Err(AiharaMapError::InvalidConfiguration);
141 }
142 if current.iter().any(|value| !value.is_finite()) {
143 return Err(AiharaMapError::NonFiniteInput);
144 }
145
146 let mut y_trace = Vec::with_capacity(current.len());
147 let mut x_trace = Vec::with_capacity(current.len());
148 let mut spikes = Vec::with_capacity(current.len());
149 let mut spike_count = 0usize;
150 for &drive in current {
151 let event = neuron.try_step(drive)?;
152 y_trace.push(neuron.y);
153 x_trace.push(neuron.output());
154 spikes.push(event as u8);
155 spike_count += event as usize;
156 }
157 Ok(AiharaMapBatchResult {
158 y: y_trace,
159 x: x_trace,
160 spikes,
161 y_final: neuron.y,
162 x_final: neuron.output(),
163 spike_count,
164 })
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn first_step_matches_primary_equation() {
173 let mut neuron = AiharaMapNeuron::new();
174 let expected = 0.7 * 0.1 - 1.0 / (1.0 + (-10.0_f64).exp()) + 0.3968;
175 assert_eq!(neuron.try_step(0.0), Ok(0));
176 assert!((neuron.y - expected).abs() < 1.0e-15);
177 }
178
179 #[test]
180 fn waveform_shaper_is_a_level_observable() {
181 let mut neuron = AiharaMapNeuron::new();
182 neuron.y = -0.1;
183 neuron.k = 0.0;
184 neuron.alpha = 1.0;
185 neuron.bias = 0.2;
186 assert_eq!(neuron.try_step(0.0), Ok(1));
187 neuron.alpha = 0.01;
188 assert_eq!(neuron.try_step(0.0), Ok(1));
189 }
190
191 #[test]
192 fn invalid_batch_is_atomic() {
193 let result = simulate_aihara_map(0.1, 0.7, 1.0, 0.3968, 0.01, &[0.0, f64::NAN]);
194 assert_eq!(result.unwrap_err(), AiharaMapError::NonFiniteInput);
195 }
196
197 #[test]
198 fn source_defaults_are_bounded_and_nontrivial() {
199 let drive = vec![0.0; 4096];
200 let result = simulate_aihara_map(0.1, 0.7, 1.0, 0.3968, 0.01, &drive).unwrap();
201 assert!(result.y.iter().all(|value| value.is_finite()));
202 assert!(result.spike_count > 0 && result.spike_count < drive.len());
203 }
204
205 #[test]
206 fn reset_preserves_parameters() {
207 let mut neuron = AiharaMapNeuron {
208 y: -2.0,
209 k: 0.6,
210 alpha: 2.0,
211 bias: 0.5,
212 epsilon: 0.015,
213 };
214 neuron.reset();
215 assert_eq!(neuron.y, 0.1);
216 assert_eq!(
217 (neuron.k, neuron.alpha, neuron.bias, neuron.epsilon),
218 (0.6, 2.0, 0.5, 0.015)
219 );
220 }
221}