sc_neurocore_engine/neurons/biophysical/
gif_population.rs1use rand::{RngExt, SeedableRng};
12use rand_xoshiro::Xoshiro256PlusPlus;
13
14#[derive(Clone, Debug)]
16pub struct GIFPopulationNeuron {
17 pub v: f64,
18 pub theta: f64,
19 pub eta: f64,
20 pub tau_m: f64,
21 pub tau_eta: f64,
22 pub delta_v: f64,
23 pub lambda_0: f64,
24 pub eta_increment: f64,
25 pub v_rest: f64,
26 pub v_reset: f64,
27 pub dt: f64,
28 pub seed: u64,
29 rng: Xoshiro256PlusPlus,
30}
31
32impl GIFPopulationNeuron {
33 pub fn new(seed: u64) -> Self {
34 Self {
35 v: -65.0,
36 theta: -50.0,
37 eta: 0.0,
38 tau_m: 20.0,
39 tau_eta: 100.0,
40 delta_v: 2.0,
41 lambda_0: 0.001,
42 eta_increment: 5.0,
43 v_rest: -65.0,
44 v_reset: -65.0,
45 dt: 0.5,
46 seed,
47 rng: Xoshiro256PlusPlus::seed_from_u64(seed),
48 }
49 }
50
51 fn finite_values(values: &[f64]) -> bool {
52 values.iter().all(|value| value.is_finite())
53 }
54
55 fn valid_runtime(&self) -> bool {
56 Self::finite_values(&[
57 self.v,
58 self.theta,
59 self.eta,
60 self.tau_m,
61 self.tau_eta,
62 self.delta_v,
63 self.lambda_0,
64 self.eta_increment,
65 self.v_rest,
66 self.v_reset,
67 self.dt,
68 ]) && self.tau_m > 0.0
69 && self.tau_eta > 0.0
70 && self.delta_v > 0.0
71 && self.lambda_0 >= 0.0
72 && self.dt > 0.0
73 }
74
75 fn advance_subthreshold(&self, current: f64) -> Option<(f64, f64)> {
76 let eta_decay = (-self.dt / self.tau_eta).exp();
77 let membrane_decay = (-self.dt / self.tau_m).exp();
78 let x0 = self.v - self.v_rest - current;
79 let eta_new = self.eta * eta_decay;
80 let x_new = if (self.tau_m - self.tau_eta).abs() <= 1e-12 {
81 membrane_decay * (x0 - self.eta * self.dt / self.tau_m)
82 } else {
83 let coupling = self.tau_eta / (self.tau_eta - self.tau_m);
84 x0 * membrane_decay - self.eta * coupling * (eta_decay - membrane_decay)
85 };
86 let v_new = self.v_rest + current + x_new;
87 if Self::finite_values(&[v_new, eta_new]) {
88 Some((v_new, eta_new))
89 } else {
90 None
91 }
92 }
93
94 fn spike_probability(&self, voltage: f64) -> f64 {
95 if self.lambda_0 == 0.0 {
96 return 0.0;
97 }
98 let exponent = ((voltage - self.theta) / self.delta_v).clamp(-745.0, 20.0);
99 let hazard = self.lambda_0 * exponent.exp();
100 (1.0 - (-hazard * self.dt).exp()).clamp(0.0, 1.0)
101 }
102
103 pub fn step(&mut self, current: f64) -> i32 {
104 if !current.is_finite() || !self.valid_runtime() {
105 return 0;
106 }
107 let Some((v_candidate, eta_candidate)) = self.advance_subthreshold(current) else {
108 return 0;
109 };
110 self.v = v_candidate;
111 self.eta = eta_candidate;
112 if self.rng.random::<f64>() < self.spike_probability(self.v) {
113 self.v = self.v_reset;
114 self.eta += self.eta_increment;
115 1
116 } else {
117 0
118 }
119 }
120
121 pub fn reset(&mut self) {
122 self.v = self.v_rest;
123 self.eta = 0.0;
124 self.rng = Xoshiro256PlusPlus::seed_from_u64(self.seed);
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn equal_time_constants_use_limit_solution() {
134 let mut n = GIFPopulationNeuron::new(42);
135 n.tau_eta = n.tau_m;
136 assert_eq!(n.step(0.0), 0);
137 assert!(n.v.is_finite());
138 assert!(n.eta.is_finite());
139 }
140
141 #[test]
142 fn zero_escape_rate_remains_subthreshold() {
143 let mut n = GIFPopulationNeuron::new(42);
144 n.lambda_0 = 0.0;
145 assert_eq!(n.step(30.0), 0);
146 assert_ne!(n.v, n.v_reset);
147 }
148
149 #[test]
150 fn nonfinite_subthreshold_candidate_preserves_state() {
151 let mut n = GIFPopulationNeuron::new(42);
152 n.v = f64::MAX;
153 n.v_rest = -f64::MAX;
154 let before = (n.v, n.eta);
155 assert_eq!(n.step(f64::MAX), 0);
156 assert_eq!((n.v, n.eta), before);
157 }
158
159 #[test]
160 fn gif_pop_fires() {
161 let mut n = GIFPopulationNeuron::new(42);
162 let t: i32 = (0..1000).map(|_| n.step(30.0)).sum();
163 assert!(t > 0);
164 }
165
166 #[test]
168 fn gif_pop_exact_subthreshold_reference_point() {
169 let mut n = GIFPopulationNeuron::new(7);
170 n.v = -68.0;
171 n.eta = 0.4;
172 assert_eq!(n.step(4.0), 0);
173 assert!((n.v - (-67.8370206677805)).abs() < 1e-12);
174 assert!((n.eta - 0.398004991677073).abs() < 1e-15);
175 }
176 #[test]
177 fn gif_pop_forced_spike_adds_decayed_adaptation() {
178 let mut n = GIFPopulationNeuron::new(42);
179 n.v = -51.0;
180 n.eta = 0.3;
181 n.theta = -90.0;
182 n.lambda_0 = 1.0e9;
183 assert_eq!(n.step(0.0), 1);
184 assert!((n.v - n.v_reset).abs() < 1e-12);
185 assert!((n.eta - 5.298503743757805).abs() < 1e-15);
186 }
187 #[test]
188 fn gif_pop_invalid_input_preserves_state() {
189 let mut n = GIFPopulationNeuron::new(42);
190 n.v = -62.0;
191 n.eta = 0.75;
192 let before = (n.v, n.eta);
193 assert_eq!(n.step(f64::NAN), 0);
194 n.tau_m = 0.0;
195 assert_eq!(n.step(1.0), 0);
196 assert_eq!((n.v, n.eta), before);
197 }
198 #[test]
199 fn gif_pop_seeded_reset_replays() {
200 let mut n = GIFPopulationNeuron::new(123);
201 n.theta = -90.0;
202 n.lambda_0 = 1.0e9;
203 let first: Vec<i32> = (0..3).map(|_| n.step(0.0)).collect();
204 n.reset();
205 let replay: Vec<i32> = (0..3).map(|_| n.step(0.0)).collect();
206 assert_eq!(first, replay);
207 assert!(n.eta > n.eta_increment);
208 }
209 #[test]
210 fn gif_pop_negative_drive_remains_finite() {
211 let mut n = GIFPopulationNeuron::new(42);
212 for _ in 0..200 {
213 n.step(-30.0);
214 }
215 assert!(n.v.is_finite());
216 assert!(n.eta.is_finite());
217 }
218}