sc_neurocore_engine/neurons/
ermentrout_kopell_map.rs1#[derive(Clone, Debug)]
19pub struct ErmentroutKopellMapNeuron {
20 pub theta: f64, pub dt: f64,
22 pub gain: f64,
23 pub theta_threshold: f64,
24}
25
26impl Default for ErmentroutKopellMapNeuron {
27 fn default() -> Self {
28 Self::new()
29 }
30}
31
32impl ErmentroutKopellMapNeuron {
33 pub fn new() -> Self {
34 Self {
35 theta: 0.0,
36 dt: 0.1, gain: 1.0,
38 theta_threshold: std::f64::consts::PI,
39 }
40 }
41
42 pub fn step(&mut self, current: f64) -> i32 {
43 let input = self.gain * current;
44 let theta_prev = self.theta;
45
46 let d_theta = (1.0 - self.theta.cos()) + (1.0 + self.theta.cos()) * input;
47 self.theta += self.dt * d_theta;
48
49 let fired = if self.theta >= self.theta_threshold && theta_prev < self.theta_threshold {
51 1
52 } else {
53 0
54 };
55
56 let two_pi = 2.0 * std::f64::consts::PI;
58 if self.theta >= two_pi {
59 self.theta -= two_pi;
60 }
61 if self.theta < 0.0 {
62 self.theta += two_pi;
63 }
64
65 if !self.theta.is_finite() {
66 self.theta = 0.0;
67 }
68
69 fired
70 }
71
72 pub fn simulate(&mut self, n_steps: usize, current: f64) -> (Vec<f64>, i64) {
79 let mut trace = Vec::with_capacity(n_steps);
80 let mut spikes: i64 = 0;
81 for _ in 0..n_steps {
82 let spiked = self.step(current);
83 trace.push(self.theta);
84 spikes += spiked as i64;
85 }
86 (trace, spikes)
87 }
88
89 pub fn reset(&mut self) {
90 *self = Self::new();
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn ek_fires_with_input() {
100 let mut n = ErmentroutKopellMapNeuron::new();
101 let t: i32 = (0..5000).map(|_| n.step(0.5)).sum();
102 assert!(t > 0, "EK must fire with input, got {t}");
103 }
104
105 #[test]
106 fn ek_silent_without_input() {
107 let mut n = ErmentroutKopellMapNeuron::new();
109 let t: i32 = (0..5000).map(|_| n.step(-0.1)).sum();
110 assert_eq!(t, 0, "EK must be silent with negative input, got {t}");
111 }
112
113 #[test]
114 fn ek_type_i_excitability() {
115 let mut n_low = ErmentroutKopellMapNeuron::new();
117 let mut n_high = ErmentroutKopellMapNeuron::new();
118 let spikes_low: i32 = (0..10_000).map(|_| n_low.step(0.01)).sum();
119 let spikes_high: i32 = (0..10_000).map(|_| n_high.step(1.0)).sum();
120 assert!(
121 spikes_high > spikes_low,
122 "Higher input → higher rate: high={spikes_high} vs low={spikes_low}"
123 );
124 }
125
126 #[test]
127 fn ek_theta_wraps() {
128 let mut n = ErmentroutKopellMapNeuron::new();
130 for _ in 0..10_000 {
131 n.step(0.5);
132 }
133 let two_pi = 2.0 * std::f64::consts::PI;
134 assert!(
135 n.theta >= 0.0 && n.theta < two_pi,
136 "Theta must wrap to [0, 2pi), theta={}",
137 n.theta
138 );
139 }
140
141 #[test]
142 fn ek_negative_input_no_crash() {
143 let mut n = ErmentroutKopellMapNeuron::new();
144 for _ in 0..10_000 {
145 n.step(-100.0);
146 }
147 assert!(n.theta.is_finite());
148 }
149
150 #[test]
151 fn ek_nan_input_stays_finite() {
152 let mut n = ErmentroutKopellMapNeuron::new();
153 n.step(f64::NAN);
154 assert!(n.theta.is_finite());
155 }
156
157 #[test]
158 fn ek_extreme_input_bounded() {
159 let mut n = ErmentroutKopellMapNeuron::new();
160 for _ in 0..1000 {
161 n.step(1e6);
162 }
163 assert!(n.theta.is_finite());
164 }
165
166 #[test]
167 fn ek_reset_clears_state() {
168 let mut n = ErmentroutKopellMapNeuron::new();
169 for _ in 0..100 {
170 n.step(0.5);
171 }
172 n.reset();
173 assert_eq!(n.theta, 0.0);
174 }
175
176 #[test]
177 fn ek_performance_100k_steps() {
178 let start = std::time::Instant::now();
179 let mut n = ErmentroutKopellMapNeuron::new();
180 for _ in 0..100_000 {
181 std::hint::black_box(n.step(0.5));
182 }
183 let elapsed = start.elapsed();
184 assert!(
185 elapsed.as_millis() < 50,
186 "100k steps must complete in <50ms"
187 );
188 }
189}