Skip to main content

sc_neurocore_engine/neurons/trivial/
mat.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 — Kobayashi 2009 non-resetting MAT* neuron
8
9//! Source-faithful non-resetting MAT* adaptive-threshold neuron.
10
11const V_MIN: f64 = -200.0;
12const V_MAX: f64 = 200.0;
13const THETA_MAX: f64 = 1.0e9;
14
15/// Complete MAT* dynamic state and configuration.
16///
17/// Voltage is relative to rest and is never reset. Units are millivolts,
18/// milliseconds, nanoamps, and megaohms. The defaults are the regular-spiking
19/// example in Kobayashi, Tsubo, and Shinomoto (2009), not a universal cell fit.
20#[derive(Clone, Debug, PartialEq)]
21pub struct MATNeuron {
22    /// Membrane voltage relative to rest, in mV.
23    pub v: f64,
24    /// Fast spike-history threshold contribution, in mV.
25    pub theta1: f64,
26    /// Slow spike-history threshold contribution, in mV.
27    pub theta2: f64,
28    /// Remaining absolute refractory interval, in ms.
29    pub refractory_remaining: f64,
30    /// Baseline threshold omega, in mV.
31    pub omega: f64,
32    /// Membrane time constant, in ms.
33    pub tau_m: f64,
34    /// Fast threshold-history time constant, in ms.
35    pub tau_1: f64,
36    /// Slow threshold-history time constant, in ms.
37    pub tau_2: f64,
38    /// Fast post-spike threshold increment, in mV.
39    pub alpha_1: f64,
40    /// Slow post-spike threshold increment, in mV.
41    pub alpha_2: f64,
42    /// Membrane resistance, in megaohms.
43    pub resistance: f64,
44    /// Absolute refractory duration, in ms.
45    pub refractory_period: f64,
46    /// Euler integration step, in ms.
47    pub dt: f64,
48}
49
50impl MATNeuron {
51    /// Construct the paper's regular-spiking example profile.
52    #[must_use]
53    pub fn new() -> Self {
54        Self {
55            v: 0.0,
56            theta1: 0.0,
57            theta2: 0.0,
58            refractory_remaining: 0.0,
59            omega: 19.0,
60            tau_m: 5.0,
61            tau_1: 10.0,
62            tau_2: 200.0,
63            alpha_1: 37.0,
64            alpha_2: 2.0,
65            resistance: 50.0,
66            refractory_period: 2.0,
67            dt: 0.001,
68        }
69    }
70
71    /// Return the paper's intrinsically-bursting example profile.
72    #[must_use]
73    pub fn intrinsically_bursting() -> Self {
74        Self {
75            omega: 26.0,
76            alpha_1: 1.7,
77            alpha_2: 2.0,
78            ..Self::new()
79        }
80    }
81
82    /// Return the paper's fast-spiking example profile.
83    #[must_use]
84    pub fn fast_spiking() -> Self {
85        Self {
86            omega: 11.0,
87            alpha_1: 10.0,
88            alpha_2: 0.002,
89            ..Self::new()
90        }
91    }
92
93    /// Return the instantaneous adaptive threshold, in mV.
94    #[must_use]
95    pub fn threshold(&self) -> f64 {
96        self.omega + self.theta1 + self.theta2
97    }
98
99    /// Return whether all state and configuration invariants hold.
100    #[must_use]
101    pub fn validate(&self) -> bool {
102        let finite = [
103            self.v,
104            self.theta1,
105            self.theta2,
106            self.refractory_remaining,
107            self.omega,
108            self.tau_m,
109            self.tau_1,
110            self.tau_2,
111            self.alpha_1,
112            self.alpha_2,
113            self.resistance,
114            self.refractory_period,
115            self.dt,
116        ]
117        .iter()
118        .all(|value| value.is_finite());
119        finite
120            && (V_MIN..=V_MAX).contains(&self.v)
121            && (-THETA_MAX..=THETA_MAX).contains(&self.omega)
122            && [self.theta1, self.theta2, self.alpha_1, self.alpha_2]
123                .iter()
124                .all(|value| (0.0..=THETA_MAX).contains(value))
125            && self.tau_m > 0.0
126            && self.tau_1 > 0.0
127            && self.tau_2 > 0.0
128            && self.resistance > 0.0
129            && self.refractory_period >= 0.0
130            && self.dt > 0.0
131            && (0.0..=self.refractory_period).contains(&self.refractory_remaining)
132    }
133
134    /// Advance one atomic MAT* step.
135    ///
136    /// Voltage uses forward Euler and both threshold-history terms use exact
137    /// exponential decay. A spike never resets voltage. Invalid input or a
138    /// candidate outside the safety envelope returns an error without mutation.
139    pub fn try_step(&mut self, current: f64) -> Result<i32, &'static str> {
140        if !current.is_finite() || !self.validate() {
141            return Err("invalid MAT state, configuration, or current");
142        }
143        let v = self.v + self.dt * (-self.v + self.resistance * current) / self.tau_m;
144        let mut theta1 = self.theta1 * (-self.dt / self.tau_1).exp();
145        let mut theta2 = self.theta2 * (-self.dt / self.tau_2).exp();
146        let mut refractory = (self.refractory_remaining - self.dt).max(0.0);
147        if ![v, theta1, theta2, refractory]
148            .iter()
149            .all(|value| value.is_finite())
150            || !(V_MIN..=V_MAX).contains(&v)
151            || !(0.0..=THETA_MAX).contains(&theta1)
152            || !(0.0..=THETA_MAX).contains(&theta2)
153        {
154            return Err("MAT candidate outside safety envelope");
155        }
156        let spike = refractory == 0.0 && v >= self.omega + theta1 + theta2;
157        if spike {
158            theta1 += self.alpha_1;
159            theta2 += self.alpha_2;
160            refractory = self.refractory_period;
161            if theta1 > THETA_MAX || theta2 > THETA_MAX {
162                return Err("MAT post-spike threshold outside safety envelope");
163            }
164        }
165        self.v = v;
166        self.theta1 = theta1;
167        self.theta2 = theta2;
168        self.refractory_remaining = refractory;
169        Ok(i32::from(spike))
170    }
171
172    /// Advance one engine-adapter step; invalid state returns no event.
173    pub fn step(&mut self, current: f64) -> i32 {
174        self.try_step(current).unwrap_or(0)
175    }
176
177    /// Reset dynamic state while preserving the configured profile.
178    pub fn reset(&mut self) {
179        self.v = 0.0;
180        self.theta1 = 0.0;
181        self.theta2 = 0.0;
182        self.refractory_remaining = 0.0;
183    }
184}
185
186impl Default for MATNeuron {
187    fn default() -> Self {
188        Self::new()
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[test]
197    fn source_step_is_non_resetting() {
198        let mut neuron = MATNeuron {
199            v: 20.0,
200            ..MATNeuron::new()
201        };
202        let expected = 20.0 + neuron.dt * (-20.0) / neuron.tau_m;
203        assert_eq!(neuron.try_step(0.0), Ok(1));
204        assert_eq!(neuron.v, expected);
205        assert_eq!(neuron.theta1, 37.0);
206        assert_eq!(neuron.theta2, 2.0);
207    }
208
209    #[test]
210    fn source_reemits_after_refractory() {
211        let mut neuron = MATNeuron {
212            v: 25.0,
213            omega: 1.0,
214            alpha_1: 0.0,
215            alpha_2: 0.0,
216            tau_m: 1.0e9,
217            refractory_period: 2.0,
218            dt: 0.5,
219            ..MATNeuron::new()
220        };
221        assert_eq!(neuron.try_step(0.0), Ok(1));
222        assert_eq!(
223            (0..4)
224                .map(|_| neuron.try_step(0.0).unwrap())
225                .collect::<Vec<_>>(),
226            vec![0, 0, 0, 1]
227        );
228    }
229
230    #[test]
231    fn failure_is_atomic() {
232        let mut neuron = MATNeuron::new();
233        let before = neuron.clone();
234        assert!(neuron.try_step(f64::NAN).is_err());
235        assert_eq!(neuron, before);
236    }
237}