use crate::algorithms::common::apply_exclusion_zone;
use crate::core::matrix_profile::MatrixProfile;
#[derive(Debug, Clone)]
pub struct Motif {
pub idx_a: usize,
pub idx_b: usize,
pub distance: f64,
}
#[derive(Debug, Clone)]
pub struct Discord {
pub idx: usize,
pub distance: f64,
}
pub fn find_motifs(mp: &MatrixProfile, k: usize) -> Vec<Motif> {
let mut profile = mp.profile.clone();
let ez = mp.exclusion_zone;
let mut motifs = Vec::with_capacity(k);
for _ in 0..k {
let (best_idx, &best_dist) = match profile
.iter()
.enumerate()
.filter(|(_, d)| d.is_finite())
.min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
{
Some(pair) => pair,
None => break,
};
let match_idx = mp.profile_index[best_idx];
motifs.push(Motif {
idx_a: best_idx,
idx_b: match_idx,
distance: best_dist,
});
apply_exclusion_zone(&mut profile, best_idx, ez);
apply_exclusion_zone(&mut profile, match_idx, ez);
}
motifs
}
pub fn find_discords(mp: &MatrixProfile, k: usize) -> Vec<Discord> {
let mut profile = mp.profile.clone();
let ez = mp.exclusion_zone;
let mut discords = Vec::with_capacity(k);
for _ in 0..k {
let (worst_idx, &worst_dist) = match profile
.iter()
.enumerate()
.filter(|(_, d)| d.is_finite())
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
{
Some(pair) => pair,
None => break,
};
discords.push(Discord {
idx: worst_idx,
distance: worst_dist,
});
apply_exclusion_zone(&mut profile, worst_idx, ez);
}
discords
}
#[cfg(test)]
mod tests {
use super::*;
use crate::algorithms::stomp::stomp;
use crate::core::matrix_profile::MatrixProfileConfig;
use crate::metrics::euclidean::ZNormalizedEuclidean;
#[test]
fn test_find_motifs_basic() {
let m = 8;
let n = 56;
let mut ts = vec![0.0; n];
let pattern = [0.0, 1.0, 0.0, -1.0, 0.0, 1.0, 0.0, -1.0];
ts[0..8].copy_from_slice(&pattern);
for (i, val) in ts.iter_mut().enumerate().take(40).skip(8) {
*val = (i as f64).powi(2) * 0.01 + (i as f64 * 1.7).sin() * 3.0;
}
ts[40..48].copy_from_slice(&pattern);
for (i, val) in ts.iter_mut().enumerate().take(n).skip(48) {
*val = -(i as f64).powi(2) * 0.01 + (i as f64 * 2.3).cos() * 5.0;
}
let config = MatrixProfileConfig::new(m);
let mp = stomp::<ZNormalizedEuclidean>(&ts, &config);
let motifs = find_motifs(&mp, 3);
assert!(!motifs.is_empty());
let top = &motifs[0];
assert!(
top.distance < 1e-4,
"Top motif distance should be very small, got {}",
top.distance
);
let pair = (top.idx_a.min(top.idx_b), top.idx_a.max(top.idx_b));
assert_eq!(pair, (0, 40), "Top motif should be at (0, 40)");
}
#[test]
fn test_find_discords_basic() {
let mut ts: Vec<f64> = (0..100).map(|i| (i as f64 * 0.2).sin()).collect();
ts[25] = 10.0;
ts[26] = -10.0;
let config = MatrixProfileConfig::new(8);
let mp = stomp::<ZNormalizedEuclidean>(&ts, &config);
let discords = find_discords(&mp, 3);
assert!(!discords.is_empty());
let top = &discords[0];
let near_anomaly = (20..=30).contains(&top.idx);
assert!(
near_anomaly,
"Top discord at index {} should be near anomaly at 25",
top.idx
);
}
#[test]
fn test_motifs_decreasing_distance() {
let ts: Vec<f64> = (0..200).map(|i| (i as f64 * 0.15).sin()).collect();
let config = MatrixProfileConfig::new(10);
let mp = stomp::<ZNormalizedEuclidean>(&ts, &config);
let motifs = find_motifs(&mp, 5);
for w in motifs.windows(2) {
assert!(
w[0].distance <= w[1].distance,
"Motifs should be sorted by distance: {} > {}",
w[0].distance,
w[1].distance
);
}
}
#[test]
fn test_discords_decreasing_distance() {
let ts: Vec<f64> = (0..200).map(|i| (i as f64 * 0.15).sin()).collect();
let config = MatrixProfileConfig::new(10);
let mp = stomp::<ZNormalizedEuclidean>(&ts, &config);
let discords = find_discords(&mp, 5);
for w in discords.windows(2) {
assert!(
w[0].distance >= w[1].distance,
"Discords should be sorted by distance: {} < {}",
w[0].distance,
w[1].distance
);
}
}
#[test]
fn test_motifs_empty_profile() {
let mp = MatrixProfile::new(10, 4, 1);
let motifs = find_motifs(&mp, 5);
assert!(motifs.is_empty());
}
#[test]
fn test_discords_empty_profile() {
let mp = MatrixProfile::new(10, 4, 1);
let discords = find_discords(&mp, 5);
assert!(discords.is_empty());
}
}