Skip to main content

sc_neurocore_engine/neurons/
aihara_map.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Commercial license available
3// © Concepts 1996–2026 Miroslav Šotek. All rights reserved.
4// © Code 2020–2026 Miroslav Šotek. All rights reserved.
5// ORCID: 0009-0009-3560-0851
6// Contact: www.anulum.li | protoscience@anulum.li
7// SC-NeuroCore — source-faithful Aihara chaotic neuron
8
9//! Checked implementation of Aihara's reduced one-state map (1989, Eqs. 10-12).
10
11/// Aihara's internal-state map and graded logistic output.
12#[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    /// Current graded output `x(t)=f(y(t))` from Eq. 11.
58    pub fn output(&self) -> f64 {
59        Self::logistic(self.y, self.epsilon)
60    }
61
62    /// Checked Eq. 10 update; failures never mutate state.
63    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    /// Compatibility update for the engine network runner.
80    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
120/// Run an atomically validated piecewise-stimulus batch.
121pub 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}