Skip to main content

sc_neurocore_engine/neurons/
medvedev_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 — Medvedev first-return map neuron
8
9//! Medvedev first-return map neuron.
10
11/// Medvedev (2005) calibrated slow-calcium first-return map.
12#[derive(Clone, Debug)]
13pub struct MedvedevMapNeuron {
14    pub u: f64,
15    pub beta_0: f64,
16    pub beta_hc: f64,
17    pub beta_sn: f64,
18    pub delta: f64,
19    pub decay_t0: f64,
20    pub alpha_t0: f64,
21    pub f_0: f64,
22    pub f_1: f64,
23    pub homoclinic_exponent: f64,
24    pub d: f64,
25    pub input_gain: f64,
26}
27
28impl MedvedevMapNeuron {
29    pub fn new() -> Self {
30        Self {
31            u: 0.251_407_883_672_443_6,
32            beta_0: 0.0015,
33            beta_hc: 0.00203,
34            beta_sn: 0.002_009_000_318_382_601,
35            delta: 0.01,
36            decay_t0: 0.990_356_335_578_673_4,
37            alpha_t0: 0.009_690_465_686_585_3,
38            f_0: 1.471_354_142_980_228_6,
39            f_1: 0.182_015_278_714_566_5,
40            homoclinic_exponent: 0.021_492_989_913_392_21,
41            d: 2_271.192_797_740_406,
42            input_gain: 0.01,
43        }
44    }
45
46    fn parameters_are_valid(&self) -> bool {
47        self.beta_0.is_finite()
48            && self.beta_hc.is_finite()
49            && self.beta_sn.is_finite()
50            && self.delta.is_finite()
51            && self.decay_t0.is_finite()
52            && self.alpha_t0.is_finite()
53            && self.f_0.is_finite()
54            && self.f_1.is_finite()
55            && self.homoclinic_exponent.is_finite()
56            && self.d.is_finite()
57            && self.input_gain.is_finite()
58            && 0.0 < self.beta_0
59            && self.beta_0 < self.beta_sn
60            && self.beta_sn < self.beta_hc
61            && self.beta_hc < self.delta
62            && 0.0 < self.decay_t0
63            && self.decay_t0 < 1.0
64            && 0.0 < self.alpha_t0
65            && self.alpha_t0 < 1.0
66            && 0.0 <= self.f_1
67            && self.f_1 < self.f_0
68            && self.homoclinic_exponent > 0.0
69            && self.d > 0.0
70            && self.input_gain >= 0.0
71    }
72
73    fn u_0(&self) -> f64 {
74        self.beta_0 / (self.delta - self.beta_0)
75    }
76
77    fn u_hc(&self) -> f64 {
78        self.beta_hc / (self.delta - self.beta_hc)
79    }
80
81    fn u_sn(&self) -> f64 {
82        self.beta_sn / (self.delta - self.beta_sn)
83    }
84
85    fn candidate(&self, current: f64) -> Result<f64, &'static str> {
86        let candidate = if self.u <= self.u_0() {
87            self.decay_t0 * self.u + (1.0 - self.decay_t0) * self.f_0 + self.input_gain * current
88        } else if self.u <= self.u_hc() {
89            let u_1 = (1.0 - self.alpha_t0) * self.u + self.alpha_t0 * self.f_0;
90            let gap = self.beta_hc - self.delta * u_1 / (1.0 + u_1);
91            let inner_return = if gap <= 0.0 {
92                self.f_1
93            } else {
94                let log_argument = self.d * gap;
95                if !log_argument.is_finite() || log_argument <= 0.0 {
96                    return Err("invalid Medvedev homoclinic log argument");
97                }
98                let scale = (self.homoclinic_exponent * log_argument.ln()).exp();
99                scale * (u_1 - self.f_1) + self.f_1
100            };
101            inner_return + self.input_gain * current
102        } else {
103            self.u_sn()
104        };
105        if !candidate.is_finite() {
106            return Err("invalid Medvedev first-return candidate");
107        }
108        Ok(candidate)
109    }
110
111    /// Checked source-derived update; a rejected step leaves the state intact.
112    pub fn try_step(&mut self, current: f64) -> Result<i32, &'static str> {
113        if !self.u.is_finite() || !self.parameters_are_valid() {
114            return Err("invalid Medvedev first-return runtime state");
115        }
116        if !current.is_finite() {
117            return Err("invalid Medvedev first-return current");
118        }
119        let event = i32::from(self.u <= self.u_hc());
120        let candidate = self.candidate(current)?;
121        self.u = candidate;
122        Ok(event)
123    }
124
125    /// Legacy infallible engine-class update. Invalid input leaves the state
126    /// unchanged and emits no event; the checked batch API reports the error.
127    pub fn step(&mut self, current: f64) -> i32 {
128        self.try_step(current).unwrap_or(0)
129    }
130
131    /// Run checked first-return iterations, returning the `u` trace and events.
132    pub fn simulate(
133        &mut self,
134        n_steps: usize,
135        current: f64,
136    ) -> Result<(Vec<f64>, i64), &'static str> {
137        let mut trace = Vec::with_capacity(n_steps);
138        let mut events = 0_i64;
139        for _ in 0..n_steps {
140            events += i64::from(self.try_step(current)?);
141            trace.push(self.u);
142        }
143        Ok((trace, events))
144    }
145
146    pub fn reset(&mut self) {
147        if self.parameters_are_valid() {
148            self.u = self.u_sn();
149        }
150    }
151}
152impl Default for MedvedevMapNeuron {
153    fn default() -> Self {
154        Self::new()
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn medvedev_matches_calibrated_first_return_goldens() {
164        let mut zero_current = MedvedevMapNeuron::new();
165        let (trace, events) = zero_current
166            .simulate(100, 0.0)
167            .expect("calibrated source regime must remain finite");
168        let mean = trace.iter().sum::<f64>() / trace.len() as f64;
169        assert_eq!(events, 100);
170        assert!((zero_current.u - 0.194_484_917_610_024_04).abs() < 1.0e-14);
171        assert!((mean - 0.216_230_983_622_399_98).abs() < 1.0e-14);
172
173        let mut driven = MedvedevMapNeuron::new();
174        let (_trace, events) = driven
175            .simulate(100, 2.0)
176            .expect("calibrated driven regime must remain finite");
177        assert_eq!(events, 75);
178        assert_eq!(driven.u, driven.u_sn());
179    }
180
181    #[test]
182    fn medvedev_rejects_invalid_runtime_without_mutation() {
183        let mut neuron = MedvedevMapNeuron::new();
184        let initial = neuron.u;
185        assert!(neuron.try_step(f64::NAN).is_err());
186        assert_eq!(neuron.u, initial);
187
188        neuron.d = f64::INFINITY;
189        assert!(neuron.try_step(0.0).is_err());
190        assert_eq!(neuron.u, initial);
191    }
192
193    #[test]
194    fn medvedev_reset_preserves_calibration() {
195        let mut neuron = MedvedevMapNeuron::new();
196        neuron.u = 0.2;
197        neuron.input_gain = 0.02;
198        neuron.reset();
199        assert_eq!(neuron.u, neuron.u_sn());
200        assert_eq!(neuron.input_gain, 0.02);
201    }
202}