use crate::core::distance_metric::DistanceMetric;
use crate::core::matrix_profile::RollingStats;
#[derive(Debug, Clone)]
pub struct ZNormalizedEuclidean;
impl DistanceMetric for ZNormalizedEuclidean {
type Context = RollingStats;
fn precompute(ts: &[f64], m: usize) -> Self::Context {
RollingStats::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 {
let msi = ctx.m_sigma_inv[i];
let msj = ctx.m_sigma_inv[j];
let m_f = m as f64;
if msi == 0.0 && msj == 0.0 {
return 0.0;
}
if msi == 0.0 || msj == 0.0 {
return (2.0 * m_f).sqrt();
}
let r = (qt - m_f * ctx.mean[i] * ctx.mean[j]) * msi * msj;
let r_clamped = r.clamp(-1.0, 1.0);
(2.0 * m_f * (1.0 - r_clamped)).max(0.0).sqrt()
}
fn supports_correlation_domain() -> bool {
true
}
fn correlation_data(ctx: &Self::Context) -> (&[f64], &[f64], bool) {
(&ctx.mean, &ctx.m_sigma_inv, ctx.has_constant)
}
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 {
let msi_a = ctx_a.m_sigma_inv[i];
let msi_b = ctx_b.m_sigma_inv[j];
let m_f = m as f64;
if msi_a == 0.0 && msi_b == 0.0 {
return 0.0;
}
if msi_a == 0.0 || msi_b == 0.0 {
return (2.0 * m_f).sqrt();
}
let r = (qt - m_f * ctx_a.mean[i] * ctx_b.mean[j]) * msi_a * msi_b;
let r_clamped = r.clamp(-1.0, 1.0);
(2.0 * m_f * (1.0 - r_clamped)).max(0.0).sqrt()
}
fn correlation_data_ab<'a>(
ctx_a: &'a Self::Context,
ctx_b: &'a Self::Context,
) -> (&'a [f64], &'a [f64], &'a [f64], &'a [f64], bool) {
(
&ctx_a.mean,
&ctx_a.m_sigma_inv,
&ctx_b.mean,
&ctx_b.m_sigma_inv,
ctx_a.has_constant || ctx_b.has_constant,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_distance_identical_subsequences() {
let ts = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
let m = 4;
let ctx = ZNormalizedEuclidean::precompute(&ts, m);
let d = ZNormalizedEuclidean::distance(&ts, 0, 0, m, &ctx);
assert!(d.abs() < 1e-6, "Self-distance should be 0, got {d}");
}
#[test]
fn test_distance_shifted_linear() {
let ts = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let m = 4;
let ctx = ZNormalizedEuclidean::precompute(&ts, m);
let d = ZNormalizedEuclidean::distance(&ts, 0, 2, m, &ctx);
assert!(
d < 1e-6,
"Shifted linear sequences should have d≈0, got {d}"
);
}
#[test]
fn test_distance_constant_both() {
let ts = vec![5.0; 10];
let m = 4;
let ctx = ZNormalizedEuclidean::precompute(&ts, m);
let d = ZNormalizedEuclidean::distance(&ts, 0, 3, m, &ctx);
assert!(d.abs() < 1e-10, "Two constant subsequences → d=0, got {d}");
}
#[test]
fn test_distance_one_constant() {
let ts = vec![5.0, 5.0, 5.0, 5.0, 1.0, 2.0, 3.0, 4.0];
let m = 4;
let ctx = ZNormalizedEuclidean::precompute(&ts, m);
let d = ZNormalizedEuclidean::distance(&ts, 0, 4, m, &ctx);
let expected = (2.0 * 4.0_f64).sqrt();
assert!(
(d - expected).abs() < 1e-10,
"One constant → d=sqrt(2m)={expected}, got {d}"
);
}
#[test]
fn test_qt_to_distance_hand_computed() {
let ts = vec![1.0, 2.0, 3.0, 4.0];
let m = 2;
let ctx = ZNormalizedEuclidean::precompute(&ts, m);
let qt = 1.0 * 2.0 + 2.0 * 3.0; let d = ZNormalizedEuclidean::qt_to_distance(qt, 0, 1, m, &ctx);
assert!(d < 1e-7, "Hand-computed: d should be ~0, got {d}");
}
#[test]
fn test_qt_to_distance_anticorrelated() {
let ts = vec![1.0, 2.0, 4.0, 3.0];
let m = 2;
let ctx = ZNormalizedEuclidean::precompute(&ts, m);
let qt = 1.0 * 4.0 + 2.0 * 3.0; let d = ZNormalizedEuclidean::qt_to_distance(qt, 0, 2, m, &ctx);
let expected = (8.0_f64).sqrt(); assert!(
(d - expected).abs() < 1e-10,
"Anticorrelated: expected {expected}, got {d}"
);
}
}