Skip to main content

sc_neurocore_engine/neurons/
chialvo_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 — Chialvo discrete map neuron
8
9//! Chialvo discrete map neuron.
10
11/// Chialvo 1995 — 2D discrete map neuron.
12#[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    /// Checked Chialvo update used by the production batch dispatcher.
50    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    /// Legacy infallible engine-class update. Invalid input leaves the state
76    /// unchanged and emits no event; the checked batch API reports the error.
77    pub fn step(&mut self, current: f64) -> i32 {
78        self.try_step(current).unwrap_or(0)
79    }
80
81    /// Run checked map iterations, returning both state traces and events.
82    ///
83    /// The batch is failure-atomic: no state is committed unless every
84    /// requested iteration produces a finite candidate.
85    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    /// Run checked map iterations, returning the legacy fast-state trace.
105    ///
106    /// This compatibility surface delegates to the complete, failure-atomic
107    /// batch and discards only the returned recovery-state trace.
108    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}