1#[derive(Clone, Debug)]
13pub struct JansenRitUnit {
14 pub y: [f64; 6],
15 pub a_exc: f64,
16 pub b_exc: f64,
17 pub a_rate: f64,
18 pub b_rate: f64,
19 pub c: f64,
20 pub e0: f64,
21 pub v0: f64,
22 pub r: f64,
23 pub dt: f64,
24}
25
26impl JansenRitUnit {
27 pub fn new() -> Self {
29 Self {
30 y: [0.0; 6],
31 a_exc: 3.25,
32 b_exc: 22.0,
33 a_rate: 100.0,
34 b_rate: 50.0,
35 c: 135.0,
36 e0: 2.5,
37 v0: 6.0,
38 r: 0.56,
39 dt: 0.0001,
40 }
41 }
42
43 #[allow(clippy::too_many_arguments)]
45 pub fn with_parameters(
46 y0: f64,
47 y3: f64,
48 y1: f64,
49 y4: f64,
50 y2: f64,
51 y5: f64,
52 a_exc: f64,
53 b_exc: f64,
54 a_rate: f64,
55 b_rate: f64,
56 c: f64,
57 e0: f64,
58 v0: f64,
59 r: f64,
60 dt: f64,
61 ) -> Result<Self, String> {
62 let unit = Self {
63 y: [y0, y1, y2, y3, y4, y5],
64 a_exc,
65 b_exc,
66 a_rate,
67 b_rate,
68 c,
69 e0,
70 v0,
71 r,
72 dt,
73 };
74 unit.validate()?;
75 Ok(unit)
76 }
77
78 fn validate(&self) -> Result<(), String> {
79 let parameters = [
80 self.a_exc,
81 self.b_exc,
82 self.a_rate,
83 self.b_rate,
84 self.c,
85 self.e0,
86 self.v0,
87 self.r,
88 self.dt,
89 ];
90 if !self
91 .y
92 .iter()
93 .chain(parameters.iter())
94 .all(|value| value.is_finite())
95 {
96 return Err("Jansen–Rit state and parameters must be finite".into());
97 }
98 if self.a_exc <= 0.0
99 || self.b_exc <= 0.0
100 || self.a_rate <= 0.0
101 || self.b_rate <= 0.0
102 || self.e0 <= 0.0
103 || self.r <= 0.0
104 || self.dt <= 0.0
105 {
106 return Err(
107 "Jansen–Rit gains, rates, sigmoid scale, slope, and dt must be positive".into(),
108 );
109 }
110 if self.c < 0.0 {
111 return Err("Jansen–Rit connectivity must be non-negative".into());
112 }
113 Ok(())
114 }
115
116 #[inline]
117 fn sigmoid(&self, voltage: f64) -> Result<f64, String> {
118 if !voltage.is_finite() {
119 return Err("Jansen–Rit sigmoid input must be finite".into());
120 }
121 let exponent = self.r * (self.v0 - voltage);
122 let response = if exponent >= 0.0 {
123 let exp_neg = (-exponent).exp();
124 2.0 * self.e0 * exp_neg / (1.0 + exp_neg)
125 } else {
126 2.0 * self.e0 / (1.0 + exponent.exp())
127 };
128 if !response.is_finite() {
129 return Err("Jansen–Rit sigmoid response must be finite".into());
130 }
131 Ok(response)
132 }
133
134 pub fn step(&mut self, p_ext: f64) -> Result<f64, String> {
136 self.validate()?;
137 if !p_ext.is_finite() {
138 return Err("Jansen–Rit external drive must be finite".into());
139 }
140 let c1 = self.c;
141 let c2 = 0.8 * c1;
142 let c3 = 0.25 * c1;
143 let c4 = 0.25 * c1;
144 let s_pyramidal = self.sigmoid(self.y[1] - self.y[2])?;
145 let s_excitatory = self.sigmoid(c1 * self.y[0])?;
146 let s_inhibitory = self.sigmoid(c3 * self.y[0])?;
147 let derivatives = [
148 self.y[3],
149 self.y[4],
150 self.y[5],
151 self.a_exc * self.a_rate * s_pyramidal
152 - 2.0 * self.a_rate * self.y[3]
153 - self.a_rate.powi(2) * self.y[0],
154 self.a_exc * self.a_rate * (p_ext + c2 * s_excitatory)
155 - 2.0 * self.a_rate * self.y[4]
156 - self.a_rate.powi(2) * self.y[1],
157 self.b_exc * self.b_rate * c4 * s_inhibitory
158 - 2.0 * self.b_rate * self.y[5]
159 - self.b_rate.powi(2) * self.y[2],
160 ];
161 let mut candidate = self.y;
162 for (next, derivative) in candidate.iter_mut().zip(derivatives) {
163 *next += self.dt * derivative;
164 }
165 if !candidate.iter().all(|value| value.is_finite()) {
166 return Err("Jansen–Rit candidate state must remain finite".into());
167 }
168 self.y = candidate;
169 Ok(self.y[1] - self.y[2])
170 }
171
172 pub fn reset(&mut self) {
174 self.y = [0.0; 6];
175 }
176}
177
178impl Default for JansenRitUnit {
179 fn default() -> Self {
180 Self::new()
181 }
182}
183
184pub struct JansenRitTrace {
186 pub y0: Vec<f64>,
187 pub y3: Vec<f64>,
188 pub y1: Vec<f64>,
189 pub y4: Vec<f64>,
190 pub y2: Vec<f64>,
191 pub y5: Vec<f64>,
192 pub eeg: Vec<f64>,
193 pub final_state: [f64; 6],
194}
195
196#[allow(clippy::too_many_arguments)]
198pub fn simulate(
199 y0: f64,
200 y3: f64,
201 y1: f64,
202 y4: f64,
203 y2: f64,
204 y5: f64,
205 a_exc: f64,
206 b_exc: f64,
207 a_rate: f64,
208 b_rate: f64,
209 c: f64,
210 e0: f64,
211 v0: f64,
212 r: f64,
213 dt: f64,
214 p_ext: &[f64],
215) -> Result<JansenRitTrace, String> {
216 let mut unit = JansenRitUnit::with_parameters(
217 y0, y3, y1, y4, y2, y5, a_exc, b_exc, a_rate, b_rate, c, e0, v0, r, dt,
218 )?;
219 let mut trace = JansenRitTrace {
220 y0: Vec::with_capacity(p_ext.len()),
221 y3: Vec::with_capacity(p_ext.len()),
222 y1: Vec::with_capacity(p_ext.len()),
223 y4: Vec::with_capacity(p_ext.len()),
224 y2: Vec::with_capacity(p_ext.len()),
225 y5: Vec::with_capacity(p_ext.len()),
226 eeg: Vec::with_capacity(p_ext.len()),
227 final_state: unit.y,
228 };
229 for drive in p_ext {
230 let eeg = unit.step(*drive)?;
231 trace.y0.push(unit.y[0]);
232 trace.y3.push(unit.y[3]);
233 trace.y1.push(unit.y[1]);
234 trace.y4.push(unit.y[4]);
235 trace.y2.push(unit.y[2]);
236 trace.y5.push(unit.y[5]);
237 trace.eeg.push(eeg);
238 }
239 trace.final_state = unit.y;
240 Ok(trace)
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 #[test]
248 fn one_step_uses_c1_inside_and_c2_outside_excitatory_sigmoid() {
249 let mut unit = JansenRitUnit::with_parameters(
250 0.1, 0.2, 0.3, -0.4, -0.1, 0.5, 3.25, 22.0, 100.0, 50.0, 135.0, 2.5, 6.0, 0.56, 0.0001,
251 )
252 .unwrap();
253 let old = unit.y;
254 let se = unit.sigmoid(135.0 * old[0]).unwrap();
255 let expected_y4 = old[4]
256 + 0.0001
257 * (3.25 * 100.0 * (220.0 + 0.8 * 135.0 * se)
258 - 2.0 * 100.0 * old[4]
259 - 100.0_f64.powi(2) * old[1]);
260 unit.step(220.0).unwrap();
261 assert_eq!(unit.y[4], expected_y4);
262 }
263
264 #[test]
265 fn batch_matches_scalar_and_preserves_empty_initial_state() {
266 let empty = simulate(
267 0.1,
268 0.2,
269 0.3,
270 -0.4,
271 -0.1,
272 0.5,
273 3.25,
274 22.0,
275 100.0,
276 50.0,
277 135.0,
278 2.5,
279 6.0,
280 0.56,
281 0.0001,
282 &[],
283 )
284 .unwrap();
285 assert!(empty.eeg.is_empty());
286 assert_eq!(empty.final_state, [0.1, 0.3, -0.1, 0.2, -0.4, 0.5]);
287
288 let drives = [120.0, 220.0, 320.0];
289 let batch = simulate(
290 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 3.25, 22.0, 100.0, 50.0, 135.0, 2.5, 6.0, 0.56, 0.0001,
291 &drives,
292 )
293 .unwrap();
294 let mut scalar = JansenRitUnit::new();
295 for drive in drives {
296 scalar.step(drive).unwrap();
297 }
298 assert_eq!(batch.final_state, scalar.y);
299 }
300
301 #[test]
302 fn invalid_input_does_not_mutate_state() {
303 let mut unit = JansenRitUnit::new();
304 let before = unit.y;
305 assert!(unit.step(f64::NAN).is_err());
306 assert_eq!(unit.y, before);
307 }
308}