Skip to main content

sc_neurocore_engine/neurons/
sc_chaotic_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 — preserved SC engineering two-state chaotic map
8
9//! Project-designed map retained separately from the source Aihara model.
10
11#[derive(Clone, Debug)]
12pub struct SCChaoticMapNeuron {
13    pub x: f64,
14    pub y: f64,
15    pub k_f: f64,
16    pub k_s: f64,
17    pub alpha: f64,
18    pub delta: f64,
19    pub x_threshold: f64,
20}
21
22impl Default for SCChaoticMapNeuron {
23    fn default() -> Self {
24        Self::new()
25    }
26}
27
28impl SCChaoticMapNeuron {
29    pub fn new() -> Self {
30        Self {
31            x: 0.0,
32            y: 0.0,
33            k_f: 0.7,
34            k_s: 0.95,
35            alpha: 2.0,
36            delta: 0.05,
37            x_threshold: 0.5,
38        }
39    }
40
41    fn sigmoid(value: f64) -> f64 {
42        if value >= 0.0 {
43            1.0 / (1.0 + (-value).exp())
44        } else {
45            let exponential = value.exp();
46            exponential / (1.0 + exponential)
47        }
48    }
49
50    fn valid(&self) -> bool {
51        [
52            self.x,
53            self.y,
54            self.k_f,
55            self.k_s,
56            self.alpha,
57            self.delta,
58            self.x_threshold,
59        ]
60        .iter()
61        .all(|value| value.is_finite())
62            && self.k_f >= 0.0
63            && self.delta >= 0.0
64    }
65
66    pub fn try_step(&mut self, current: f64) -> Result<i32, &'static str> {
67        if !self.valid() || !current.is_finite() {
68            return Err("invalid SC chaotic map state, parameters, or current");
69        }
70        let previous = self.x;
71        let x_next = self.k_f * self.x * Self::sigmoid(self.x + self.alpha) - self.y + current;
72        let y_next = self.k_s * self.y + self.delta * self.x;
73        if !x_next.is_finite() || !y_next.is_finite() {
74            return Err("non-finite SC chaotic map candidate");
75        }
76        self.x = x_next.clamp(-10.0, 10.0);
77        self.y = y_next.clamp(-10.0, 10.0);
78        Ok(i32::from(
79            previous < self.x_threshold && self.x >= self.x_threshold,
80        ))
81    }
82
83    pub fn step(&mut self, current: f64) -> i32 {
84        self.try_step(current).unwrap_or(0)
85    }
86
87    pub fn reset(&mut self) {
88        self.x = 0.0;
89        self.y = 0.0;
90    }
91}
92
93#[derive(Clone, Debug)]
94pub struct SCChaoticMapBatchResult {
95    pub x: Vec<f64>,
96    pub y: Vec<f64>,
97    pub spikes: Vec<u8>,
98    pub x_final: f64,
99    pub y_final: f64,
100    pub spike_count: usize,
101}
102
103pub fn simulate_sc_chaotic_map(
104    x: f64,
105    y: f64,
106    k_f: f64,
107    k_s: f64,
108    alpha: f64,
109    delta: f64,
110    x_threshold: f64,
111    current: &[f64],
112) -> Result<SCChaoticMapBatchResult, &'static str> {
113    if current.len() > i32::MAX as usize {
114        return Err("current exceeds the signed-32-bit step limit");
115    }
116    let mut neuron = SCChaoticMapNeuron {
117        x,
118        y,
119        k_f,
120        k_s,
121        alpha,
122        delta,
123        x_threshold,
124    };
125    if current.iter().any(|value| !value.is_finite()) {
126        return Err("current must contain only finite values");
127    }
128    if !neuron.valid() {
129        return Err("invalid SC chaotic map state or parameters");
130    }
131
132    let mut x_trace = Vec::with_capacity(current.len());
133    let mut y_trace = Vec::with_capacity(current.len());
134    let mut spikes = Vec::with_capacity(current.len());
135    let mut spike_count = 0usize;
136    for &drive in current {
137        let event = neuron.try_step(drive)?;
138        x_trace.push(neuron.x);
139        y_trace.push(neuron.y);
140        spikes.push(event as u8);
141        spike_count += event as usize;
142    }
143    Ok(SCChaoticMapBatchResult {
144        x: x_trace,
145        y: y_trace,
146        spikes,
147        x_final: neuron.x,
148        y_final: neuron.y,
149        spike_count,
150    })
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn retained_recurrence_matches_independent_step() {
159        let mut neuron = SCChaoticMapNeuron {
160            x: 0.4,
161            y: -0.2,
162            ..Default::default()
163        };
164        let expected_x = 0.7 * 0.4 / (1.0 + (-2.4_f64).exp()) + 0.2 + 0.1;
165        let expected_y = 0.95 * -0.2 + 0.05 * 0.4;
166        neuron.try_step(0.1).unwrap();
167        assert!((neuron.x - expected_x).abs() < 1.0e-15);
168        assert!((neuron.y - expected_y).abs() < 1.0e-15);
169    }
170
171    #[test]
172    fn rejected_input_is_atomic() {
173        let mut neuron = SCChaoticMapNeuron::new();
174        assert!(neuron.try_step(f64::NAN).is_err());
175        assert_eq!((neuron.x, neuron.y), (0.0, 0.0));
176    }
177
178    #[test]
179    fn batch_returns_complete_receipts() {
180        let result =
181            simulate_sc_chaotic_map(0.4, -0.2, 0.7, 0.95, 2.0, 0.05, 0.5, &[0.1, 0.1]).unwrap();
182        assert_eq!(result.x.len(), 2);
183        assert_eq!(result.y.len(), 2);
184        assert_eq!(result.spikes, vec![1, 0]);
185        assert_eq!(result.spike_count, 1);
186    }
187}