sc_neurocore_engine/neurons/cerebellar/
unipolar_brush.rs1#[derive(Clone, Debug)]
26pub struct UnipolarBrushCell {
27 pub v: f64,
28 pub persistent: f64, pub v_rest: f64,
30 pub v_reset: f64,
31 pub v_threshold: f64,
32 pub tau_m: f64,
33 pub tau_persistent: f64, pub persistent_gain: f64, pub gain: f64,
36 pub dt: f64,
37}
38
39impl Default for UnipolarBrushCell {
40 fn default() -> Self {
41 Self::new()
42 }
43}
44
45impl UnipolarBrushCell {
46 pub fn new() -> Self {
47 Self {
48 v: -65.0,
49 persistent: 0.0,
50 v_rest: -65.0,
51 v_reset: -70.0,
52 v_threshold: -50.0,
53 tau_m: 8.0,
54 tau_persistent: 200.0,
55 persistent_gain: 0.5,
56 gain: 2.5,
57 dt: 0.5,
58 }
59 }
60
61 fn finite(values: &[f64]) -> bool {
62 values.iter().all(|value| value.is_finite())
63 }
64
65 fn valid_configuration(&self) -> bool {
66 Self::finite(&[
67 self.v_rest,
68 self.v_reset,
69 self.v_threshold,
70 self.tau_m,
71 self.tau_persistent,
72 self.persistent_gain,
73 self.gain,
74 self.dt,
75 ]) && self.tau_m > 0.0
76 && self.tau_persistent > 0.0
77 && self.persistent_gain >= 0.0
78 && self.gain >= 0.0
79 && self.dt > 0.0
80 && self.v_reset < self.v_threshold
81 }
82
83 fn valid_state(&self) -> bool {
84 Self::finite(&[self.v, self.persistent])
85 && (-100.0..=60.0).contains(&self.v)
86 && self.persistent >= 0.0
87 }
88
89 fn first_order_relaxation(previous: f64, steady_state: f64, dt: f64, tau: f64) -> f64 {
90 previous + (steady_state - previous) * (-(-dt / tau).exp_m1())
91 }
92
93 pub fn step(&mut self, current: f64) -> i32 {
94 if !self.valid_configuration() || !self.valid_state() || !current.is_finite() {
95 return 0;
96 }
97 let input = self.gain * current.max(0.0);
98 if !input.is_finite() {
99 return 0;
100 }
101 let next_persistent = Self::first_order_relaxation(
102 self.persistent,
103 self.persistent_gain * input,
104 self.dt,
105 self.tau_persistent,
106 )
107 .max(0.0);
108 let next_v = Self::first_order_relaxation(
109 self.v,
110 self.v_rest + input + next_persistent,
111 self.dt,
112 self.tau_m,
113 );
114 if !Self::finite(&[next_persistent, next_v]) {
115 return 0;
116 }
117 self.persistent = next_persistent;
118 if next_v >= self.v_threshold {
119 self.v = self.v_reset;
120 return 1;
121 }
122 self.v = next_v.clamp(-100.0, 60.0);
123 0
124 }
125
126 pub fn reset(&mut self) {
127 self.v = self.v_rest;
128 self.persistent = 0.0;
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
139 fn ubc_fires_with_input() {
140 let mut n = UnipolarBrushCell::new();
141 let mut spikes = 0;
142 for _ in 0..10_000 {
143 spikes += n.step(5.0);
144 }
145 assert!(
146 spikes > 10,
147 "UBC must fire with excitatory input, got {spikes}"
148 );
149 }
150
151 #[test]
152 fn ubc_silent_without_input() {
153 let mut n = UnipolarBrushCell::new();
154 let mut spikes = 0;
155 for _ in 0..10_000 {
156 spikes += n.step(0.0);
157 }
158 assert_eq!(spikes, 0, "UBC must be silent without input");
159 }
160
161 fn ubc_exact_relaxation(previous: f64, steady_state: f64, dt: f64, tau: f64) -> f64 {
162 previous + (steady_state - previous) * (-(-dt / tau).exp_m1())
163 }
164
165 #[test]
166 fn ubc_uses_closed_form_persistent_and_membrane_relaxation() {
167 let mut n = UnipolarBrushCell::new();
168
169 let spike = n.step(1.0);
170
171 let input_drive = n.gain;
172 let expected_persistent =
173 ubc_exact_relaxation(0.0, n.persistent_gain * input_drive, n.dt, n.tau_persistent);
174 let expected_v = ubc_exact_relaxation(
175 n.v_rest,
176 n.v_rest + input_drive + expected_persistent,
177 n.dt,
178 n.tau_m,
179 );
180 assert_eq!(spike, 0);
181 assert!(
182 (n.persistent - expected_persistent).abs() <= 1e-12,
183 "persistent={} expected={}",
184 n.persistent,
185 expected_persistent
186 );
187 assert!(
188 (n.v - expected_v).abs() <= 1e-12,
189 "v={} expected={}",
190 n.v,
191 expected_v
192 );
193 }
194
195 #[test]
196 fn ubc_corrupted_state_is_preserved_on_step() {
197 let mut n = UnipolarBrushCell::new();
198 n.v = f64::NAN;
199 n.persistent = 2.0;
200
201 assert_eq!(n.step(10.0), 0);
202
203 assert!(n.v.is_nan());
204 assert_eq!(n.persistent, 2.0);
205 }
206
207 #[test]
208 fn ubc_persistent_activity() {
209 let mut n = UnipolarBrushCell::new();
211 for _ in 0..2000 {
213 n.step(10.0);
214 }
215 assert!(
216 n.persistent > 0.0,
217 "Persistent current must build during input"
218 );
219
220 let persistent_before = n.persistent;
222 for _ in 0..100 {
223 n.step(0.0);
224 }
225 assert!(
226 n.persistent > 0.0,
227 "Persistent current must persist after input removal"
228 );
229 assert!(
230 n.persistent < persistent_before,
231 "Persistent current must decay"
232 );
233 }
234
235 #[test]
236 fn ubc_persistent_spikes_after_input() {
237 let mut n = UnipolarBrushCell::new();
239 for _ in 0..5000 {
241 n.step(10.0);
242 }
243 let post_spikes: i32 = (0..500).map(|_| n.step(0.0)).sum();
245 assert!(post_spikes >= 0, "post_spikes must be non-negative");
247 assert!(n.v.is_finite());
248 }
249
250 #[test]
251 fn ubc_negative_input_no_crash() {
252 let mut n = UnipolarBrushCell::new();
253 for _ in 0..10_000 {
254 n.step(-100.0);
255 }
256 assert!(n.v.is_finite());
257 }
258
259 #[test]
260 fn ubc_nan_input_stays_finite() {
261 let mut n = UnipolarBrushCell::new();
262 n.step(f64::NAN);
263 assert!(n.v.is_finite());
264 }
265
266 #[test]
267 fn ubc_extreme_input_bounded() {
268 let mut n = UnipolarBrushCell::new();
269 for _ in 0..1000 {
270 n.step(1e6);
271 }
272 assert!(n.v.is_finite() && n.v <= 60.0);
273 }
274
275 #[test]
276 fn ubc_reset_clears_state() {
277 let mut n = UnipolarBrushCell::new();
278 for _ in 0..1000 {
279 n.step(10.0);
280 }
281 n.reset();
282 assert_eq!(n.v, -65.0);
283 assert_eq!(n.persistent, 0.0);
284 }
285
286 #[test]
287 fn ubc_performance_10k_steps() {
288 let start = std::time::Instant::now();
289 let mut n = UnipolarBrushCell::new();
290 for _ in 0..10_000 {
291 std::hint::black_box(n.step(5.0));
292 }
293 let elapsed = start.elapsed();
294 assert!(elapsed.as_millis() < 50, "10k steps must complete in <50ms");
295 }
296
297 #[test]
298 fn ubc_default_matches_constructor_contract() {
299 let default = UnipolarBrushCell::default();
300 let constructed = UnipolarBrushCell::new();
301 assert_eq!(default.v, constructed.v);
302 assert_eq!(default.persistent, constructed.persistent);
303 assert_eq!(default.tau_persistent, constructed.tau_persistent);
304 assert_eq!(default.dt, constructed.dt);
305 }
306}