use crate::core::matrix_profile::MatrixProfile;
#[derive(Debug, Clone)]
pub struct Chain {
pub indices: Vec<usize>,
}
impl Chain {
pub fn len(&self) -> usize {
self.indices.len()
}
pub fn is_empty(&self) -> bool {
self.indices.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct ChainsResult {
pub chains: Vec<Chain>,
pub longest: Chain,
pub longest_anchored: Chain,
}
pub fn atsc(mp: &MatrixProfile, anchor: usize) -> Chain {
let n = mp.profile.len();
assert!(anchor < n, "anchor {anchor} out of range (n_subs={n})");
let mut indices = vec![anchor];
let mut current = anchor;
loop {
let next = mp.right_profile_index[current];
if next >= n || next <= current {
break;
}
if mp.left_profile_index[next] != current {
break;
}
indices.push(next);
current = next;
}
Chain { indices }
}
pub fn allc(mp: &MatrixProfile) -> ChainsResult {
let n = mp.profile.len();
let mut fwd = vec![None; n];
let mut has_predecessor = vec![false; n];
for (i, fwd_i) in fwd.iter_mut().enumerate() {
let j = mp.right_profile_index[i];
if j < n && j > i && mp.left_profile_index[j] == i {
*fwd_i = Some(j);
has_predecessor[j] = true;
}
}
let mut chains = Vec::new();
let mut longest = Chain {
indices: Vec::new(),
};
let mut longest_anchored = Chain {
indices: Vec::new(),
};
let mut visited = vec![false; n];
for start in 0..n {
if visited[start] {
continue;
}
let mut indices = vec![start];
visited[start] = true;
let mut current = start;
while let Some(next) = fwd[current] {
if visited[next] {
break;
}
indices.push(next);
visited[next] = true;
current = next;
}
if indices.len() >= 2 {
let chain = Chain {
indices: indices.clone(),
};
if chain.len() > longest.len() {
longest = chain.clone();
}
if !has_predecessor[start] && chain.len() > longest_anchored.len() {
longest_anchored = chain.clone();
}
chains.push(chain);
}
}
if longest.is_empty() {
longest = Chain { indices: vec![0] };
longest_anchored = Chain { indices: vec![0] };
}
if longest_anchored.is_empty() {
longest_anchored = longest.clone();
}
ChainsResult {
chains,
longest,
longest_anchored,
}
}
#[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_atsc_basic() {
let n = 200;
let m = 10;
let ts: Vec<f64> = (0..n)
.map(|i| (i as f64 * 2.0 * std::f64::consts::PI / 50.0).sin())
.collect();
let config = MatrixProfileConfig::new(m);
let mp = stomp::<ZNormalizedEuclidean>(&ts, &config);
let chain = atsc(&mp, 0);
assert!(!chain.is_empty());
for w in chain.indices.windows(2) {
assert!(w[0] < w[1], "Chain indices should be increasing");
}
}
#[test]
fn test_allc_basic() {
let n = 200;
let m = 10;
let ts: Vec<f64> = (0..n)
.map(|i| (i as f64 * 2.0 * std::f64::consts::PI / 50.0).sin())
.collect();
let config = MatrixProfileConfig::new(m);
let mp = stomp::<ZNormalizedEuclidean>(&ts, &config);
let result = allc(&mp);
assert!(!result.longest.is_empty(), "Should find at least one chain");
assert!(result.longest_anchored.len() <= result.longest.len() + 1);
}
#[test]
fn test_atsc_single_index() {
let mp = MatrixProfile::new(5, 3, 1);
let chain = atsc(&mp, 0);
assert_eq!(chain.len(), 1);
assert_eq!(chain.indices, vec![0]);
}
#[test]
fn test_chain_indices_temporal_order() {
let n = 300;
let m = 15;
let ts: Vec<f64> = (0..n)
.map(|i| (i as f64 * 0.1).sin() + (i as f64 * 0.03).cos() * 0.5)
.collect();
let config = MatrixProfileConfig::new(m);
let mp = stomp::<ZNormalizedEuclidean>(&ts, &config);
let result = allc(&mp);
for chain in &result.chains {
for w in chain.indices.windows(2) {
assert!(
w[0] < w[1],
"Chain indices must be strictly increasing: {} >= {}",
w[0],
w[1]
);
}
}
}
}