1const A: f64 = 270.0;
27const B: f64 = 108.0;
28const D: f64 = 0.154;
29
30#[inline]
31fn phi(i_syn: f64) -> f64 {
32 let x = A * i_syn - B;
33 if x.abs() < 1e-6 {
34 1.0 / D
35 } else {
36 x / (1.0 - (-D * x).exp())
37 }
38}
39
40#[allow(clippy::too_many_arguments)]
44pub fn simulate(
45 mut s1: f64,
46 mut s2: f64,
47 tau_s: f64,
48 gamma: f64,
49 j_n: f64,
50 j_cross: f64,
51 i_0: f64,
52 sigma: f64,
53 dt: f64,
54 stim1: &[f64],
55 stim2: &[f64],
56 xi: &[f64],
57 s1_out: &mut [f64],
58 s2_out: &mut [f64],
59 r1_out: &mut [f64],
60 r2_out: &mut [f64],
61) -> (f64, f64) {
62 let n = stim1.len();
63 assert_eq!(stim2.len(), n, "stim2 length mismatch");
64 assert_eq!(xi.len(), 2 * n, "xi length must be 2 * n_steps");
65 assert_eq!(s1_out.len(), n, "s1_out length mismatch");
66 assert_eq!(s2_out.len(), n, "s2_out length mismatch");
67 assert_eq!(r1_out.len(), n, "r1_out length mismatch");
68 assert_eq!(r2_out.len(), n, "r2_out length mismatch");
69
70 for t in 0..n {
71 let xi1 = xi[2 * t];
72 let xi2 = xi[2 * t + 1];
73 let i1 = j_n * s1 - j_cross * s2 + i_0 + stim1[t] + sigma * xi1;
74 let i2 = j_n * s2 - j_cross * s1 + i_0 + stim2[t] + sigma * xi2;
75 let r1 = phi(i1);
76 let r2 = phi(i2);
77 s1 += (-s1 / tau_s + (1.0 - s1) * gamma * r1) * dt;
78 s2 += (-s2 / tau_s + (1.0 - s2) * gamma * r2) * dt;
79 s1 = s1.clamp(0.0, 1.0);
80 s2 = s2.clamp(0.0, 1.0);
81 s1_out[t] = s1;
82 s2_out[t] = s2;
83 r1_out[t] = r1;
84 r2_out[t] = r2;
85 }
86 (s1, s2)
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 fn params() -> (f64, f64, f64, f64, f64, f64, f64) {
94 (0.1, 0.641, 0.2609, 0.0497, 0.3255, 0.0, 0.001)
96 }
97
98 #[test]
99 fn phi_singularity_guard_returns_finite() {
100 let r = phi(B / A);
102 assert!(r.is_finite());
103 assert!((r - 1.0 / D).abs() < 1e-6);
104 }
105
106 #[test]
107 fn phi_monotone_increasing() {
108 let lo = phi(0.5);
109 let hi = phi(1.0);
110 assert!(hi > lo);
111 }
112
113 #[test]
114 fn zero_noise_zero_stim_converges_to_fixed_point() {
115 let (tau_s, gamma, j_n, j_cross, i_0, _, dt) = params();
118 let n = 10_000;
119 let stim = vec![0.0_f64; n];
120 let xi = vec![0.0_f64; 2 * n];
121 let mut s1o = vec![0.0_f64; n];
122 let mut s2o = vec![0.0_f64; n];
123 let mut r1o = vec![0.0_f64; n];
124 let mut r2o = vec![0.0_f64; n];
125 let (s1_f, s2_f) = simulate(
126 0.1, 0.1, tau_s, gamma, j_n, j_cross, i_0, 0.0, dt, &stim, &stim, &xi, &mut s1o,
127 &mut s2o, &mut r1o, &mut r2o,
128 );
129 assert!(s1_f.is_finite() && s2_f.is_finite());
130 assert!((0.0..=1.0).contains(&s1_f));
131 assert!((0.0..=1.0).contains(&s2_f));
132 assert!(
133 (s1_f - s2_f).abs() < 1e-9,
134 "symmetric init must stay symmetric under zero noise"
135 );
136 }
137
138 #[test]
139 fn biased_stimulus_drives_winner() {
140 let (tau_s, gamma, j_n, j_cross, i_0, _, dt) = params();
142 let n = 50_000;
143 let stim1 = vec![0.2_f64; n];
144 let stim2 = vec![0.0_f64; n];
145 let xi = vec![0.0_f64; 2 * n];
146 let mut s1o = vec![0.0_f64; n];
147 let mut s2o = vec![0.0_f64; n];
148 let mut r1o = vec![0.0_f64; n];
149 let mut r2o = vec![0.0_f64; n];
150 let (s1_f, s2_f) = simulate(
151 0.1, 0.1, tau_s, gamma, j_n, j_cross, i_0, 0.0, dt, &stim1, &stim2, &xi, &mut s1o,
152 &mut s2o, &mut r1o, &mut r2o,
153 );
154 assert!(s1_f > 0.5, "winner s1 should reach attractor; got {s1_f}");
155 assert!(s2_f < 0.2, "loser s2 should be suppressed; got {s2_f}");
156 }
157
158 #[test]
159 fn output_trace_shape_matches_input() {
160 let n = 128;
161 let stim = vec![0.1_f64; n];
162 let xi = vec![0.0_f64; 2 * n];
163 let mut s1o = vec![0.0_f64; n];
164 let mut s2o = vec![0.0_f64; n];
165 let mut r1o = vec![0.0_f64; n];
166 let mut r2o = vec![0.0_f64; n];
167 simulate(
168 0.1, 0.1, 0.1, 0.641, 0.2609, 0.0497, 0.3255, 0.0, 0.001, &stim, &stim, &xi, &mut s1o,
169 &mut s2o, &mut r1o, &mut r2o,
170 );
171 assert!(r1o.iter().all(|&r| r > 0.0));
175 assert!(r2o.iter().all(|&r| r > 0.0));
176 }
177
178 #[test]
179 #[should_panic(expected = "xi length must be 2 * n_steps")]
180 fn mismatched_xi_length_panics() {
181 let n = 10;
182 let stim = vec![0.0_f64; n];
183 let xi = vec![0.0_f64; n]; let mut s1o = vec![0.0_f64; n];
185 let mut s2o = vec![0.0_f64; n];
186 let mut r1o = vec![0.0_f64; n];
187 let mut r2o = vec![0.0_f64; n];
188 simulate(
189 0.1, 0.1, 0.1, 0.641, 0.2609, 0.0497, 0.3255, 0.0, 0.001, &stim, &stim, &xi, &mut s1o,
190 &mut s2o, &mut r1o, &mut r2o,
191 );
192 }
193}