pub const DEFAULT_SIGMA_THRESHOLD: f64 = 1e-15;
#[derive(Debug, Clone)]
pub struct MatrixProfileConfig {
pub m: usize,
pub ignore_trivial: bool,
pub exclusion_zone_denom: usize,
pub sigma_threshold: f64,
}
impl MatrixProfileConfig {
pub fn new(m: usize) -> Self {
Self {
m,
ignore_trivial: true,
exclusion_zone_denom: 4,
sigma_threshold: DEFAULT_SIGMA_THRESHOLD,
}
}
pub fn exclusion_zone(&self) -> usize {
if self.ignore_trivial {
(self.m as f64 / self.exclusion_zone_denom as f64).ceil() as usize
} else {
0
}
}
}
#[derive(Debug, Clone)]
pub struct MatrixProfile {
pub profile: Vec<f64>,
pub profile_index: Vec<usize>,
pub left_profile: Vec<f64>,
pub left_profile_index: Vec<usize>,
pub right_profile: Vec<f64>,
pub right_profile_index: Vec<usize>,
pub m: usize,
pub exclusion_zone: usize,
}
impl MatrixProfile {
pub fn new(n_subs: usize, m: usize, exclusion_zone: usize) -> Self {
Self {
profile: vec![f64::INFINITY; n_subs],
profile_index: vec![0; n_subs],
left_profile: vec![f64::INFINITY; n_subs],
left_profile_index: vec![0; n_subs],
right_profile: vec![f64::INFINITY; n_subs],
right_profile_index: vec![0; n_subs],
m,
exclusion_zone,
}
}
pub fn merge(&mut self, other: &MatrixProfile) {
debug_assert_eq!(self.profile.len(), other.profile.len());
for i in 0..self.profile.len() {
if other.profile[i] < self.profile[i] {
self.profile[i] = other.profile[i];
self.profile_index[i] = other.profile_index[i];
}
if other.left_profile[i] < self.left_profile[i] {
self.left_profile[i] = other.left_profile[i];
self.left_profile_index[i] = other.left_profile_index[i];
}
if other.right_profile[i] < self.right_profile[i] {
self.right_profile[i] = other.right_profile[i];
self.right_profile_index[i] = other.right_profile_index[i];
}
}
}
#[inline]
pub fn update(&mut self, idx: usize, distance: f64, neighbor_idx: usize) {
if distance < self.profile[idx] {
self.profile[idx] = distance;
self.profile_index[idx] = neighbor_idx;
}
if neighbor_idx < idx && distance < self.left_profile[idx] {
self.left_profile[idx] = distance;
self.left_profile_index[idx] = neighbor_idx;
}
if neighbor_idx > idx && distance < self.right_profile[idx] {
self.right_profile[idx] = distance;
self.right_profile_index[idx] = neighbor_idx;
}
}
}
#[derive(Debug, Clone)]
pub struct RollingStats {
pub mean: Vec<f64>,
pub std: Vec<f64>,
pub m_sigma_inv: Vec<f64>,
pub has_constant: bool,
}
impl RollingStats {
pub fn compute(ts: &[f64], m: usize) -> Self {
Self::compute_with_threshold(ts, m, DEFAULT_SIGMA_THRESHOLD)
}
pub fn compute_with_threshold(ts: &[f64], m: usize, sigma_threshold: f64) -> Self {
assert!(m > 0, "Subsequence length must be > 0");
assert!(ts.len() >= m, "Time series must be at least as long as m");
let n = ts.len();
let n_subs = 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] + ts[i];
cumsum_sq[i + 1] = cumsum_sq[i] + ts[i] * ts[i];
}
let mut mean = vec![0.0; n_subs];
let mut std = vec![0.0; n_subs];
let mut m_sigma_inv = vec![0.0; n_subs];
let mut has_constant = false;
let m_f = m as f64;
let sqrt_m = m_f.sqrt();
for i in 0..n_subs {
let sum = cumsum[i + m] - cumsum[i];
let sum_sq = cumsum_sq[i + m] - cumsum_sq[i];
let mu = sum / m_f;
let var = (sum_sq / m_f - mu * mu).max(0.0);
let sigma = var.sqrt();
mean[i] = mu;
std[i] = sigma;
if sigma < sigma_threshold {
m_sigma_inv[i] = 0.0;
has_constant = true;
} else {
m_sigma_inv[i] = 1.0 / (sqrt_m * sigma);
}
}
Self {
mean,
std,
m_sigma_inv,
has_constant,
}
}
pub fn extend(&mut self, ts: &[f64], m: usize) {
self.extend_with_threshold(ts, m, DEFAULT_SIGMA_THRESHOLD);
}
pub fn extend_with_threshold(&mut self, ts: &[f64], m: usize, sigma_threshold: f64) {
let n = ts.len();
assert!(n >= m);
let start = n - m;
let m_f = m as f64;
let sum: f64 = ts[start..n].iter().sum();
let sum_sq: f64 = ts[start..n].iter().map(|x| x * x).sum();
let mu = sum / m_f;
let var = (sum_sq / m_f - mu * mu).max(0.0);
let sigma = var.sqrt();
self.mean.push(mu);
self.std.push(sigma);
if sigma < sigma_threshold {
self.m_sigma_inv.push(0.0);
self.has_constant = true;
} else {
self.m_sigma_inv.push(1.0 / (m_f.sqrt() * sigma));
}
}
}
#[derive(Clone, Copy)]
#[repr(C)]
pub(crate) struct AccEntry {
pub neg_corr: f64,
pub right_neg_corr: f64,
pub left_neg_corr: f64,
pub index: usize,
pub right_index: usize,
pub left_index: usize,
}
pub(crate) struct ProfileAccumulator {
pub entries: Vec<AccEntry>,
}
impl ProfileAccumulator {
pub fn new(n: usize) -> Self {
Self {
entries: vec![
AccEntry {
neg_corr: f64::INFINITY,
right_neg_corr: f64::INFINITY,
left_neg_corr: f64::INFINITY,
index: 0,
right_index: 0,
left_index: 0,
};
n
],
}
}
#[inline(always)]
pub fn update_right(&mut self, idx: usize, neg_corr: f64, neighbor: usize) {
let e = unsafe { self.entries.get_unchecked_mut(idx) };
if neg_corr < e.neg_corr {
e.neg_corr = neg_corr;
e.index = neighbor;
}
if neg_corr < e.right_neg_corr {
e.right_neg_corr = neg_corr;
e.right_index = neighbor;
}
}
#[inline(always)]
pub fn update_left(&mut self, idx: usize, neg_corr: f64, neighbor: usize) {
let e = unsafe { self.entries.get_unchecked_mut(idx) };
if neg_corr < e.neg_corr {
e.neg_corr = neg_corr;
e.index = neighbor;
}
if neg_corr < e.left_neg_corr {
e.left_neg_corr = neg_corr;
e.left_index = neighbor;
}
}
#[cfg(feature = "parallel")]
pub fn merge(&mut self, other: &Self) {
for (a, b) in self.entries.iter_mut().zip(other.entries.iter()) {
if b.neg_corr < a.neg_corr {
a.neg_corr = b.neg_corr;
a.index = b.index;
}
if b.left_neg_corr < a.left_neg_corr {
a.left_neg_corr = b.left_neg_corr;
a.left_index = b.left_index;
}
if b.right_neg_corr < a.right_neg_corr {
a.right_neg_corr = b.right_neg_corr;
a.right_index = b.right_index;
}
}
}
pub fn write_to_matrix_profile(&self, mp: &mut MatrixProfile, convert: impl Fn(f64) -> f64) {
for (i, e) in self.entries.iter().enumerate() {
mp.profile[i] = convert(e.neg_corr);
mp.profile_index[i] = e.index;
mp.left_profile[i] = convert(e.left_neg_corr);
mp.left_profile_index[i] = e.left_index;
mp.right_profile[i] = convert(e.right_neg_corr);
mp.right_profile_index[i] = e.right_index;
}
}
}
#[derive(Debug, Clone)]
pub struct JoinProfile {
pub distances: Vec<f64>,
pub indices: Vec<usize>,
pub m: usize,
}
impl JoinProfile {
pub fn new(n_subs: usize, m: usize) -> Self {
Self {
distances: vec![f64::INFINITY; n_subs],
indices: vec![0; n_subs],
m,
}
}
}
pub(crate) struct JoinAccumulator {
pub neg_corrs: Vec<f64>,
pub indices: Vec<usize>,
}
impl JoinAccumulator {
pub fn new(n: usize) -> Self {
Self {
neg_corrs: vec![f64::INFINITY; n],
indices: vec![0; n],
}
}
#[inline(always)]
pub fn update(&mut self, idx: usize, neg_corr: f64, neighbor: usize) {
unsafe {
let curr = self.neg_corrs.get_unchecked_mut(idx);
if neg_corr < *curr {
*curr = neg_corr;
*self.indices.get_unchecked_mut(idx) = neighbor;
}
}
}
#[cfg(feature = "parallel")]
pub fn merge(&mut self, other: &Self) {
for i in 0..self.neg_corrs.len() {
if other.neg_corrs[i] < self.neg_corrs[i] {
self.neg_corrs[i] = other.neg_corrs[i];
self.indices[i] = other.indices[i];
}
}
}
pub fn write_to_join_profile(&self, jp: &mut JoinProfile, convert: impl Fn(f64) -> f64) {
for (i, (&nc, &idx)) in self.neg_corrs.iter().zip(self.indices.iter()).enumerate() {
jp.distances[i] = convert(nc);
jp.indices[i] = idx;
}
}
}
pub(crate) struct JoinAccumulatorDist {
pub distances: Vec<f64>,
pub indices: Vec<usize>,
}
impl JoinAccumulatorDist {
pub fn new(n: usize) -> Self {
Self {
distances: vec![f64::INFINITY; n],
indices: vec![0; n],
}
}
#[inline(always)]
pub fn update(&mut self, idx: usize, dist: f64, neighbor: usize) {
if dist < self.distances[idx] {
self.distances[idx] = dist;
self.indices[idx] = neighbor;
}
}
#[cfg(feature = "parallel")]
pub fn merge(&mut self, other: &Self) {
for i in 0..self.distances.len() {
if other.distances[i] < self.distances[i] {
self.distances[i] = other.distances[i];
self.indices[i] = other.indices[i];
}
}
}
pub fn write_to_join_profile(&self, jp: &mut JoinProfile) {
for i in 0..self.distances.len() {
jp.distances[i] = self.distances[i];
jp.indices[i] = self.indices[i];
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rolling_stats_simple() {
let ts = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let stats = RollingStats::compute(&ts, 3);
assert_eq!(stats.mean.len(), 3);
assert!((stats.mean[0] - 2.0).abs() < 1e-10);
assert!((stats.mean[1] - 3.0).abs() < 1e-10);
assert!((stats.mean[2] - 4.0).abs() < 1e-10);
let expected_std = (2.0_f64 / 3.0).sqrt();
for s in &stats.std {
assert!((s - expected_std).abs() < 1e-10);
}
}
#[test]
fn test_rolling_stats_constant() {
let ts = vec![5.0; 10];
let stats = RollingStats::compute(&ts, 4);
for mu in &stats.mean {
assert!((mu - 5.0).abs() < 1e-10);
}
for s in &stats.std {
assert!(*s < 1e-10);
}
}
#[test]
fn test_rolling_stats_extend() {
let mut ts = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let mut stats = RollingStats::compute(&ts, 3);
assert_eq!(stats.mean.len(), 3);
ts.push(6.0);
stats.extend(&ts, 3);
assert_eq!(stats.mean.len(), 4);
assert!((stats.mean[3] - 5.0).abs() < 1e-10); }
#[test]
fn test_matrix_profile_update() {
let mut mp = MatrixProfile::new(5, 3, 1);
mp.update(0, 1.5, 3);
assert!((mp.profile[0] - 1.5).abs() < 1e-10);
assert_eq!(mp.profile_index[0], 3);
assert!((mp.right_profile[0] - 1.5).abs() < 1e-10);
mp.update(0, 0.5, 2);
assert!((mp.profile[0] - 0.5).abs() < 1e-10);
assert_eq!(mp.profile_index[0], 2);
mp.update(0, 2.0, 4);
assert!((mp.profile[0] - 0.5).abs() < 1e-10);
}
#[test]
fn test_matrix_profile_merge() {
let mut a = MatrixProfile::new(3, 4, 1);
let mut b = MatrixProfile::new(3, 4, 1);
a.update(0, 1.0, 2); a.update(1, 3.0, 0); a.update(2, 2.0, 0);
b.update(0, 2.0, 1); b.update(1, 1.0, 2); b.update(2, 1.5, 0);
a.merge(&b);
assert!((a.profile[0] - 1.0).abs() < 1e-10);
assert_eq!(a.profile_index[0], 2); assert!((a.profile[1] - 1.0).abs() < 1e-10);
assert_eq!(a.profile_index[1], 2); assert!((a.profile[2] - 1.5).abs() < 1e-10);
assert_eq!(a.profile_index[2], 0);
assert!(a.left_profile[0].is_infinite());
assert!((a.left_profile[1] - 3.0).abs() < 1e-10);
assert!((a.left_profile[2] - 1.5).abs() < 1e-10);
assert!((a.right_profile[0] - 1.0).abs() < 1e-10);
assert!((a.right_profile[1] - 1.0).abs() < 1e-10);
assert!(a.right_profile[2].is_infinite());
}
#[test]
fn test_exclusion_zone() {
let config = MatrixProfileConfig::new(8);
assert_eq!(config.exclusion_zone(), 2);
let config = MatrixProfileConfig::new(10);
assert_eq!(config.exclusion_zone(), 3);
let mut config = MatrixProfileConfig::new(10);
config.ignore_trivial = false;
assert_eq!(config.exclusion_zone(), 0);
}
#[test]
fn test_config_sigma_threshold_default() {
let config = MatrixProfileConfig::new(10);
assert_eq!(config.sigma_threshold, 1e-15);
}
#[test]
fn test_config_sigma_threshold_custom() {
let ts = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let stats_default = RollingStats::compute(&ts, 3);
assert!(!stats_default.has_constant);
let stats_high = RollingStats::compute_with_threshold(&ts, 3, 1.0);
assert!(stats_high.has_constant);
for &inv in &stats_high.m_sigma_inv {
assert_eq!(inv, 0.0);
}
}
}