use crate::core::distance_metric::DistanceMetric;
#[derive(Debug, Clone)]
pub struct AampContext {
pub sum_sq: Vec<f64>,
}
impl AampContext {
pub fn compute(ts: &[f64], m: usize) -> Self {
assert!(m > 0, "Subsequence length must be > 0");
assert!(ts.len() >= m, "Time series must be at least as long as m");
let n = ts.len();
let n_subs = n - m + 1;
let mut cumsum_sq = vec![0.0; n + 1];
for i in 0..n {
cumsum_sq[i + 1] = cumsum_sq[i] + ts[i] * ts[i];
}
let sum_sq: Vec<f64> = (0..n_subs)
.map(|i| cumsum_sq[i + m] - cumsum_sq[i])
.collect();
Self { sum_sq }
}
pub fn extend(&mut self, ts: &[f64], m: usize) {
let n = ts.len();
assert!(n >= m);
let start = n - m;
let sum_sq: f64 = ts[start..n].iter().map(|x| x * x).sum();
self.sum_sq.push(sum_sq);
}
}
#[derive(Debug, Clone)]
pub struct AbsoluteEuclidean;
impl DistanceMetric for AbsoluteEuclidean {
type Context = AampContext;
fn precompute(ts: &[f64], m: usize) -> Self::Context {
AampContext::compute(ts, m)
}
fn distance(ts: &[f64], i: usize, j: usize, m: usize, ctx: &Self::Context) -> f64 {
let qt: f64 = ts[i..i + m]
.iter()
.zip(&ts[j..j + m])
.map(|(a, b)| a * b)
.sum();
Self::qt_to_distance(qt, i, j, m, ctx)
}
fn supports_qt_optimization() -> bool {
true
}
fn qt_to_distance(qt: f64, i: usize, j: usize, _m: usize, ctx: &Self::Context) -> f64 {
(ctx.sum_sq[i] + ctx.sum_sq[j] - 2.0 * qt).max(0.0).sqrt()
}
fn supports_correlation_domain() -> bool {
false
}
fn update_context(ctx: &mut Self::Context, ts: &[f64], m: usize) {
ctx.extend(ts, m);
}
fn supports_ab_join() -> bool {
true
}
fn qt_to_distance_ab(
qt: f64,
i: usize,
j: usize,
_m: usize,
ctx_a: &Self::Context,
ctx_b: &Self::Context,
) -> f64 {
(ctx_a.sum_sq[i] + ctx_b.sum_sq[j] - 2.0 * qt)
.max(0.0)
.sqrt()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_aamp_context_compute() {
let ts = vec![1.0, 2.0, 3.0, 4.0];
let ctx = AampContext::compute(&ts, 2);
assert_eq!(ctx.sum_sq.len(), 3);
assert!((ctx.sum_sq[0] - 5.0).abs() < 1e-10);
assert!((ctx.sum_sq[1] - 13.0).abs() < 1e-10);
assert!((ctx.sum_sq[2] - 25.0).abs() < 1e-10);
}
#[test]
fn test_aamp_distance_identical() {
let ts = vec![1.0, 2.0, 3.0, 4.0, 5.0, 1.0, 2.0, 3.0];
let m = 3;
let ctx = AbsoluteEuclidean::precompute(&ts, m);
let d = AbsoluteEuclidean::distance(&ts, 0, 5, m, &ctx);
assert!(d.abs() < 1e-10, "Identical subsequences → d=0, got {d}");
}
#[test]
fn test_aamp_distance_hand_computed() {
let ts = vec![1.0, 2.0, 3.0, 4.0];
let m = 2;
let ctx = AbsoluteEuclidean::precompute(&ts, m);
let d = AbsoluteEuclidean::distance(&ts, 0, 1, m, &ctx);
let expected = 2.0_f64.sqrt();
assert!((d - expected).abs() < 1e-10, "Expected {expected}, got {d}");
}
#[test]
fn test_aamp_qt_to_distance() {
let ts = vec![1.0, 2.0, 3.0, 4.0];
let m = 2;
let ctx = AbsoluteEuclidean::precompute(&ts, m);
let qt = 1.0 * 3.0 + 2.0 * 4.0; let d = AbsoluteEuclidean::qt_to_distance(qt, 0, 2, m, &ctx);
let expected = 8.0_f64.sqrt();
assert!((d - expected).abs() < 1e-10, "Expected {expected}, got {d}");
}
#[test]
fn test_aamp_context_extend() {
let mut ts = vec![1.0, 2.0, 3.0, 4.0];
let m = 2;
let mut ctx = AampContext::compute(&ts, m);
assert_eq!(ctx.sum_sq.len(), 3);
ts.push(5.0);
ctx.extend(&ts, m);
assert_eq!(ctx.sum_sq.len(), 4);
assert!((ctx.sum_sq[3] - 41.0).abs() < 1e-10);
}
#[test]
fn test_aamp_stomp_integration() {
use crate::algorithms::stomp::stomp;
use crate::core::matrix_profile::MatrixProfileConfig;
let ts = vec![1.0, 2.0, 3.0, 2.0, 1.0, 2.0, 3.0, 2.0, 1.0];
let config = MatrixProfileConfig::new(4);
let mp = stomp::<AbsoluteEuclidean>(&ts, &config);
assert_eq!(mp.profile.len(), ts.len() - 4 + 1);
assert!(
mp.profile[0] < 1e-6,
"Identical subsequence distance should be ~0, got {}",
mp.profile[0]
);
}
}