sc_neurocore_engine/neurons/misc/
graded_synapse.rs1#[derive(Clone, Debug)]
34pub struct GradedSynapseNeuron {
35 pub v: f64, pub c_m: f64, pub g_l: f64, pub e_l: f64, pub g_in: f64, pub v_half: f64, pub k_release: f64, pub v_min: f64, pub v_max: f64, pub v_threshold: f64, pub dt: f64,
46 pub gain: f64,
47}
48
49impl Default for GradedSynapseNeuron {
50 fn default() -> Self {
51 Self::new()
52 }
53}
54
55impl GradedSynapseNeuron {
56 pub fn new() -> Self {
57 Self {
58 v: -60.0,
59 c_m: 1.0,
60 g_l: 0.05, e_l: -60.0,
62 g_in: 0.1, v_half: -40.0, k_release: 5.0, v_min: -80.0,
66 v_max: -10.0,
67 v_threshold: -35.0, dt: 0.1,
69 gain: 1.0,
70 }
71 }
72
73 pub fn release(&self) -> f64 {
75 1.0 / (1.0 + (-(self.v - self.v_half) / self.k_release).exp())
76 }
77
78 pub fn step(&mut self, current: f64) -> i32 {
79 let input = self.gain * current;
80 let v_prev = self.v;
81
82 let dv = (-self.g_l * (self.v - self.e_l) + self.g_in * input) / self.c_m;
83 self.v += self.dt * dv;
84
85 self.v = self.v.clamp(self.v_min, self.v_max);
87 if !self.v.is_finite() {
88 self.v = self.e_l;
89 }
90
91 if self.v >= self.v_threshold && v_prev < self.v_threshold {
93 1
94 } else {
95 0
96 }
97 }
98
99 pub fn reset(&mut self) {
100 *self = Self::new();
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
111 fn graded_depolarises_with_input() {
112 let mut n = GradedSynapseNeuron::new();
113 let v0 = n.v;
114 for _ in 0..10_000 {
115 n.step(200.0);
116 }
117 assert!(
118 n.v > v0,
119 "Must depolarise with positive input: v0={v0}, v={}",
120 n.v
121 );
122 }
123
124 #[test]
125 fn graded_hyperpolarises_with_negative_input() {
126 let mut n = GradedSynapseNeuron::new();
127 let v0 = n.v;
128 for _ in 0..10_000 {
129 n.step(-200.0);
130 }
131 assert!(
132 n.v < v0,
133 "Must hyperpolarise with negative input: v0={v0}, v={}",
134 n.v
135 );
136 }
137
138 #[test]
139 fn graded_fires_with_strong_input() {
140 let mut n = GradedSynapseNeuron::new();
141 let mut spikes = 0;
142 for _ in 0..50_000 {
143 spikes += n.step(500.0);
144 }
145 assert!(
146 spikes > 0,
147 "Must cross threshold with strong input, got {spikes}"
148 );
149 }
150
151 #[test]
152 fn graded_silent_without_input() {
153 let mut n = GradedSynapseNeuron::new();
154 let mut spikes = 0;
155 for _ in 0..50_000 {
156 spikes += n.step(0.0);
157 }
158 assert_eq!(
159 spikes, 0,
160 "Must be silent without input (V starts at E_L), got {spikes}"
161 );
162 }
163
164 #[test]
165 fn graded_release_monotonic() {
166 let mut n = GradedSynapseNeuron::new();
168 n.v = -70.0;
169 let r_low = n.release();
170 n.v = -40.0;
171 let r_mid = n.release();
172 n.v = -10.0;
173 let r_high = n.release();
174 assert!(
175 r_low < r_mid && r_mid < r_high,
176 "Release must be monotonic: r_low={r_low:.3}, r_mid={r_mid:.3}, r_high={r_high:.3}"
177 );
178 }
179
180 #[test]
181 fn graded_release_bounded() {
182 let mut n = GradedSynapseNeuron::new();
183 n.v = -100.0;
184 assert!(n.release() >= 0.0 && n.release() <= 1.0);
185 n.v = 50.0;
186 assert!(n.release() >= 0.0 && n.release() <= 1.0);
187 }
188
189 #[test]
190 fn graded_v_saturates() {
191 let mut n = GradedSynapseNeuron::new();
192 for _ in 0..50_000 {
193 n.step(1e6);
194 }
195 assert!(
196 n.v <= n.v_max,
197 "V must not exceed v_max={}, got {}",
198 n.v_max,
199 n.v
200 );
201 n.reset();
202 for _ in 0..50_000 {
203 n.step(-1e6);
204 }
205 assert!(
206 n.v >= n.v_min,
207 "V must not go below v_min={}, got {}",
208 n.v_min,
209 n.v
210 );
211 }
212
213 #[test]
214 fn graded_nan_input_stays_finite() {
215 let mut n = GradedSynapseNeuron::new();
216 n.step(f64::NAN);
217 assert!(n.v.is_finite());
218 }
219
220 #[test]
221 fn graded_reset_clears_state() {
222 let mut n = GradedSynapseNeuron::new();
223 for _ in 0..10_000 {
224 n.step(500.0);
225 }
226 n.reset();
227 assert_eq!(n.v, -60.0);
228 }
229
230 #[test]
231 fn graded_performance_100k_steps() {
232 let start = std::time::Instant::now();
233 let mut n = GradedSynapseNeuron::new();
234 for _ in 0..100_000 {
235 std::hint::black_box(n.step(100.0));
236 }
237 let elapsed = start.elapsed();
238 assert!(
239 elapsed.as_millis() < 50,
240 "100k steps must complete in <50ms"
241 );
242 }
243
244 #[test]
245 fn graded_default_matches_constructor() {
246 let default = GradedSynapseNeuron::default();
247 let constructed = GradedSynapseNeuron::new();
248 assert_eq!(default.v, constructed.v);
249 assert_eq!(default.v_threshold, constructed.v_threshold);
250 assert_eq!(default.dt, constructed.dt);
251 }
252}