sc_neurocore_engine/neurons/trivial/
quadratic_if.rs1#[derive(Clone, Debug)]
12pub struct QuadraticIFNeuron {
13 pub v: f64,
14 pub v_reset: f64,
15 pub v_peak: f64,
16 pub dt: f64,
17 pub source_profile: bool,
18}
19
20pub type QuadraticIFCompleteTrace = (Vec<f64>, Vec<u8>, f64);
21
22impl QuadraticIFNeuron {
23 pub fn new(v_reset: f64, v_peak: f64, dt: f64) -> Self {
24 Self {
25 v: v_reset,
26 v_reset,
27 v_peak,
28 dt,
29 source_profile: false,
30 }
31 }
32
33 pub fn latham_2000() -> Self {
35 Self {
36 v: -1.0,
37 v_reset: -3.0,
38 v_peak: 31.0 / 3.0,
39 dt: 0.05,
40 source_profile: true,
41 }
42 }
43
44 fn valid_numeric_contract(&self) -> bool {
45 self.v.is_finite()
46 && self.v_reset.is_finite()
47 && self.v_peak.is_finite()
48 && self.dt.is_finite()
49 && self.v < self.v_peak
50 && self.v_reset < self.v_peak
51 && self.dt > 0.0
52 }
53
54 pub fn try_step(&mut self, current: f64) -> Result<i32, &'static str> {
55 if !self.valid_numeric_contract() || !current.is_finite() {
56 return Err("quadratic-IF state/current violates its finite profile contract");
57 }
58 let (next_v, spiked) = self.exact_candidate(current);
59 if !next_v.is_finite() {
60 return Err("quadratic-IF exact-flow candidate became non-finite");
61 }
62 self.v = next_v;
63 Ok(i32::from(spiked))
64 }
65
66 pub fn step(&mut self, current: f64) -> i32 {
67 self.try_step(current).unwrap_or(0)
68 }
69
70 pub fn simulate_complete(
72 &self,
73 n_steps: usize,
74 current: f64,
75 ) -> Result<QuadraticIFCompleteTrace, &'static str> {
76 if !self.valid_numeric_contract() || !current.is_finite() {
77 return Err("invalid quadratic-IF batch contract");
78 }
79 let mut candidate = self.clone();
80 let mut voltage = Vec::with_capacity(n_steps);
81 let mut events = Vec::with_capacity(n_steps);
82 for _ in 0..n_steps {
83 let event = candidate.try_step(current)?;
84 voltage.push(candidate.v);
85 events.push(event as u8);
86 }
87 Ok((voltage, events, candidate.v))
88 }
89
90 pub fn reset(&mut self) {
91 self.v = self.v_reset;
92 }
93
94 fn exact_candidate(&self, current: f64) -> (f64, bool) {
95 if current > 0.0 {
96 let root_i = current.sqrt();
97 let phase = (self.v / root_i).atan();
98 let peak_phase = (self.v_peak / root_i).atan();
99 let next_phase = phase + root_i * self.dt;
100 if next_phase >= peak_phase || next_phase >= std::f64::consts::FRAC_PI_2 {
101 return (self.v_reset, true);
102 }
103 return (root_i * next_phase.tan(), false);
104 }
105 if current == 0.0 {
106 let denominator = 1.0 - self.v * self.dt;
107 if denominator <= 0.0 {
108 return (self.v_reset, true);
109 }
110 let next_v = self.v / denominator;
111 if next_v >= self.v_peak {
112 return (self.v_reset, true);
113 }
114 return (next_v, false);
115 }
116
117 let root_i = (-current).sqrt();
118 if (self.v + root_i).abs() <= 1e-15 {
119 return (self.v, false);
120 }
121 let numerator_ratio = (self.v - root_i) / (self.v + root_i);
122 let evolved_ratio = numerator_ratio * (2.0 * root_i * self.dt).exp();
123 let denominator = 1.0 - evolved_ratio;
124 if (numerator_ratio < 1.0 && evolved_ratio >= 1.0) || denominator.abs() <= 1e-15 {
125 return (self.v_reset, true);
126 }
127 let next_v = root_i * (1.0 + evolved_ratio) / denominator;
128 if next_v >= self.v_peak {
129 (self.v_reset, true)
130 } else {
131 (next_v, false)
132 }
133 }
134}
135
136impl Default for QuadraticIFNeuron {
137 fn default() -> Self {
138 Self::new(-1.0, 1.0, 0.01)
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn qif_fires_with_positive_input() {
148 let mut n = QuadraticIFNeuron::default();
149 let total: i32 = (0..1000).map(|_| n.step(0.5)).sum();
150 assert!(total > 0);
151 }
152 #[test]
153 fn qif_silent_without_input() {
154 let mut n = QuadraticIFNeuron::default();
155 let t: i32 = (0..1000).map(|_| n.step(0.0)).sum();
156 assert_eq!(t, 0);
157 }
158 #[test]
159 fn qif_reset_clears_state() {
160 let mut n = QuadraticIFNeuron::default();
161 for _ in 0..100 {
162 n.step(0.5);
163 }
164 n.reset();
165 assert!((n.v - n.v_reset).abs() < 1e-10);
166 }
167 #[test]
168 fn qif_bounded() {
169 let mut n = QuadraticIFNeuron::default();
170 for _ in 0..1000 {
171 n.step(10.0);
172 }
173 assert!(n.v.is_finite());
174 }
175 #[test]
176 fn qif_nan_no_panic() {
177 let mut n = QuadraticIFNeuron::default();
178 let before = n.v;
179 assert_eq!(n.step(f64::NAN), 0);
180 assert_eq!(n.v, before);
181 }
182 #[test]
183 fn latham_profile_has_source_boundaries_and_complete_events() {
184 let n = QuadraticIFNeuron::latham_2000();
185 assert_eq!(n.v_reset, -3.0);
186 assert_eq!(n.v_peak, 31.0 / 3.0);
187 assert_eq!(n.dt, 0.05);
188 assert!(n.source_profile);
189 let (trace, events, final_v) = n.simulate_complete(8, 4.0).unwrap();
190 assert_eq!(trace.len(), 8);
191 assert_eq!(events.len(), 8);
192 assert_eq!(trace.last().copied(), Some(final_v));
193 }
194 #[test]
195 fn complete_batch_rejects_without_mutating_source() {
196 let mut n = QuadraticIFNeuron::latham_2000();
197 n.v = -0.25;
198 assert!(n.simulate_complete(2, f64::NAN).is_err());
199 assert_eq!(n.v, -0.25);
200 }
201 #[test]
202 fn qif_nonfinite_increment_preserves_state() {
203 let mut n = QuadraticIFNeuron {
204 v: -0.25,
205 ..Default::default()
206 };
207 let before = n.v;
208 assert_eq!(n.step(-1.0e308), 0);
209 assert_eq!(n.v, before);
210 }
211 #[test]
212 fn qif_matches_exact_positive_current_flow() {
213 let mut n = QuadraticIFNeuron::default();
214 let root_i = 0.5_f64.sqrt();
215 let expected = root_i * ((n.v / root_i).atan() + root_i * n.dt).tan();
216 assert_eq!(n.step(0.5), 0);
217 assert!((n.v - expected).abs() < 1e-12);
218 }
219 #[test]
220 fn qif_preserves_negative_current_fixed_point() {
221 let mut n = QuadraticIFNeuron::default();
222 assert_eq!(n.step(-1.0), 0);
223 assert_eq!(n.v, -1.0);
224 }
225 #[test]
226 fn qif_exact_flow_resets_on_peak_crossing() {
227 let mut n = QuadraticIFNeuron {
228 v: 0.95,
229 dt: 0.5,
230 ..Default::default()
231 };
232 assert_eq!(n.step(1.0), 1);
233 assert_eq!(n.v, n.v_reset);
234 }
235 #[test]
236 fn qif_negative_no_crash() {
237 let mut n = QuadraticIFNeuron::default();
238 for _ in 0..500 {
239 n.step(-5.0);
240 }
241 assert!(n.v.is_finite());
242 }
243}