use num_traits::Float;
const PARABOLA_FLAT: f64 = 1e-12;
pub fn lagged_dot<T: Float>(x: &[T], y: &[T], lag: isize) -> T {
let start = (-lag).max(0) as usize;
let end = (y.len() as isize - lag).clamp(0, x.len() as isize) as usize;
(start..end.max(start)).fold(T::zero(), |acc, t| acc + x[t] * y[(t as isize + lag) as usize])
}
pub fn cross_correlation<T: Float>(x: &[T], y: &[T], max_lag: usize) -> Vec<T> {
let m = max_lag as isize;
(-m..=m).map(|lag| lagged_dot(x, y, lag)).collect()
}
pub fn parabolic_vertex_offset<T: Float>(y_prev: T, y_mid: T, y_next: T) -> T {
let two = T::one() + T::one();
let denom = two * (y_prev - two * y_mid + y_next);
let half = T::from(0.5).expect("0.5");
if denom.abs() <= T::from(PARABOLA_FLAT).expect("tolerance") {
return T::zero();
}
((y_prev - y_next) / denom).max(-half).min(half)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LagPeak<T> {
pub lag: isize,
pub value: T,
pub offset: T,
}
impl<T: Float> LagPeak<T> {
pub fn fractional_lag(&self) -> T {
T::from(self.lag).expect("lag") + self.offset
}
}
pub fn peak_lag<T: Float>(corr: &[T], max_lag: usize) -> Option<LagPeak<T>> {
let (best, &value) = corr
.iter()
.enumerate()
.fold(None, |best: Option<(usize, &T)>, (i, v)| match best {
Some((_, b)) if *v <= *b => best,
_ => Some((i, v)),
})?;
let offset = if best > 0 && best + 1 < corr.len() {
parabolic_vertex_offset(corr[best - 1], corr[best], corr[best + 1])
} else {
T::zero()
};
Some(LagPeak { lag: best as isize - max_lag as isize, value, offset })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn matches_scipy_correlate() {
let x = [1.0, 2.0, 0.0, 0.0];
let y = [0.0, 1.0, 2.0, 0.0];
assert_eq!(cross_correlation(&x, &y, 2), vec![0.0, 0.0, 2.0, 5.0, 2.0]);
let peak = peak_lag(&cross_correlation(&x, &y, 2), 2).unwrap();
assert_eq!((peak.lag, peak.value), (1, 5.0), "y is x delayed by one sample");
}
#[test]
fn parabola_vertex() {
let y = |t: f64| -(t - 0.25) * (t - 0.25);
assert!((parabolic_vertex_offset(y(-1.0), y(0.0), y(1.0)) - 0.25).abs() < 1e-12);
assert_eq!(parabolic_vertex_offset(1.0, 1.0, 1.0), 0.0);
}
}