1#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum HrfModel {
18 Spm,
19 SpmDerivative,
20 SpmDerivativeDispersion,
21 Glover,
22 GloverDerivative,
23 GloverDerivativeDispersion,
24 Fir,
25 None,
26}
27
28impl HrfModel {
29 pub fn parse(s: &str) -> Self {
30 match s.to_lowercase().as_str() {
31 "spm" => Self::Spm,
32 "spm + derivative" => Self::SpmDerivative,
33 "spm + derivative + dispersion" => Self::SpmDerivativeDispersion,
34 "glover" => Self::Glover,
35 "glover + derivative" => Self::GloverDerivative,
36 "glover + derivative + dispersion" => Self::GloverDerivativeDispersion,
37 "fir" => Self::Fir,
38 _ => Self::None,
39 }
40 }
41}
42
43struct HrfParams {
45 delay: f64,
46 undershoot: f64,
47 dispersion: f64,
48 u_dispersion: f64,
49 ratio: f64,
50}
51
52const SPM_PARAMS: HrfParams = HrfParams {
53 delay: 6.0,
54 undershoot: 16.0,
55 dispersion: 1.0,
56 u_dispersion: 1.0,
57 ratio: 0.167,
58};
59const GLOVER_PARAMS: HrfParams = HrfParams {
60 delay: 6.0,
61 undershoot: 12.0,
62 dispersion: 0.9,
63 u_dispersion: 0.9,
64 ratio: 0.35,
65};
66
67fn gamma_difference_hrf(
69 tr: f64,
70 oversampling: usize,
71 time_length: f64,
72 onset: f64,
73 p: &HrfParams,
74) -> Vec<f64> {
75 let dt = tr / oversampling as f64;
76 let n = (time_length / dt).round() as usize;
77 let mut hrf = Vec::with_capacity(n);
78 for i in 0..n {
80 let t = if n > 1 {
81 i as f64 * time_length / (n - 1) as f64
82 } else {
83 0.0
84 };
85 let t = t - onset;
86 if t <= 0.0 {
87 hrf.push(0.0);
88 continue;
89 }
90 let g1 = gamma_pdf(t, p.delay / p.dispersion, dt / p.dispersion);
96 let g2 = gamma_pdf(t, p.undershoot / p.u_dispersion, dt / p.u_dispersion);
97 hrf.push(g1 - p.ratio * g2);
98 }
99 let sum: f64 = hrf.iter().sum();
100 if sum.abs() > 1e-15 {
101 for v in &mut hrf {
102 *v /= sum;
103 }
104 }
105 hrf
106}
107
108fn gamma_pdf(x: f64, shape: f64, loc: f64) -> f64 {
113 let x = x - loc;
114 if x < 0.0 || shape <= 0.0 {
115 return 0.0;
116 }
117 if x == 0.0 {
118 return if shape == 1.0 {
119 1.0
120 } else if shape > 1.0 {
121 0.0
122 } else {
123 f64::INFINITY
124 };
125 }
126 let log_pdf = (shape - 1.0) * x.ln() - x - gammaln(shape);
128 log_pdf.exp()
129}
130
131fn gammaln(x: f64) -> f64 {
137 if x <= 0.0 {
138 return f64::INFINITY;
139 }
140 let mut xx = x;
141
142 if xx < 13.0 {
143 let mut z = 1.0;
144 while xx >= 3.0 {
145 xx -= 1.0;
146 z *= xx;
147 }
148 while xx < 2.0 {
149 if xx == 0.0 {
150 return f64::INFINITY;
151 }
152 z /= xx;
153 xx += 1.0;
154 }
155 if z < 0.0 {
156 z = -z;
157 }
158 if xx == 2.0 {
159 return z.ln();
160 }
161 xx -= 2.0;
162 const P: [f64; 8] = [
164 -1.716_185_138_865_495,
165 2.476_565_080_557_592e1,
166 -3.798_042_564_709_456_3e2,
167 6.293_311_553_128_184e2,
168 8.669_662_027_904_133e2,
169 -3.145_127_296_884_836_7e4,
170 -3.614_441_341_869_117_6e4,
171 6.645_614_382_024_054e4,
172 ];
173 const Q: [f64; 8] = [
174 -3.084_023_001_197_389_7e1,
175 3.153_506_269_796_041_6e2,
176 -1.015_156_367_490_219_2e3,
177 -3.107_771_671_572_311e3,
178 2.253_811_842_098_015e4,
179 4.755_846_277_527_881e3,
180 -1.346_599_598_649_693e5,
181 -1.151_322_596_755_534_9e5,
182 ];
183 let mut xnum = 0.0;
184 let mut xden = 1.0;
185 for i in 0..8 {
186 xnum = (xnum + P[i]) * xx;
187 xden = xden * xx + Q[i];
188 }
189 return (z * (xnum / xden + 1.0)).ln();
190 }
191
192 let q = (xx - 0.5) * xx.ln() - xx + 0.918_938_533_204_672_8;
194 if xx > 1.0e8 {
195 return q;
196 }
197 let inv_x = 1.0 / xx;
198 let inv_x2 = inv_x * inv_x;
199 const S: [f64; 6] = [
200 8.333_333_333_333_333e-2,
201 -2.777_777_777_777_778e-3,
202 7.936_507_936_507_937e-4,
203 -5.952_380_952_380_953e-4,
204 8.417_508_417_508_417e-4,
205 -1.917_526_917_526_917_6e-3,
206 ];
207 let mut s = S[5];
208 for i in (0..5).rev() {
209 s = s * inv_x2 + S[i];
210 }
211 q + s * inv_x
212}
213
214#[must_use]
216pub fn spm_hrf(tr: f64, oversampling: usize, time_length: f64, onset: f64) -> Vec<f64> {
217 gamma_difference_hrf(tr, oversampling, time_length, onset, &SPM_PARAMS)
218}
219
220#[must_use]
222pub fn glover_hrf(tr: f64, oversampling: usize, time_length: f64, onset: f64) -> Vec<f64> {
223 gamma_difference_hrf(tr, oversampling, time_length, onset, &GLOVER_PARAMS)
224}
225
226pub fn spm_time_derivative(tr: f64, oversampling: usize, time_length: f64, onset: f64) -> Vec<f64> {
228 let d = 0.1;
229 let h1 = spm_hrf(tr, oversampling, time_length, onset);
230 let h2 = spm_hrf(tr, oversampling, time_length, onset + d);
231 h1.iter().zip(&h2).map(|(a, b)| (a - b) / d).collect()
232}
233
234pub fn glover_time_derivative(
236 tr: f64,
237 oversampling: usize,
238 time_length: f64,
239 onset: f64,
240) -> Vec<f64> {
241 let d = 0.1;
242 let h1 = glover_hrf(tr, oversampling, time_length, onset);
243 let h2 = glover_hrf(tr, oversampling, time_length, onset + d);
244 h1.iter().zip(&h2).map(|(a, b)| (a - b) / d).collect()
245}
246
247pub fn spm_dispersion_derivative(
249 tr: f64,
250 oversampling: usize,
251 time_length: f64,
252 onset: f64,
253) -> Vec<f64> {
254 let dd = 0.01;
255 let h1 = gamma_difference_hrf(tr, oversampling, time_length, onset, &SPM_PARAMS);
256 let p2 = HrfParams {
257 dispersion: SPM_PARAMS.dispersion + dd,
258 ..SPM_PARAMS
259 };
260 let h2 = gamma_difference_hrf(tr, oversampling, time_length, onset, &p2);
261 h1.iter().zip(&h2).map(|(a, b)| (a - b) / dd).collect()
262}
263
264pub fn glover_dispersion_derivative(
266 tr: f64,
267 oversampling: usize,
268 time_length: f64,
269 onset: f64,
270) -> Vec<f64> {
271 let dd = 0.01;
272 let h1 = gamma_difference_hrf(tr, oversampling, time_length, onset, &GLOVER_PARAMS);
273 let p2 = HrfParams {
274 dispersion: GLOVER_PARAMS.dispersion + dd,
275 ..GLOVER_PARAMS
276 };
277 let h2 = gamma_difference_hrf(tr, oversampling, time_length, onset, &p2);
278 h1.iter().zip(&h2).map(|(a, b)| (a - b) / dd).collect()
279}
280
281#[must_use]
283pub fn hrf_kernel(
284 model: &HrfModel,
285 tr: f64,
286 oversampling: usize,
287 fir_delays: Option<&[f64]>,
288) -> Vec<Vec<f64>> {
289 match model {
290 HrfModel::Spm => vec![spm_hrf(tr, oversampling, 32.0, 0.0)],
291 HrfModel::SpmDerivative => vec![
292 spm_hrf(tr, oversampling, 32.0, 0.0),
293 spm_time_derivative(tr, oversampling, 32.0, 0.0),
294 ],
295 HrfModel::SpmDerivativeDispersion => vec![
296 spm_hrf(tr, oversampling, 32.0, 0.0),
297 spm_time_derivative(tr, oversampling, 32.0, 0.0),
298 spm_dispersion_derivative(tr, oversampling, 32.0, 0.0),
299 ],
300 HrfModel::Glover => vec![glover_hrf(tr, oversampling, 32.0, 0.0)],
301 HrfModel::GloverDerivative => vec![
302 glover_hrf(tr, oversampling, 32.0, 0.0),
303 glover_time_derivative(tr, oversampling, 32.0, 0.0),
304 ],
305 HrfModel::GloverDerivativeDispersion => vec![
306 glover_hrf(tr, oversampling, 32.0, 0.0),
307 glover_time_derivative(tr, oversampling, 32.0, 0.0),
308 glover_dispersion_derivative(tr, oversampling, 32.0, 0.0),
309 ],
310 HrfModel::Fir => fir_delays
311 .unwrap_or(&[])
312 .iter()
313 .map(|&delay| {
314 let d = delay as usize;
315 let mut kernel = vec![0.0; d * oversampling + oversampling];
316 for i in (d * oversampling)..(d * oversampling + oversampling) {
317 if i < kernel.len() {
318 kernel[i] = 1.0;
319 }
320 }
321 kernel
322 })
323 .collect(),
324 HrfModel::None => vec![{
325 let mut k = vec![0.0; oversampling];
326 k[0] = 1.0;
327 k
328 }],
329 }
330}
331
332pub fn compute_regressor(
337 onsets: &[f64],
338 durations: &[f64],
339 amplitudes: &[f64],
340 hrf_model: &HrfModel,
341 frame_times: &[f64],
342 oversampling: usize,
343 fir_delays: Option<&[f64]>,
344) -> Vec<Vec<f64>> {
345 let n = frame_times.len();
346 if n < 2 {
347 return vec![vec![0.0; n]];
348 }
349
350 let tr = frame_times[frame_times.len() - 1] / (n - 1) as f64;
351 let n_hr = n * oversampling;
352 let dt = tr / oversampling as f64;
353
354 let mut hr_reg = vec![0.0f64; n_hr];
356 for i in 0..onsets.len() {
357 let onset_idx = ((onsets[i] / dt).round() as usize).min(n_hr - 1);
358 let dur_idx = (((onsets[i] + durations[i]) / dt).round() as usize).min(n_hr - 1);
359 hr_reg[onset_idx] += amplitudes[i];
360 if dur_idx < n_hr && dur_idx != onset_idx {
361 hr_reg[dur_idx] -= amplitudes[i];
362 }
363 }
364 for i in 1..n_hr {
366 hr_reg[i] += hr_reg[i - 1];
367 }
368
369 let kernels = hrf_kernel(hrf_model, tr, oversampling, fir_delays);
371
372 kernels
374 .iter()
375 .map(|kernel| {
376 let conv = convolve(&hr_reg, kernel);
377 (0..n)
379 .map(|i| {
380 let idx = i * oversampling;
381 if idx < conv.len() { conv[idx] } else { 0.0 }
382 })
383 .collect()
384 })
385 .collect()
386}
387
388fn convolve(a: &[f64], b: &[f64]) -> Vec<f64> {
390 let n = a.len();
391 let m = b.len();
392 let mut result = vec![0.0; n];
393 for i in 0..n {
394 for j in 0..m {
395 if i >= j {
396 result[i] += a[i - j] * b[j];
397 }
398 }
399 }
400 result
401}
402
403#[must_use]
405pub fn regressor_names(
406 con_name: &str,
407 model: &HrfModel,
408 fir_delays: Option<&[f64]>,
409) -> Vec<String> {
410 match model {
411 HrfModel::Spm | HrfModel::Glover | HrfModel::None => vec![con_name.into()],
412 HrfModel::SpmDerivative | HrfModel::GloverDerivative => {
413 vec![con_name.into(), format!("{}_derivative", con_name)]
414 }
415 HrfModel::SpmDerivativeDispersion | HrfModel::GloverDerivativeDispersion => vec![
416 con_name.into(),
417 format!("{}_derivative", con_name),
418 format!("{}_dispersion", con_name),
419 ],
420 HrfModel::Fir => fir_delays
421 .unwrap_or(&[])
422 .iter()
423 .map(|d| format!("{}_delay_{}", con_name, *d as i64))
424 .collect(),
425 }
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431
432 #[test]
433 fn test_spm_hrf() {
434 let hrf = spm_hrf(2.0, 50, 32.0, 0.0);
435 assert!(!hrf.is_empty());
436 let sum: f64 = hrf.iter().sum();
437 assert!((sum - 1.0).abs() < 0.1, "HRF should sum to ~1, got {}", sum);
438 let peak_idx = hrf
440 .iter()
441 .enumerate()
442 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
443 .unwrap()
444 .0;
445 assert!(
446 peak_idx < hrf.len() / 2,
447 "Peak should be in first half, at idx {}",
448 peak_idx
449 );
450 }
451
452 #[test]
453 fn test_glover_hrf() {
454 let hrf = glover_hrf(2.0, 50, 32.0, 0.0);
455 assert!(!hrf.is_empty());
456 let sum: f64 = hrf.iter().sum();
457 assert!((sum - 1.0).abs() < 0.01);
458 }
459
460 #[test]
461 fn test_compute_regressor() {
462 let onsets = vec![2.0, 6.0];
463 let durations = vec![1.0, 1.0];
464 let amplitudes = vec![1.0, 1.0];
465 let frame_times: Vec<f64> = (0..100).map(|i| i as f64 * 0.5).collect();
466
467 let regs = compute_regressor(
468 &onsets,
469 &durations,
470 &litudes,
471 &HrfModel::Spm,
472 &frame_times,
473 50,
474 None,
475 );
476 assert_eq!(regs.len(), 1); assert_eq!(regs[0].len(), 100);
478 }
479
480 #[test]
481 fn test_regressor_names() {
482 assert_eq!(regressor_names("face", &HrfModel::Spm, None), vec!["face"]);
483 assert_eq!(
484 regressor_names("face", &HrfModel::SpmDerivative, None),
485 vec!["face", "face_derivative"]
486 );
487 }
488}