Skip to main content

sc_neurocore_engine/neurons/
rulkov_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 — Rulkov discrete map neuron
8
9//! Rulkov discrete map neuron.
10
11/// Rulkov (2002) piecewise nonlinear map for fast/slow bursting.
12#[derive(Clone, Debug)]
13pub struct RulkovMapNeuron {
14    pub x: f64,
15    pub y: f64,
16    pub alpha: f64,
17    pub sigma: f64,
18    pub mu: f64,
19}
20
21impl RulkovMapNeuron {
22    pub fn new() -> Self {
23        Self {
24            x: -1.0,
25            y: -3.0,
26            alpha: 4.0,
27            sigma: -1.6,
28            mu: 0.001,
29        }
30    }
31    fn valid_numeric_contract(&self) -> bool {
32        self.x.is_finite()
33            && self.y.is_finite()
34            && self.alpha.is_finite()
35            && self.alpha > 0.0
36            && self.sigma.is_finite()
37            && self.mu.is_finite()
38            && self.mu > 0.0
39    }
40
41    fn candidate(&self, current: f64) -> Option<(f64, f64, i32)> {
42        let boundary = self.alpha + self.y + current;
43        if !boundary.is_finite() {
44            return None;
45        }
46        let event = i32::from(self.x > 0.0 && self.x >= boundary);
47        let x_new = if self.x <= 0.0 {
48            let denominator = 1.0 - self.x;
49            if denominator <= 0.0 || !denominator.is_finite() {
50                return None;
51            }
52            self.alpha / denominator + self.y + current
53        } else if self.x < boundary {
54            boundary
55        } else {
56            -1.0
57        };
58        let y_new = self.y - self.mu * (self.x + 1.0) + self.mu * self.sigma;
59        if x_new.is_finite() && y_new.is_finite() {
60            Some((x_new, y_new, event))
61        } else {
62            None
63        }
64    }
65    /// Checked source update; an invalid candidate leaves state unchanged.
66    pub fn try_step(&mut self, current: f64) -> Option<i32> {
67        if !self.valid_numeric_contract() || !current.is_finite() {
68            return None;
69        }
70        let (x_new, y_new, event) = self.candidate(current)?;
71        self.x = x_new;
72        self.y = y_new;
73        Some(event)
74    }
75    /// Network-runner update; invalid input emits no event and preserves state.
76    pub fn step(&mut self, current: f64) -> i32 {
77        self.try_step(current).unwrap_or(0)
78    }
79    /// Run `n_steps` under a constant input, returning the `x` trace and the
80    /// reset-branch event count. Reuses `step` so the trace is bit-identical to
81    /// the per-step path and to the Python reference. The final state is left
82    /// in `self.x` / `self.y`.
83    pub fn simulate(&mut self, n_steps: usize, current: f64) -> (Vec<f64>, i64) {
84        self.try_simulate(n_steps, current).unwrap_or_default()
85    }
86    /// Run a failure-atomic source batch, returning `None` on any invalid stage.
87    pub fn try_simulate(&mut self, n_steps: usize, current: f64) -> Option<(Vec<f64>, i64)> {
88        let mut candidate = self.clone();
89        let mut trace = Vec::with_capacity(n_steps);
90        let mut events: i64 = 0;
91        for _ in 0..n_steps {
92            events += i64::from(candidate.try_step(current)?);
93            trace.push(candidate.x);
94        }
95        *self = candidate;
96        Some((trace, events))
97    }
98    pub fn reset(&mut self) {
99        self.x = -1.0;
100        self.y = -3.0;
101    }
102}
103impl Default for RulkovMapNeuron {
104    fn default() -> Self {
105        Self::new()
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn rulkov_fires() {
115        let mut n = RulkovMapNeuron::new();
116        let t: i32 = (0..2000).map(|_| n.step(0.5)).sum();
117        assert!(t > 0);
118    }
119
120    #[test]
121    fn source_event_is_rightmost_branch_execution() {
122        let mut neuron = RulkovMapNeuron {
123            x: 1.0,
124            y: -3.0,
125            ..Default::default()
126        };
127        assert_eq!(neuron.step(0.0), 1);
128        assert_eq!(neuron.x, -1.0);
129    }
130
131    #[test]
132    fn invalid_batch_is_failure_atomic() {
133        let mut neuron = RulkovMapNeuron::new();
134        neuron.alpha = f64::NAN;
135        let before = (neuron.x, neuron.y);
136        assert!(neuron.try_simulate(2, 0.0).is_none());
137        assert_eq!((neuron.x, neuron.y), before);
138    }
139}