sc_neurocore_engine/neurons/
medvedev_map.rs1#[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 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 pub fn step(&mut self, current: f64) -> i32 {
128 self.try_step(current).unwrap_or(0)
129 }
130
131 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}