1#[inline]
26fn sigmoid(a: f64, theta: f64, x: f64) -> f64 {
27 let baseline = 1.0 / (1.0 + (a * theta).exp());
31 1.0 / (1.0 + (-a * (x - theta)).exp()) - baseline
32}
33
34#[allow(clippy::too_many_arguments)]
38pub fn simulate(
39 mut e: f64,
40 mut i: f64,
41 w_ee: f64,
42 w_ei: f64,
43 w_ie: f64,
44 w_ii: f64,
45 tau_e: f64,
46 tau_i: f64,
47 a: f64,
48 theta: f64,
49 dt: f64,
50 ext_input: &[f64],
51 e_out: &mut [f64],
52 i_out: &mut [f64],
53) -> (f64, f64) {
54 let n = ext_input.len();
55 assert_eq!(e_out.len(), n, "e_out length mismatch");
56 assert_eq!(i_out.len(), n, "i_out length mismatch");
57
58 for t in 0..n {
59 let s_e = sigmoid(a, theta, w_ee * e - w_ei * i + ext_input[t]);
60 let s_i = sigmoid(a, theta, w_ie * e - w_ii * i);
61 e += (-e + s_e) / tau_e * dt;
62 i += (-i + s_i) / tau_i * dt;
63 e_out[t] = e;
64 i_out[t] = i;
65 }
66 (e, i)
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 fn defaults() -> (f64, f64, f64, f64, f64, f64, f64, f64, f64) {
74 (10.0, 6.0, 10.0, 1.0, 1.0, 2.0, 1.2, 4.0, 0.1)
76 }
77
78 #[test]
79 fn sigmoid_monotone_increasing() {
80 let (_, _, _, _, _, _, a, theta, _) = defaults();
81 let lo = sigmoid(a, theta, 0.0);
82 let mid = sigmoid(a, theta, 4.0);
83 let hi = sigmoid(a, theta, 10.0);
84 assert!(lo < mid && mid < hi);
85 }
86
87 #[test]
88 fn sigmoid_at_zero_is_zero() {
89 let (_, _, _, _, _, _, a, theta, _) = defaults();
91 assert!(sigmoid(a, theta, 0.0).abs() < 1e-12);
92 }
93
94 #[test]
95 fn sigmoid_at_theta_equals_half_minus_baseline() {
96 let (_, _, _, _, _, _, a, theta, _) = defaults();
97 let baseline = 1.0 / (1.0 + (a * theta).exp());
98 let r = sigmoid(a, theta, theta);
99 assert!((r - (0.5 - baseline)).abs() < 1e-12);
100 }
101
102 #[test]
103 fn sigmoid_asymptotes_respect_baseline() {
104 let (_, _, _, _, _, _, a, theta, _) = defaults();
106 let baseline = 1.0 / (1.0 + (a * theta).exp());
107 assert!((sigmoid(a, theta, 1e6) - (1.0 - baseline)).abs() < 1e-50);
108 assert!((sigmoid(a, theta, -1e6) - (-baseline)).abs() < 1e-50);
109 }
110
111 #[test]
112 fn quiescent_converges() {
113 let (w_ee, w_ei, w_ie, w_ii, tau_e, tau_i, a, theta, dt) = defaults();
114 let n = 20_000;
115 let ext = vec![0.0_f64; n];
116 let mut e_out = vec![0.0_f64; n];
117 let mut i_out = vec![0.0_f64; n];
118 let (e_f, i_f) = simulate(
119 0.1, 0.05, w_ee, w_ei, w_ie, w_ii, tau_e, tau_i, a, theta, dt, &ext, &mut e_out,
120 &mut i_out,
121 );
122 assert!(e_f.is_finite() && i_f.is_finite());
123 assert!(e_f < 0.2, "quiescent E must stay low, got {e_f}");
124 assert!(i_f < 0.2, "quiescent I must stay low, got {i_f}");
125 }
126
127 #[test]
128 fn strong_drive_elevates_activity() {
129 let (w_ee, w_ei, w_ie, w_ii, tau_e, tau_i, a, theta, dt) = defaults();
130 let n = 10_000;
131 let ext = vec![10.0_f64; n];
132 let mut e_out = vec![0.0_f64; n];
133 let mut i_out = vec![0.0_f64; n];
134 let (e_f, _) = simulate(
135 0.1, 0.05, w_ee, w_ei, w_ie, w_ii, tau_e, tau_i, a, theta, dt, &ext, &mut e_out,
136 &mut i_out,
137 );
138 assert!(e_f > 0.3, "strong external drive must elevate E, got {e_f}");
139 }
140
141 #[test]
142 fn output_trace_shape_matches_input() {
143 let n = 64;
144 let ext = vec![1.0_f64; n];
145 let mut e_out = vec![f64::NAN; n];
146 let mut i_out = vec![f64::NAN; n];
147 simulate(
148 0.1, 0.05, 10.0, 6.0, 10.0, 1.0, 1.0, 2.0, 1.2, 4.0, 0.1, &ext, &mut e_out, &mut i_out,
149 );
150 assert!(e_out.iter().all(|v| v.is_finite()));
151 assert!(i_out.iter().all(|v| v.is_finite()));
152 }
153
154 #[test]
155 #[should_panic(expected = "e_out length mismatch")]
156 fn mismatched_e_out_panics() {
157 let n = 10;
158 let ext = vec![0.0_f64; n];
159 let mut e_out = vec![0.0_f64; n + 1];
160 let mut i_out = vec![0.0_f64; n];
161 simulate(
162 0.1, 0.05, 10.0, 6.0, 10.0, 1.0, 1.0, 2.0, 1.2, 4.0, 0.1, &ext, &mut e_out, &mut i_out,
163 );
164 }
165}