use crate::algorithms::common::sliding_dot_product;
#[cfg(feature = "parallel")]
use crate::algorithms::stomp::compute_diagonal_ranges;
use crate::core::matrix_profile::RollingStats;
#[cfg(feature = "parallel")]
const MIN_PARALLEL_SUBS: usize = 256;
#[derive(Debug, Clone)]
pub struct MultiDimensionalProfile {
pub profile: Vec<Vec<f64>>,
pub profile_index: Vec<Vec<usize>>,
pub d: usize,
pub m: usize,
}
struct MstumpCtx<'a> {
ts: &'a [&'a [f64]],
stats: &'a [RollingStats],
qt_first: &'a [Vec<f64>],
d: usize,
m_f: f64,
sqrt_2m: f64,
n_subs: usize,
}
struct MstumpAcc {
profile: Vec<Vec<f64>>,
profile_index: Vec<Vec<usize>>,
dists: Vec<f64>,
qt: Vec<f64>,
}
impl MstumpAcc {
fn new(d: usize, n_subs: usize) -> Self {
Self {
profile: vec![vec![f64::INFINITY; n_subs]; d],
profile_index: vec![vec![0usize; n_subs]; d],
dists: vec![0.0; d],
qt: vec![0.0; d],
}
}
fn merge(&mut self, other: &MstumpAcc) {
let d = self.profile.len();
let n_subs = self.profile[0].len();
for k in 0..d {
for j in 0..n_subs {
if other.profile[k][j] < self.profile[k][j] {
self.profile[k][j] = other.profile[k][j];
self.profile_index[k][j] = other.profile_index[k][j];
}
}
}
}
}
pub fn mstump(ts: &[&[f64]], m: usize) -> MultiDimensionalProfile {
let d = ts.len();
assert!(d >= 1, "Need at least one dimension");
let n = ts[0].len();
for (i, t) in ts.iter().enumerate() {
assert_eq!(
t.len(),
n,
"Dimension {i} has length {}, expected {n}",
t.len()
);
}
assert!(
n >= 2 * m,
"Time series length ({n}) must be >= 2*m ({})",
2 * m
);
assert!(m >= 2, "Subsequence length must be >= 2");
let n_subs = n - m + 1;
let ez = (m as f64 / 4.0).ceil() as usize;
let m_f = m as f64;
let stats: Vec<RollingStats> = ts.iter().map(|t| RollingStats::compute(t, m)).collect();
let qt_first: Vec<Vec<f64>> = (0..d)
.map(|dim| sliding_dot_product(&ts[dim][0..m], ts[dim]))
.collect();
let cx = MstumpCtx {
ts,
stats: &stats,
qt_first: &qt_first,
d,
m_f,
sqrt_2m: (2.0 * m_f).sqrt(),
n_subs,
};
let mut acc = MstumpAcc::new(d, n_subs);
#[cfg(feature = "parallel")]
if n_subs >= MIN_PARALLEL_SUBS {
mstump_diagonal_parallel(&cx, ez, &mut acc);
} else {
mstump_diagonal(&cx, ez, &mut acc);
}
#[cfg(not(feature = "parallel"))]
mstump_diagonal(&cx, ez, &mut acc);
MultiDimensionalProfile {
profile: acc.profile,
profile_index: acc.profile_index,
d,
m,
}
}
#[inline(always)]
fn update_position(cx: &MstumpCtx<'_>, acc: &mut MstumpAcc, i: usize, j: usize) {
let d = cx.d;
for dim in 0..d {
let si = cx.stats[dim].m_sigma_inv[i];
let sj = cx.stats[dim].m_sigma_inv[j];
acc.dists[dim] = if si == 0.0 && sj == 0.0 {
0.0
} else if si == 0.0 || sj == 0.0 {
cx.sqrt_2m
} else {
let neg_r = (cx.m_f * cx.stats[dim].mean[i])
.mul_add(cx.stats[dim].mean[j], -acc.qt[dim])
* si
* sj;
(2.0 * cx.m_f * (1.0 + neg_r)).max(0.0).sqrt()
};
}
acc.dists[..d].sort_unstable_by(|a, b| a.partial_cmp(b).unwrap());
let mut cum_sum = 0.0;
for k in 0..d {
cum_sum += acc.dists[k];
let cum_avg = cum_sum / (k + 1) as f64;
if cum_avg < acc.profile[k][i] {
acc.profile[k][i] = cum_avg;
acc.profile_index[k][i] = j;
}
if cum_avg < acc.profile[k][j] {
acc.profile[k][j] = cum_avg;
acc.profile_index[k][j] = i;
}
}
}
fn mstump_diagonal(cx: &MstumpCtx<'_>, ez: usize, acc: &mut MstumpAcc) {
let d = cx.d;
let m = cx.m_f as usize;
for k in (ez + 1)..cx.n_subs {
let diag_len = cx.n_subs - k;
for (dim, qf) in cx.qt_first.iter().enumerate() {
acc.qt[dim] = qf[k];
}
update_position(cx, acc, 0, k);
for p in 1..diag_len {
let j = p + k;
for dim in 0..d {
acc.qt[dim] = (-cx.ts[dim][p - 1]).mul_add(cx.ts[dim][j - 1], acc.qt[dim]);
acc.qt[dim] = cx.ts[dim][p + m - 1].mul_add(cx.ts[dim][j + m - 1], acc.qt[dim]);
}
update_position(cx, acc, p, j);
}
}
}
#[cfg(feature = "parallel")]
fn mstump_diagonal_parallel(cx: &MstumpCtx<'_>, ez: usize, acc: &mut MstumpAcc) {
use rayon::prelude::*;
let d = cx.d;
let m = cx.m_f as usize;
let n_threads = rayon::current_num_threads();
let ranges = compute_diagonal_ranges(ez + 1, cx.n_subs, n_threads);
let results: Vec<MstumpAcc> = ranges
.into_par_iter()
.map(|(start_k, end_k)| {
let mut local = MstumpAcc::new(d, cx.n_subs);
for k in start_k..end_k {
for (dim, qf) in cx.qt_first.iter().enumerate() {
local.qt[dim] = qf[k];
}
update_position(cx, &mut local, 0, k);
for p in 1..(cx.n_subs - k) {
let j = p + k;
for dim in 0..d {
local.qt[dim] =
(-cx.ts[dim][p - 1]).mul_add(cx.ts[dim][j - 1], local.qt[dim]);
local.qt[dim] =
cx.ts[dim][p + m - 1].mul_add(cx.ts[dim][j + m - 1], local.qt[dim]);
}
update_position(cx, &mut local, p, j);
}
}
local
})
.collect();
for result in &results {
acc.merge(result);
}
}
#[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_mstump_single_dimension() {
let ts: Vec<f64> = (0..100).map(|i| (i as f64 * 0.2).sin()).collect();
let m = 10;
let config = MatrixProfileConfig::new(m);
let mp_1d = stomp::<ZNormalizedEuclidean>(&ts, &config);
let ts_refs: [&[f64]; 1] = [&ts];
let mdp = mstump(&ts_refs, m);
assert_eq!(mdp.d, 1);
assert_eq!(mdp.profile.len(), 1);
assert_eq!(mdp.profile[0].len(), mp_1d.profile.len());
for (i, (a, b)) in mdp.profile[0].iter().zip(&mp_1d.profile).enumerate() {
if a.is_infinite() && b.is_infinite() {
continue;
}
assert!(
(a - b).abs() < 1e-6,
"Mismatch at {i}: mstump={a}, stomp={b}"
);
}
}
#[test]
fn test_mstump_profile_nondecreasing() {
let n = 100;
let m = 10;
let ts0: Vec<f64> = (0..n).map(|i| (i as f64 * 0.2).sin()).collect();
let ts1: Vec<f64> = (0..n).map(|i| (i as f64 * 0.3).cos()).collect();
let ts2: Vec<f64> = (0..n).map(|i| (i as f64 * 0.15).sin() + 0.5).collect();
let ts_refs: [&[f64]; 3] = [&ts0, &ts1, &ts2];
let mdp = mstump(&ts_refs, m);
assert_eq!(mdp.d, 3);
let n_subs = n - m + 1;
for j in 0..n_subs {
for k in 1..3 {
assert!(
mdp.profile[k][j] >= mdp.profile[k - 1][j] - 1e-10,
"Profile not non-decreasing at position {j}: P[{}]={}, P[{}]={}",
k - 1,
mdp.profile[k - 1][j],
k,
mdp.profile[k][j]
);
}
}
}
#[test]
fn test_mstump_distances_nonnegative() {
let n = 80;
let m = 8;
let ts0: Vec<f64> = (0..n).map(|i| (i as f64 * 0.3).sin()).collect();
let ts1: Vec<f64> = (0..n).map(|i| (i as f64 * 0.5).cos()).collect();
let ts_refs: [&[f64]; 2] = [&ts0, &ts1];
let mdp = mstump(&ts_refs, m);
for k in 0..mdp.d {
for (j, &v) in mdp.profile[k].iter().enumerate() {
assert!(
v >= 0.0 || v.is_infinite(),
"Negative distance at P[{k}][{j}] = {v}"
);
}
}
}
#[test]
fn test_mstump_output_shapes() {
let n = 60;
let m = 8;
let d = 3;
let ts0: Vec<f64> = (0..n).map(|i| (i as f64 * 0.2).sin()).collect();
let ts1: Vec<f64> = (0..n).map(|i| (i as f64 * 0.3).cos()).collect();
let ts2: Vec<f64> = (0..n).map(|i| (i as f64 * 0.1).sin() + 1.0).collect();
let ts_refs: [&[f64]; 3] = [&ts0, &ts1, &ts2];
let mdp = mstump(&ts_refs, m);
let n_subs = n - m + 1;
assert_eq!(mdp.d, d);
assert_eq!(mdp.m, m);
assert_eq!(mdp.profile.len(), d);
assert_eq!(mdp.profile_index.len(), d);
for k in 0..d {
assert_eq!(mdp.profile[k].len(), n_subs);
assert_eq!(mdp.profile_index[k].len(), n_subs);
}
}
}