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 the fast-state trace and events.
82    pub fn simulate(
83        &mut self,
84        n_steps: usize,
85        current: f64,
86    ) -> Result<(Vec<f64>, i64), &'static str> {
87        let mut trace = Vec::with_capacity(n_steps);
88        let mut spikes = 0_i64;
89        for _ in 0..n_steps {
90            spikes += i64::from(self.try_step(current)?);
91            trace.push(self.x);
92        }
93        Ok((trace, spikes))
94    }
95
96    pub fn reset(&mut self) {
97        self.x = 0.0;
98        self.y = 0.0;
99    }
100}
101impl Default for ChialvoMapNeuron {
102    fn default() -> Self {
103        Self::new()
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn chialvo_matches_independent_source_step() {
113        let mut neuron = ChialvoMapNeuron {
114            x: 0.2,
115            y: 0.7,
116            ..Default::default()
117        };
118        let x = neuron.x;
119        let y = neuron.y;
120        let expected_x = x * x * (y - x).exp() + neuron.k + 0.01;
121        let expected_y = neuron.a * y - neuron.b * x + neuron.c;
122        assert_eq!(neuron.try_step(0.01), Ok(0));
123        assert_eq!(neuron.x, expected_x);
124        assert_eq!(neuron.y, expected_y);
125    }
126
127    #[test]
128    fn chialvo_matches_python_golden_event_counts() {
129        for (current, expected) in [(-0.05, 0_i64), (0.0, 26), (0.01, 30), (0.1, 0), (1.0, 1)] {
130            let mut neuron = ChialvoMapNeuron::new();
131            let (_trace, spikes) = neuron
132                .simulate(1000, current)
133                .expect("finite source regime");
134            assert_eq!(spikes, expected, "current={current}");
135        }
136    }
137
138    #[test]
139    fn chialvo_rejects_non_finite_input_without_mutation() {
140        let mut neuron = ChialvoMapNeuron::new();
141        let initial = (neuron.x, neuron.y);
142        assert!(neuron.try_step(f64::NAN).is_err());
143        assert_eq!((neuron.x, neuron.y), initial);
144
145        neuron.y = f64::INFINITY;
146        assert!(neuron.try_step(0.0).is_err());
147    }
148
149    #[test]
150    fn chialvo_reset_preserves_parameters() {
151        let mut neuron = ChialvoMapNeuron {
152            x: 2.0,
153            y: -1.0,
154            a: 0.8,
155            b: 0.4,
156            c: 0.2,
157            k: 0.03,
158            x_threshold: 0.75,
159        };
160        neuron.reset();
161        assert_eq!((neuron.x, neuron.y), (0.0, 0.0));
162        assert_eq!(
163            (neuron.a, neuron.b, neuron.c, neuron.k, neuron.x_threshold),
164            (0.8, 0.4, 0.2, 0.03, 0.75)
165        );
166    }
167}