Skip to main content

sc_neurocore_engine/neurons/
nagumo_sato_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 — source-faithful Nagumo–Sato refractory map
8
9//! Checked implementation of the Nagumo–Sato reduction (Aihara 1989, Eqs. 1-7).
10
11#![warn(missing_docs)]
12
13/// Source-faithful internal state and parameters of the Nagumo–Sato map.
14#[derive(Clone, Debug)]
15pub struct NagumoSatoMapNeuron {
16    /// Current internal state `y(t)`.
17    pub y: f64,
18    /// Refractory-memory damping factor in `[0, 1)`.
19    pub k: f64,
20    /// Positive refractory decrement.
21    pub alpha: f64,
22    /// Constant transformed stimulus `a`.
23    pub bias: f64,
24}
25
26impl Default for NagumoSatoMapNeuron {
27    fn default() -> Self {
28        Self::new()
29    }
30}
31
32impl NagumoSatoMapNeuron {
33    /// Construct the documented source operating configuration.
34    pub fn new() -> Self {
35        Self {
36            y: 0.1,
37            k: 0.6,
38            alpha: 1.0,
39            bias: 0.2,
40        }
41    }
42
43    fn heaviside(value: f64) -> i32 {
44        i32::from(value >= 0.0)
45    }
46
47    fn valid(&self) -> bool {
48        [self.y, self.k, self.alpha, self.bias]
49            .iter()
50            .all(|value| value.is_finite())
51            && (0.0..1.0).contains(&self.k)
52            && self.alpha > 0.0
53    }
54
55    /// Return the all-or-none source output `H(y)`, with `H(0)=1`.
56    pub fn output(&self) -> i32 {
57        Self::heaviside(self.y)
58    }
59
60    /// Advance one source-equation step, leaving state unchanged on error.
61    pub fn try_step(&mut self, current: f64) -> Result<i32, NagumoSatoMapError> {
62        if !self.valid() {
63            return Err(NagumoSatoMapError::InvalidConfiguration);
64        }
65        if !current.is_finite() {
66            return Err(NagumoSatoMapError::NonFiniteInput);
67        }
68        let next_y = self.k * self.y - self.alpha * f64::from(self.output()) + self.bias + current;
69        if !next_y.is_finite() {
70            return Err(NagumoSatoMapError::NonFiniteCandidate);
71        }
72        let event = Self::heaviside(next_y);
73        self.y = next_y;
74        Ok(event)
75    }
76
77    /// Advance one step and fail closed for the network-runner interface.
78    pub fn step(&mut self, current: f64) -> i32 {
79        self.try_step(current).unwrap_or(0)
80    }
81
82    /// Restore `y` to the source initial condition while preserving parameters.
83    pub fn reset(&mut self) {
84        self.y = 0.1;
85    }
86}
87
88/// Validation failures produced by the checked source map and batch runner.
89#[derive(Clone, Copy, Debug, PartialEq, Eq)]
90pub enum NagumoSatoMapError {
91    /// State or a parameter violates the source contract.
92    InvalidConfiguration,
93    /// A scalar or batch input is not finite.
94    NonFiniteInput,
95    /// An otherwise valid step produced a non-finite candidate.
96    NonFiniteCandidate,
97    /// A batch exceeds the signed 32-bit native ABI length.
98    StepLimitExceeded,
99}
100
101impl std::fmt::Display for NagumoSatoMapError {
102    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103        formatter.write_str(match self {
104            Self::InvalidConfiguration => "invalid Nagumo-Sato state or parameters",
105            Self::NonFiniteInput => "current must contain only finite values",
106            Self::NonFiniteCandidate => "Nagumo-Sato map candidate must be finite",
107            Self::StepLimitExceeded => "current exceeds the signed-32-bit step limit",
108        })
109    }
110}
111
112impl std::error::Error for NagumoSatoMapError {}
113
114/// Complete state/output trajectory and final receipts for one atomic batch.
115#[derive(Clone, Debug)]
116pub struct NagumoSatoMapBatchResult {
117    /// Internal state after every step.
118    pub y: Vec<f64>,
119    /// All-or-none output after every step.
120    pub x: Vec<u8>,
121    /// Event alias of the all-or-none output.
122    pub spikes: Vec<u8>,
123    /// Final internal state, or the initial state for an empty batch.
124    pub y_final: f64,
125    /// Final all-or-none output.
126    pub x_final: u8,
127    /// Number of firing outputs in the batch.
128    pub spike_count: usize,
129}
130
131/// Run an atomically validated complete Nagumo–Sato batch.
132pub fn simulate_nagumo_sato_map(
133    y: f64,
134    k: f64,
135    alpha: f64,
136    bias: f64,
137    current: &[f64],
138) -> Result<NagumoSatoMapBatchResult, NagumoSatoMapError> {
139    if current.len() > i32::MAX as usize {
140        return Err(NagumoSatoMapError::StepLimitExceeded);
141    }
142    let mut neuron = NagumoSatoMapNeuron { y, k, alpha, bias };
143    if !neuron.valid() {
144        return Err(NagumoSatoMapError::InvalidConfiguration);
145    }
146    if current.iter().any(|value| !value.is_finite()) {
147        return Err(NagumoSatoMapError::NonFiniteInput);
148    }
149    let mut y_trace = Vec::with_capacity(current.len());
150    let mut output = Vec::with_capacity(current.len());
151    let mut spike_count = 0usize;
152    for &drive in current {
153        let event = neuron.try_step(drive)? as u8;
154        y_trace.push(neuron.y);
155        output.push(event);
156        spike_count += event as usize;
157    }
158    Ok(NagumoSatoMapBatchResult {
159        y: y_trace,
160        x: output.clone(),
161        spikes: output,
162        y_final: neuron.y,
163        x_final: neuron.output() as u8,
164        spike_count,
165    })
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn first_steps_match_source_equation() {
174        let mut neuron = NagumoSatoMapNeuron::new();
175        assert_eq!(neuron.try_step(0.0), Ok(0));
176        assert!((neuron.y - (-0.74)).abs() < 1.0e-15);
177        assert_eq!(neuron.try_step(0.0), Ok(0));
178        assert!((neuron.y - (-0.244)).abs() < 1.0e-15);
179        assert_eq!(neuron.try_step(0.0), Ok(1));
180        assert!((neuron.y - 0.0536).abs() < 1.0e-15);
181    }
182
183    #[test]
184    fn invalid_batch_is_atomic() {
185        assert_eq!(
186            simulate_nagumo_sato_map(0.1, 0.6, 1.0, 0.2, &[0.0, f64::NAN]).unwrap_err(),
187            NagumoSatoMapError::NonFiniteInput
188        );
189    }
190
191    #[test]
192    fn reset_preserves_parameters() {
193        let mut neuron = NagumoSatoMapNeuron {
194            y: -2.0,
195            k: 0.5,
196            alpha: 2.0,
197            bias: 0.7,
198        };
199        neuron.reset();
200        assert_eq!(
201            (neuron.y, neuron.k, neuron.alpha, neuron.bias),
202            (0.1, 0.5, 2.0, 0.7)
203        );
204    }
205}