1use rand::RngExt;
14use rand::SeedableRng;
15use rand_xoshiro::Xoshiro256PlusPlus;
16
17const A: f64 = 270.0;
18const B: f64 = 108.0;
19const D: f64 = 0.154;
20
21#[derive(Clone, Debug)]
23pub struct WongWangUnit {
24 pub s1: f64,
25 pub s2: f64,
26 pub noise1: f64,
27 pub noise2: f64,
28 pub tau_s: f64,
29 pub tau_ampa: f64,
30 pub gamma: f64,
31 pub j_n: f64,
32 pub j_cross: f64,
33 pub i_0: f64,
34 pub sigma: f64,
35 pub dt: f64,
36 rng: Xoshiro256PlusPlus,
37}
38
39impl WongWangUnit {
40 pub fn new(seed: u64) -> Self {
42 Self {
43 s1: 0.1,
44 s2: 0.1,
45 noise1: 0.0,
46 noise2: 0.0,
47 tau_s: 0.1,
48 tau_ampa: 0.002,
49 gamma: 0.641,
50 j_n: 0.2609,
51 j_cross: 0.0497,
52 i_0: 0.3255,
53 sigma: 0.02,
54 dt: 0.0001,
55 rng: Xoshiro256PlusPlus::seed_from_u64(seed),
56 }
57 }
58
59 #[allow(clippy::too_many_arguments)]
61 pub fn with_parameters(
62 s1: f64,
63 s2: f64,
64 noise1: f64,
65 noise2: f64,
66 tau_s: f64,
67 tau_ampa: f64,
68 gamma: f64,
69 j_n: f64,
70 j_cross: f64,
71 i_0: f64,
72 sigma: f64,
73 dt: f64,
74 seed: u64,
75 ) -> Result<Self, String> {
76 let unit = Self {
77 s1,
78 s2,
79 noise1,
80 noise2,
81 tau_s,
82 tau_ampa,
83 gamma,
84 j_n,
85 j_cross,
86 i_0,
87 sigma,
88 dt,
89 rng: Xoshiro256PlusPlus::seed_from_u64(seed),
90 };
91 unit.validate()?;
92 Ok(unit)
93 }
94
95 fn validate(&self) -> Result<(), String> {
96 let finite = [
97 self.s1,
98 self.s2,
99 self.noise1,
100 self.noise2,
101 self.tau_s,
102 self.tau_ampa,
103 self.gamma,
104 self.j_n,
105 self.j_cross,
106 self.i_0,
107 self.sigma,
108 self.dt,
109 ];
110 if !finite.iter().all(|value| value.is_finite()) {
111 return Err("Wong-Wang state and parameters must be finite".into());
112 }
113 if !(0.0..=1.0).contains(&self.s1) || !(0.0..=1.0).contains(&self.s2) {
114 return Err("Wong-Wang gating state must remain in [0, 1]".into());
115 }
116 if self.tau_s <= 0.0 || self.tau_ampa <= 0.0 || self.gamma <= 0.0 || self.dt <= 0.0 {
117 return Err("Wong-Wang time constants, gamma, and dt must be positive".into());
118 }
119 if self.j_n < 0.0 || self.j_cross < 0.0 || self.sigma < 0.0 {
120 return Err("Wong-Wang couplings and sigma must be non-negative".into());
121 }
122 Ok(())
123 }
124
125 #[inline]
126 fn phi(i_syn: f64) -> Result<f64, String> {
127 if !i_syn.is_finite() {
128 return Err("Wong-Wang synaptic current must be finite".into());
129 }
130 let x = A * i_syn - B;
131 let scaled = -D * x;
132 let response = if scaled > 700.0 {
133 0.0
134 } else if x.abs() < 1.0e-7 {
135 1.0 / D
136 } else {
137 x / -scaled.exp_m1()
138 };
139 if !response.is_finite() {
140 return Err("Wong-Wang transfer response must be finite".into());
141 }
142 Ok(response.max(0.0))
143 }
144
145 pub fn step_with_gaussian_samples(
147 &mut self,
148 stim1: f64,
149 stim2: f64,
150 xi1: f64,
151 xi2: f64,
152 ) -> Result<(f64, f64), String> {
153 self.validate()?;
154 if ![stim1, stim2, xi1, xi2]
155 .iter()
156 .all(|value| value.is_finite())
157 {
158 return Err("Wong-Wang stimuli and Gaussian samples must be finite".into());
159 }
160 let current1 = self.j_n * self.s1 - self.j_cross * self.s2 + self.i_0 + stim1 + self.noise1;
161 let current2 = self.j_n * self.s2 - self.j_cross * self.s1 + self.i_0 + stim2 + self.noise2;
162 let rate1 = Self::phi(current1)?;
163 let rate2 = Self::phi(current2)?;
164 let ds1 = -self.s1 / self.tau_s + (1.0 - self.s1) * self.gamma * rate1;
165 let ds2 = -self.s2 / self.tau_s + (1.0 - self.s2) * self.gamma * rate2;
166 let noise_scale = (self.dt / self.tau_ampa).sqrt() * self.sigma;
167 let next_s1 = self.s1 + self.dt * ds1;
168 let next_s2 = self.s2 + self.dt * ds2;
169 let next_noise1 = self.noise1 - (self.dt / self.tau_ampa) * self.noise1 + noise_scale * xi1;
170 let next_noise2 = self.noise2 - (self.dt / self.tau_ampa) * self.noise2 + noise_scale * xi2;
171 if ![next_s1, next_s2, next_noise1, next_noise2]
172 .iter()
173 .all(|value| value.is_finite())
174 {
175 return Err("Wong-Wang candidate state must remain finite".into());
176 }
177 if !(0.0..=1.0).contains(&next_s1) || !(0.0..=1.0).contains(&next_s2) {
178 return Err("Wong-Wang candidate gating state left [0, 1]".into());
179 }
180 self.s1 = next_s1;
181 self.s2 = next_s2;
182 self.noise1 = next_noise1;
183 self.noise2 = next_noise2;
184 Ok((rate1, rate2))
185 }
186
187 pub fn step(&mut self, stim1: f64, stim2: f64) -> Result<(f64, f64), String> {
189 let xi1 = self.randn();
190 let xi2 = self.randn();
191 self.step_with_gaussian_samples(stim1, stim2, xi1, xi2)
192 }
193
194 fn randn(&mut self) -> f64 {
195 let u1 = self.rng.random::<f64>().max(f64::MIN_POSITIVE);
196 let u2 = self.rng.random::<f64>();
197 (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
198 }
199
200 pub fn reset(&mut self) {
202 self.s1 = 0.1;
203 self.s2 = 0.1;
204 self.noise1 = 0.0;
205 self.noise2 = 0.0;
206 }
207}
208
209pub struct WongWangTrace {
211 pub s1: Vec<f64>,
212 pub s2: Vec<f64>,
213 pub noise1: Vec<f64>,
214 pub noise2: Vec<f64>,
215 pub rate1: Vec<f64>,
216 pub rate2: Vec<f64>,
217 pub final_s1: f64,
218 pub final_s2: f64,
219 pub final_noise1: f64,
220 pub final_noise2: f64,
221}
222
223#[allow(clippy::too_many_arguments)]
225pub fn simulate(
226 s1: f64,
227 s2: f64,
228 noise1: f64,
229 noise2: f64,
230 tau_s: f64,
231 tau_ampa: f64,
232 gamma: f64,
233 j_n: f64,
234 j_cross: f64,
235 i_0: f64,
236 sigma: f64,
237 dt: f64,
238 stim1: &[f64],
239 stim2: &[f64],
240 xi: &[f64],
241) -> Result<WongWangTrace, String> {
242 let n_steps = stim1.len();
243 if stim2.len() != n_steps {
244 return Err(format!(
245 "stim1 and stim2 length mismatch: {n_steps} vs {}",
246 stim2.len()
247 ));
248 }
249 if xi.len() != 2 * n_steps {
250 return Err(format!(
251 "xi length must be 2 * n_steps ({}): got {}",
252 2 * n_steps,
253 xi.len()
254 ));
255 }
256 let mut unit = WongWangUnit::with_parameters(
257 s1, s2, noise1, noise2, tau_s, tau_ampa, gamma, j_n, j_cross, i_0, sigma, dt, 0,
258 )?;
259 let mut trace = WongWangTrace {
260 s1: Vec::with_capacity(n_steps),
261 s2: Vec::with_capacity(n_steps),
262 noise1: Vec::with_capacity(n_steps),
263 noise2: Vec::with_capacity(n_steps),
264 rate1: Vec::with_capacity(n_steps),
265 rate2: Vec::with_capacity(n_steps),
266 final_s1: s1,
267 final_s2: s2,
268 final_noise1: noise1,
269 final_noise2: noise2,
270 };
271 for step in 0..n_steps {
272 let (rate1, rate2) = unit.step_with_gaussian_samples(
273 stim1[step],
274 stim2[step],
275 xi[2 * step],
276 xi[2 * step + 1],
277 )?;
278 trace.s1.push(unit.s1);
279 trace.s2.push(unit.s2);
280 trace.noise1.push(unit.noise1);
281 trace.noise2.push(unit.noise2);
282 trace.rate1.push(rate1);
283 trace.rate2.push(rate2);
284 }
285 trace.final_s1 = unit.s1;
286 trace.final_s2 = unit.s2;
287 trace.final_noise1 = unit.noise1;
288 trace.final_noise2 = unit.noise2;
289 Ok(trace)
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 #[test]
297 fn one_step_matches_appendix_euler_and_ou_equations() {
298 let mut unit = WongWangUnit::with_parameters(
299 0.1, 0.2, 0.01, -0.02, 0.1, 0.002, 0.641, 0.2609, 0.0497, 0.3255, 0.02, 0.0001, 3,
300 )
301 .unwrap();
302 let old = (unit.s1, unit.s2, unit.noise1, unit.noise2);
303 let (rate1, rate2) = unit
304 .step_with_gaussian_samples(0.03, -0.01, 0.5, -1.0)
305 .unwrap();
306 let expected_s1 = old.0 + 0.0001 * (-old.0 / 0.1 + (1.0 - old.0) * 0.641 * rate1);
307 let expected_s2 = old.1 + 0.0001 * (-old.1 / 0.1 + (1.0 - old.1) * 0.641 * rate2);
308 let scale = (0.0001_f64 / 0.002).sqrt() * 0.02;
309 assert_eq!(unit.s1, expected_s1);
310 assert_eq!(unit.s2, expected_s2);
311 assert_eq!(unit.noise1, old.2 - 0.05 * old.2 + scale * 0.5);
312 assert_eq!(unit.noise2, old.3 - 0.05 * old.3 - scale);
313 }
314
315 #[test]
316 fn batch_matches_scalar_and_preserves_empty_initial_state() {
317 let empty = simulate(
318 0.2,
319 0.3,
320 0.01,
321 -0.02,
322 0.1,
323 0.002,
324 0.641,
325 0.2609,
326 0.0497,
327 0.3255,
328 0.02,
329 0.0001,
330 &[],
331 &[],
332 &[],
333 )
334 .unwrap();
335 assert!(empty.s1.is_empty());
336 assert_eq!(empty.final_s1, 0.2);
337 assert_eq!(empty.final_noise2, -0.02);
338
339 let stim1 = [0.02, 0.01, -0.01];
340 let stim2 = [-0.01, 0.0, 0.03];
341 let xi = [0.5, -1.0, 0.25, 0.75, -0.5, 0.0];
342 let batch = simulate(
343 0.2, 0.3, 0.01, -0.02, 0.12, 0.003, 0.7, 0.28, 0.06, 0.31, 0.015, 0.0002, &stim1,
344 &stim2, &xi,
345 )
346 .unwrap();
347 let mut scalar = WongWangUnit::with_parameters(
348 0.2, 0.3, 0.01, -0.02, 0.12, 0.003, 0.7, 0.28, 0.06, 0.31, 0.015, 0.0002, 0,
349 )
350 .unwrap();
351 for step in 0..stim1.len() {
352 let rates = scalar
353 .step_with_gaussian_samples(
354 stim1[step],
355 stim2[step],
356 xi[2 * step],
357 xi[2 * step + 1],
358 )
359 .unwrap();
360 assert_eq!(batch.s1[step], scalar.s1);
361 assert_eq!(batch.s2[step], scalar.s2);
362 assert_eq!(batch.noise1[step], scalar.noise1);
363 assert_eq!(batch.noise2[step], scalar.noise2);
364 assert_eq!((batch.rate1[step], batch.rate2[step]), rates);
365 }
366 assert_eq!(batch.final_s1, scalar.s1);
367 assert_eq!(batch.final_s2, scalar.s2);
368 assert_eq!(batch.final_noise1, scalar.noise1);
369 assert_eq!(batch.final_noise2, scalar.noise2);
370 }
371
372 #[test]
373 fn invalid_input_is_rejected_before_state_commit() {
374 let mut unit = WongWangUnit::new(5);
375 let before = (unit.s1, unit.s2, unit.noise1, unit.noise2);
376 assert!(unit
377 .step_with_gaussian_samples(f64::NAN, 0.0, 0.0, 0.0)
378 .is_err());
379 assert_eq!((unit.s1, unit.s2, unit.noise1, unit.noise2), before);
380 assert!(simulate(
381 0.1,
382 0.1,
383 0.0,
384 0.0,
385 0.1,
386 0.002,
387 0.641,
388 0.2609,
389 0.0497,
390 0.3255,
391 0.02,
392 0.0001,
393 &[0.0],
394 &[0.0],
395 &[],
396 )
397 .is_err());
398 }
399
400 #[test]
401 fn reset_preserves_configured_parameters() {
402 let mut unit = WongWangUnit::with_parameters(
403 0.2, 0.3, 0.01, -0.02, 0.12, 0.003, 0.7, 0.3, 0.04, 0.31, 0.03, 0.0002, 7,
404 )
405 .unwrap();
406 unit.reset();
407 assert_eq!(
408 (unit.s1, unit.s2, unit.noise1, unit.noise2),
409 (0.1, 0.1, 0.0, 0.0)
410 );
411 assert_eq!((unit.tau_s, unit.tau_ampa, unit.dt), (0.12, 0.003, 0.0002));
412 }
413}