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#![warn(missing_docs)]
12
13/// State and parameters of the bounded two-state SC engineering map.
14#[derive(Clone, Debug)]
15pub struct SCChaoticMapNeuron {
16    /// Current fast map state.
17    pub x: f64,
18    /// Current slow recovery state.
19    pub y: f64,
20    /// Fast-state gain.
21    pub k_f: f64,
22    /// Slow-state retention.
23    pub k_s: f64,
24    /// Sigmoid offset.
25    pub alpha: f64,
26    /// Fast-to-slow coupling.
27    pub delta: f64,
28    /// Upward-crossing output threshold.
29    pub x_threshold: f64,
30}
31
32impl Default for SCChaoticMapNeuron {
33    fn default() -> Self {
34        Self::new()
35    }
36}
37
38impl SCChaoticMapNeuron {
39    /// Construct the frozen project reference configuration.
40    pub fn new() -> Self {
41        Self {
42            x: 0.0,
43            y: 0.0,
44            k_f: 0.7,
45            k_s: 0.95,
46            alpha: 2.0,
47            delta: 0.05,
48            x_threshold: 0.5,
49        }
50    }
51
52    fn sigmoid(value: f64) -> f64 {
53        if value >= 0.0 {
54            1.0 / (1.0 + (-value).exp())
55        } else {
56            let exponential = value.exp();
57            exponential / (1.0 + exponential)
58        }
59    }
60
61    fn valid(&self) -> bool {
62        [
63            self.x,
64            self.y,
65            self.k_f,
66            self.k_s,
67            self.alpha,
68            self.delta,
69            self.x_threshold,
70        ]
71        .iter()
72        .all(|value| value.is_finite())
73            && self.k_f >= 0.0
74            && self.delta >= 0.0
75    }
76
77    /// Advance both states simultaneously, leaving state unchanged on error.
78    pub fn try_step(&mut self, current: f64) -> Result<i32, &'static str> {
79        if !self.valid() || !current.is_finite() {
80            return Err("invalid SC chaotic map state, parameters, or current");
81        }
82        let previous = self.x;
83        let x_next = self.k_f * self.x * Self::sigmoid(self.x + self.alpha) - self.y + current;
84        let y_next = self.k_s * self.y + self.delta * self.x;
85        if !x_next.is_finite() || !y_next.is_finite() {
86            return Err("non-finite SC chaotic map candidate");
87        }
88        self.x = x_next.clamp(-10.0, 10.0);
89        self.y = y_next.clamp(-10.0, 10.0);
90        Ok(i32::from(
91            previous < self.x_threshold && self.x >= self.x_threshold,
92        ))
93    }
94
95    /// Advance one step and fail closed for the network-runner interface.
96    pub fn step(&mut self, current: f64) -> i32 {
97        self.try_step(current).unwrap_or(0)
98    }
99
100    /// Clear both states while preserving all configured parameters.
101    pub fn reset(&mut self) {
102        self.x = 0.0;
103        self.y = 0.0;
104    }
105}
106
107/// Complete two-state trajectory and final receipts for one atomic batch.
108#[derive(Clone, Debug)]
109pub struct SCChaoticMapBatchResult {
110    /// Fast state after every step.
111    pub x: Vec<f64>,
112    /// Slow state after every step.
113    pub y: Vec<f64>,
114    /// Upward-crossing output events.
115    pub spikes: Vec<u8>,
116    /// Final fast state, or the initial state for an empty batch.
117    pub x_final: f64,
118    /// Final slow state, or the initial state for an empty batch.
119    pub y_final: f64,
120    /// Number of upward-crossing events in the batch.
121    pub spike_count: usize,
122}
123
124/// Run an atomically validated complete SC chaotic-map batch.
125pub fn simulate_sc_chaotic_map(
126    x: f64,
127    y: f64,
128    k_f: f64,
129    k_s: f64,
130    alpha: f64,
131    delta: f64,
132    x_threshold: f64,
133    current: &[f64],
134) -> Result<SCChaoticMapBatchResult, &'static str> {
135    if current.len() > i32::MAX as usize {
136        return Err("current exceeds the signed-32-bit step limit");
137    }
138    let mut neuron = SCChaoticMapNeuron {
139        x,
140        y,
141        k_f,
142        k_s,
143        alpha,
144        delta,
145        x_threshold,
146    };
147    if current.iter().any(|value| !value.is_finite()) {
148        return Err("current must contain only finite values");
149    }
150    if !neuron.valid() {
151        return Err("invalid SC chaotic map state or parameters");
152    }
153
154    let mut x_trace = Vec::with_capacity(current.len());
155    let mut y_trace = Vec::with_capacity(current.len());
156    let mut spikes = Vec::with_capacity(current.len());
157    let mut spike_count = 0usize;
158    for &drive in current {
159        let event = neuron.try_step(drive)?;
160        x_trace.push(neuron.x);
161        y_trace.push(neuron.y);
162        spikes.push(event as u8);
163        spike_count += event as usize;
164    }
165    Ok(SCChaoticMapBatchResult {
166        x: x_trace,
167        y: y_trace,
168        spikes,
169        x_final: neuron.x,
170        y_final: neuron.y,
171        spike_count,
172    })
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn retained_recurrence_matches_independent_step() {
181        let mut neuron = SCChaoticMapNeuron {
182            x: 0.4,
183            y: -0.2,
184            ..Default::default()
185        };
186        let expected_x = 0.7 * 0.4 / (1.0 + (-2.4_f64).exp()) + 0.2 + 0.1;
187        let expected_y = 0.95 * -0.2 + 0.05 * 0.4;
188        neuron.try_step(0.1).unwrap();
189        assert!((neuron.x - expected_x).abs() < 1.0e-15);
190        assert!((neuron.y - expected_y).abs() < 1.0e-15);
191    }
192
193    #[test]
194    fn rejected_input_is_atomic() {
195        let mut neuron = SCChaoticMapNeuron::new();
196        assert!(neuron.try_step(f64::NAN).is_err());
197        assert_eq!((neuron.x, neuron.y), (0.0, 0.0));
198    }
199
200    #[test]
201    fn batch_returns_complete_receipts() {
202        let result =
203            simulate_sc_chaotic_map(0.4, -0.2, 0.7, 0.95, 2.0, 0.05, 0.5, &[0.1, 0.1]).unwrap();
204        assert_eq!(result.x.len(), 2);
205        assert_eq!(result.y.len(), 2);
206        assert_eq!(result.spikes, vec![1, 0]);
207        assert_eq!(result.spike_count, 1);
208    }
209}