pub fn lb_kim(a: &[f64], b: &[f64]) -> f64 {
assert!(
!a.is_empty() && !b.is_empty(),
"Time series must be non-empty"
);
let a_first = a[0];
let a_last = a[a.len() - 1];
let a_min = a.iter().copied().fold(f64::INFINITY, f64::min);
let a_max = a.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let b_first = b[0];
let b_last = b[b.len() - 1];
let b_min = b.iter().copied().fold(f64::INFINITY, f64::min);
let b_max = b.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let d_first = (a_first - b_first).powi(2);
let d_last = (a_last - b_last).powi(2);
let d_min = (a_min - b_min).powi(2);
let d_max = (a_max - b_max).powi(2);
d_first.max(d_last).max(d_min).max(d_max).sqrt()
}
pub fn lb_keogh(a: &[f64], b: &[f64], window_size: usize) -> f64 {
assert_eq!(a.len(), b.len(), "Time series must have same length");
let n = a.len();
let mut sum = 0.0;
for i in 0..n {
let lo = i.saturating_sub(window_size);
let hi = (i + window_size + 1).min(n);
let b_slice = &b[lo..hi];
let upper = b_slice.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let lower = b_slice.iter().copied().fold(f64::INFINITY, f64::min);
if a[i] > upper {
sum += (a[i] - upper).powi(2);
} else if a[i] < lower {
sum += (a[i] - lower).powi(2);
}
}
sum.sqrt()
}
pub fn lb_yi(a: &[f64], b: &[f64]) -> f64 {
assert_eq!(a.len(), b.len(), "Time series must have same length");
let a_max = a.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let a_min = a.iter().copied().fold(f64::INFINITY, f64::min);
let sum: f64 = b
.iter()
.map(|&v| {
if v > a_max {
(v - a_max).powi(2)
} else if v < a_min {
(v - a_min).powi(2)
} else {
0.0
}
})
.sum();
sum.sqrt()
}
pub fn lb_improved(a: &[f64], b: &[f64], window_size: usize) -> f64 {
assert_eq!(a.len(), b.len(), "Time series must have same length");
let n = a.len();
let mut sum1 = 0.0;
let mut projected = vec![0.0; n];
for i in 0..n {
let lo = i.saturating_sub(window_size);
let hi = (i + window_size + 1).min(n);
let b_slice = &b[lo..hi];
let upper = b_slice.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let lower = b_slice.iter().copied().fold(f64::INFINITY, f64::min);
if a[i] > upper {
sum1 += (a[i] - upper).powi(2);
projected[i] = upper;
} else if a[i] < lower {
sum1 += (a[i] - lower).powi(2);
projected[i] = lower;
} else {
projected[i] = a[i];
}
}
let mut sum2 = 0.0;
for i in 0..n {
let lo = i.saturating_sub(window_size);
let hi = (i + window_size + 1).min(n);
let p_slice = &projected[lo..hi];
let upper = p_slice.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let lower = p_slice.iter().copied().fold(f64::INFINITY, f64::min);
if b[i] > upper {
sum2 += (b[i] - upper).powi(2);
} else if b[i] < lower {
sum2 += (b[i] - lower).powi(2);
}
}
(sum1 + sum2).sqrt()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lb_kim_identical() {
let a = vec![1.0, 2.0, 3.0, 4.0];
assert!(lb_kim(&a, &a).abs() < 1e-10);
}
#[test]
fn test_lb_kim_different() {
let a = vec![0.0, 1.0, 2.0];
let b = vec![3.0, 4.0, 5.0];
let d = lb_kim(&a, &b);
assert!(d > 0.0);
}
#[test]
fn test_lb_keogh_identical() {
let a = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let d = lb_keogh(&a, &a, 1);
assert!(d.abs() < 1e-10);
}
#[test]
fn test_lb_keogh_lower_bound() {
use crate::metrics::dtw::dtw_classic;
let a = vec![0.0, 1.0, 0.0, -1.0, 0.0];
let b = vec![1.0, 0.0, -1.0, 0.0, 1.0];
let lb = lb_keogh(&a, &b, 1);
let dtw = dtw_classic(&a, &b);
assert!(lb <= dtw + 1e-8, "LB_Keogh ({lb}) should be <= DTW ({dtw})");
}
#[test]
fn test_lb_yi_identical() {
let a = vec![1.0, 2.0, 3.0];
assert!(lb_yi(&a, &a).abs() < 1e-10);
}
#[test]
fn test_lb_yi_lower_bound() {
use crate::metrics::dtw::dtw_classic;
let a = vec![0.0, 1.0, 0.0, -1.0, 0.0];
let b = vec![1.0, 0.0, -1.0, 0.0, 1.0];
let lb = lb_yi(&a, &b);
let dtw = dtw_classic(&a, &b);
assert!(lb <= dtw + 1e-8, "LB_Yi ({lb}) should be <= DTW ({dtw})");
}
#[test]
fn test_lb_improved_lower_bound() {
use crate::metrics::dtw::dtw_classic;
let a = vec![0.0, 1.0, 0.0, -1.0, 0.0];
let b = vec![1.0, 0.0, -1.0, 0.0, 1.0];
let lb = lb_improved(&a, &b, 1);
let dtw = dtw_classic(&a, &b);
assert!(
lb <= dtw + 1e-8,
"LB_Improved ({lb}) should be <= DTW ({dtw})"
);
}
#[test]
fn test_lb_improved_tighter_than_keogh() {
let a = vec![0.0, 1.0, 2.0, 1.0, 0.0, -1.0, -2.0, -1.0];
let b = vec![1.0, 2.0, 1.0, 0.0, -1.0, -2.0, -1.0, 0.0];
let lb_k = lb_keogh(&a, &b, 1);
let lb_i = lb_improved(&a, &b, 1);
assert!(
lb_i >= lb_k - 1e-10,
"LB_Improved ({lb_i}) should be >= LB_Keogh ({lb_k})"
);
}
}