use crate::matrix::FdMatrix;
use super::compute_mean_curve;
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct MatrixProfileResult {
pub profile: Vec<f64>,
pub profile_index: Vec<usize>,
pub subsequence_length: usize,
pub detected_periods: Vec<f64>,
pub arc_counts: Vec<usize>,
pub primary_period: f64,
pub confidence: f64,
}
pub fn matrix_profile(
values: &[f64],
subsequence_length: Option<usize>,
exclusion_zone: Option<f64>,
) -> MatrixProfileResult {
let n = values.len();
let m = subsequence_length.unwrap_or_else(|| {
let default_m = n / 4;
default_m.max(4).min(n / 2)
});
if m < 3 || m > n / 2 {
return MatrixProfileResult {
profile: vec![],
profile_index: vec![],
subsequence_length: m,
detected_periods: vec![],
arc_counts: vec![],
primary_period: f64::NAN,
confidence: 0.0,
};
}
let exclusion_zone = exclusion_zone.unwrap_or(0.5);
let exclusion_radius = (m as f64 * exclusion_zone).ceil() as usize;
let profile_len = n - m + 1;
let (means, stds) = compute_sliding_stats(values, m);
let (profile, profile_index) = stomp_core(values, m, &means, &stds, exclusion_radius);
let (arc_counts, detected_periods, primary_period, confidence) =
analyze_arcs(&profile_index, profile_len, m);
MatrixProfileResult {
profile,
profile_index,
subsequence_length: m,
detected_periods,
arc_counts,
primary_period,
confidence,
}
}
fn compute_sliding_stats(values: &[f64], m: usize) -> (Vec<f64>, Vec<f64>) {
let n = values.len();
let profile_len = n - m + 1;
let mut cumsum = vec![0.0; n + 1];
let mut cumsum_sq = vec![0.0; n + 1];
for i in 0..n {
cumsum[i + 1] = cumsum[i] + values[i];
cumsum_sq[i + 1] = cumsum_sq[i] + values[i] * values[i];
}
let mut means = Vec::with_capacity(profile_len);
let mut stds = Vec::with_capacity(profile_len);
let m_f64 = m as f64;
for i in 0..profile_len {
let sum = cumsum[i + m] - cumsum[i];
let sum_sq = cumsum_sq[i + m] - cumsum_sq[i];
let mean = sum / m_f64;
let variance = (sum_sq / m_f64) - mean * mean;
let std = variance.max(0.0).sqrt();
means.push(mean);
stds.push(std.max(1e-10)); }
(means, stds)
}
fn stomp_core(
values: &[f64],
m: usize,
means: &[f64],
stds: &[f64],
exclusion_radius: usize,
) -> (Vec<f64>, Vec<usize>) {
let n = values.len();
let profile_len = n - m + 1;
let mut profile = vec![f64::INFINITY; profile_len];
let mut profile_index = vec![0usize; profile_len];
let mut qt = vec![0.0; profile_len];
for j in 0..profile_len {
let mut dot = 0.0;
for k in 0..m {
dot += values[k] * values[j + k];
}
qt[j] = dot;
}
update_profile_row(
0,
&qt,
means,
stds,
m,
exclusion_radius,
&mut profile,
&mut profile_index,
);
for i in 1..profile_len {
let mut qt_new = vec![0.0; profile_len];
let mut dot = 0.0;
for k in 0..m {
dot += values[i + k] * values[k];
}
qt_new[0] = dot;
for j in 1..profile_len {
qt_new[j] =
qt[j - 1] - values[i - 1] * values[j - 1] + values[i + m - 1] * values[j + m - 1];
}
qt = qt_new;
update_profile_row(
i,
&qt,
means,
stds,
m,
exclusion_radius,
&mut profile,
&mut profile_index,
);
}
(profile, profile_index)
}
fn update_profile_row(
i: usize,
qt: &[f64],
means: &[f64],
stds: &[f64],
m: usize,
exclusion_radius: usize,
profile: &mut [f64],
profile_index: &mut [usize],
) {
let profile_len = profile.len();
let m_f64 = m as f64;
for j in 0..profile_len {
if i.abs_diff(j) <= exclusion_radius {
continue;
}
let numerator = qt[j] - m_f64 * means[i] * means[j];
let denominator = m_f64 * stds[i] * stds[j];
let pearson = if denominator > 0.0 {
(numerator / denominator).clamp(-1.0, 1.0)
} else {
0.0
};
let dist_sq = 2.0 * m_f64 * (1.0 - pearson);
let dist = dist_sq.max(0.0).sqrt();
if dist < profile[i] {
profile[i] = dist;
profile_index[i] = j;
}
if dist < profile[j] {
profile[j] = dist;
profile_index[j] = i;
}
}
}
fn analyze_arcs(
profile_index: &[usize],
profile_len: usize,
m: usize,
) -> (Vec<usize>, Vec<f64>, f64, f64) {
let max_distance = profile_len;
let mut arc_counts = vec![0usize; max_distance];
for (i, &j) in profile_index.iter().enumerate() {
let distance = i.abs_diff(j);
if distance < max_distance {
arc_counts[distance] += 1;
}
}
let min_period = m / 2; let mut peaks: Vec<(usize, usize)> = Vec::new();
for i in min_period..arc_counts.len().saturating_sub(1) {
if arc_counts[i] > arc_counts[i.saturating_sub(1)]
&& arc_counts[i] > arc_counts[(i + 1).min(arc_counts.len() - 1)]
&& arc_counts[i] >= 3
{
peaks.push((i, arc_counts[i]));
}
}
peaks.sort_by(|a, b| b.1.cmp(&a.1));
let detected_periods: Vec<f64> = peaks.iter().take(5).map(|(p, _)| *p as f64).collect();
let (primary_period, confidence) = if let Some(&(period, count)) = peaks.first() {
let total_arcs: usize = arc_counts[min_period..].iter().sum();
let conf = if total_arcs > 0 {
count as f64 / total_arcs as f64
} else {
0.0
};
(period as f64, conf.min(1.0))
} else {
(0.0, 0.0)
};
(arc_counts, detected_periods, primary_period, confidence)
}
pub fn matrix_profile_fdata(
data: &FdMatrix,
subsequence_length: Option<usize>,
exclusion_zone: Option<f64>,
) -> MatrixProfileResult {
let mean_curve = compute_mean_curve(data);
matrix_profile(&mean_curve, subsequence_length, exclusion_zone)
}
pub fn matrix_profile_seasonality(
values: &[f64],
subsequence_length: Option<usize>,
confidence_threshold: Option<f64>,
) -> (bool, f64, f64) {
let result = matrix_profile(values, subsequence_length, None);
let threshold = confidence_threshold.unwrap_or(0.1);
let is_seasonal = result.confidence >= threshold && result.primary_period > 0.0;
(is_seasonal, result.primary_period, result.confidence)
}