use crate::error::FdarError;
const STD_EPS: f64 = 1e-12;
pub fn z_normalize_into(src: &[f64], dst: &mut [f64]) {
debug_assert_eq!(
src.len(),
dst.len(),
"z_normalize_into: src and dst length mismatch"
);
let n = src.len().min(dst.len());
if n == 0 {
return;
}
let len_f = n as f64;
let mut sum = 0.0;
for &v in &src[..n] {
sum += v;
}
let mean = sum / len_f;
let mut sq = 0.0;
for &v in &src[..n] {
let d = v - mean;
sq += d * d;
}
let std = (sq / len_f).sqrt();
if std <= STD_EPS {
for d in &mut dst[..n] {
*d = 0.0;
}
return;
}
let inv = 1.0 / std;
for i in 0..n {
dst[i] = (src[i] - mean) * inv;
}
}
#[must_use]
pub fn z_normalize_window(slice: &[f64]) -> Vec<f64> {
let mut out = vec![0.0; slice.len()];
z_normalize_into(slice, &mut out);
out
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct Shapelet {
pub values: Vec<f64>,
pub series_idx: usize,
pub start: usize,
pub length: usize,
pub quality: f64,
}
impl Shapelet {
pub fn from_source(
series: &[f64],
series_idx: usize,
start: usize,
length: usize,
) -> Result<Self, FdarError> {
if length == 0 {
return Err(FdarError::InvalidDimension {
parameter: "length",
expected: "length >= 1".to_string(),
actual: length.to_string(),
});
}
let end = start
.checked_add(length)
.ok_or(FdarError::InvalidDimension {
parameter: "start+length",
expected: format!("<= series length {}", series.len()),
actual: "overflow".to_string(),
})?;
if end > series.len() {
return Err(FdarError::InvalidDimension {
parameter: "start+length",
expected: format!("<= series length {}", series.len()),
actual: end.to_string(),
});
}
Ok(Self {
values: z_normalize_window(&series[start..end]),
series_idx,
start,
length,
quality: 0.0,
})
}
#[must_use]
pub fn len(&self) -> usize {
self.length
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.length == 0
}
}
#[must_use = "the shapelet distance and best-match offset should not be discarded"]
pub fn shapelet_distance(
shapelet_z: &[f64],
series: &[f64],
best_so_far: f64,
) -> Result<(f64, usize), FdarError> {
let l = shapelet_z.len();
if l == 0 {
return Err(FdarError::InvalidDimension {
parameter: "shapelet_z",
expected: "length >= 1".to_string(),
actual: "0".to_string(),
});
}
if l > series.len() {
return Err(FdarError::InvalidDimension {
parameter: "shapelet_z.len",
expected: format!("<= series length {}", series.len()),
actual: l.to_string(),
});
}
let mut best_sq = if best_so_far.is_finite() {
best_so_far * best_so_far
} else {
f64::INFINITY
};
let mut best_offset = 0usize;
let mut found = false;
let mut window_z = vec![0.0; l];
let n_windows = series.len() - l + 1;
for t in 0..n_windows {
let window = &series[t..t + l];
z_normalize_into(window, &mut window_z);
let mut acc = 0.0;
let mut abandoned = false;
for k in 0..l {
let diff = window_z[k] - shapelet_z[k];
acc += diff * diff;
if acc > best_sq {
abandoned = true;
break;
}
}
if abandoned {
continue;
}
if !found || acc < best_sq {
best_sq = acc;
best_offset = t;
found = true;
}
}
let min_dist = if found { best_sq.sqrt() } else { best_so_far };
Ok((min_dist, best_offset))
}
#[cfg(test)]
mod tests {
use super::*;
fn population_std(z: &[f64]) -> f64 {
let n = z.len() as f64;
let mean = z.iter().sum::<f64>() / n;
(z.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n).sqrt()
}
#[test]
fn test_znorm_constant_window() {
let z = z_normalize_window(&[5.0, 5.0, 5.0, 5.0]);
assert_eq!(z, vec![0.0; 4]);
assert!(z.iter().all(|v| v.is_finite()));
let mut x = vec![5.0; 20];
x[3] += 1e-15;
let z = z_normalize_window(&x);
assert!(
z.iter().all(|v| v.is_finite()),
"near-constant produced non-finite"
);
}
#[test]
fn test_znorm_mean_std() {
let z = z_normalize_window(&[1.0, 2.0, 3.0, 4.0, 5.0]);
let mean = z.iter().sum::<f64>() / z.len() as f64;
assert!(mean.abs() < 1e-12, "mean not ~0: {mean}");
assert!(
(population_std(&z) - 1.0).abs() < 1e-12,
"population std not ~1"
);
assert!(z.iter().all(|v| v.is_finite()));
}
#[test]
fn test_sdist_scale_offset_invariant() {
let shapelet = z_normalize_window(&[1.0, 3.0, 2.0, 4.0]);
let series = vec![0.5, 1.0, 3.0, 2.0, 4.0, 0.7, 0.2];
let (d0, o0) = shapelet_distance(&shapelet, &series, f64::INFINITY).unwrap();
let shifted: Vec<f64> = series.iter().map(|v| v + 100.0).collect();
let (d1, o1) = shapelet_distance(&shapelet, &shifted, f64::INFINITY).unwrap();
let scaled: Vec<f64> = series.iter().map(|v| v * 50.0).collect();
let (d2, o2) = shapelet_distance(&shapelet, &scaled, f64::INFINITY).unwrap();
assert!(
(d0 - d1).abs() < 1e-10,
"offset invariance failed: {d0} vs {d1}"
);
assert!(
(d0 - d2).abs() < 1e-10,
"scale invariance failed: {d0} vs {d2}"
);
assert_eq!(o0, o1);
assert_eq!(o0, o2);
}
#[test]
fn test_sdist_min_semantics() {
let motif = [2.0, -1.0, 0.5, 3.0, 1.0];
let shapelet = z_normalize_window(&motif);
let mut series = vec![9.0, 8.0, 7.0]; series.extend_from_slice(&motif);
series.extend_from_slice(&[6.0, 5.0]);
let (dist, offset) = shapelet_distance(&shapelet, &series, f64::INFINITY).unwrap();
assert!(dist < 1e-9, "exact-motif sdist not ~0: {dist}");
assert_eq!(offset, 3, "wrong best-match offset");
}
#[test]
fn test_sdist_early_abandon_identical() {
let shapelet = z_normalize_window(&[0.0, 1.0, 0.5, -1.0, 2.0]);
let series = vec![
3.0, 1.0, -2.0, 0.4, 1.5, 0.0, 1.0, 0.5, -1.0, 2.0, 4.0, 2.2, -0.3,
];
let (d_inf, o_inf) = shapelet_distance(&shapelet, &series, f64::INFINITY).unwrap();
let bound = d_inf + 0.5;
let (d_tight, o_tight) = shapelet_distance(&shapelet, &series, bound).unwrap();
assert!((d_inf - d_tight).abs() < 1e-12, "abandon changed the min");
assert_eq!(o_inf, o_tight, "abandon changed the offset");
let (d_eq, o_eq) = shapelet_distance(&shapelet, &series, d_inf).unwrap();
assert!((d_inf - d_eq).abs() < 1e-12);
assert_eq!(o_inf, o_eq);
}
#[test]
fn test_sdist_dimension_error() {
let shapelet = z_normalize_window(&[1.0, 2.0, 3.0, 4.0, 5.0]);
let series = [1.0, 2.0]; let err = shapelet_distance(&shapelet, &series, f64::INFINITY).unwrap_err();
assert!(matches!(err, FdarError::InvalidDimension { .. }));
let err = shapelet_distance(&[], &series, f64::INFINITY).unwrap_err();
assert!(matches!(err, FdarError::InvalidDimension { .. }));
}
#[test]
fn test_shapelet_from_source() {
let series = [0.0, 1.0, 2.0, 3.0, 4.0];
let s = Shapelet::from_source(&series, 7, 1, 3).unwrap();
assert_eq!(s.series_idx, 7);
assert_eq!(s.start, 1);
assert_eq!(s.length, 3);
assert_eq!(s.len(), 3);
assert!(!s.is_empty());
assert_eq!(s.quality, 0.0);
assert_eq!(s.values, z_normalize_window(&series[1..4]));
assert!(Shapelet::from_source(&series, 0, 3, 5).is_err());
assert!(Shapelet::from_source(&series, 0, 0, 0).is_err());
}
}