use crate::algorithms::common::apply_exclusion_zone;
use crate::core::matrix_profile::MatrixProfile;
#[derive(Debug, Clone)]
pub struct SegmentationResult {
pub cac: Vec<f64>,
pub regime_boundaries: Vec<usize>,
}
fn compute_arc_counts(profile_index: &[usize], n: usize) -> Vec<usize> {
let mut deltas = vec![0i64; n + 1];
for (i, &j) in profile_index.iter().enumerate() {
if j >= n {
continue; }
let lo = i.min(j);
let hi = i.max(j);
deltas[lo] += 1;
if hi < n + 1 {
deltas[hi] -= 1;
}
}
let mut counts = vec![0usize; n];
let mut running = 0i64;
for i in 0..n {
running += deltas[i];
counts[i] = running.max(0) as usize;
}
counts
}
fn corrected_arc_curve(arc_counts: &[usize], n: usize, m: usize, excl_factor: usize) -> Vec<f64> {
let excl_width = excl_factor * m;
let mut cac = vec![1.0; n];
for p in 0..n {
let max_arcs = (p + 1).min(n - p);
if max_arcs == 0 {
cac[p] = 1.0;
} else {
cac[p] = (arc_counts[p] as f64 / max_arcs as f64).min(1.0);
}
}
for v in cac.iter_mut().take(excl_width.min(n)) {
*v = 1.0;
}
for v in cac.iter_mut().take(n).skip(n.saturating_sub(excl_width)) {
*v = 1.0;
}
cac
}
fn find_regime_boundaries(cac: &[f64], num_regimes: usize, ez: usize) -> Vec<usize> {
let mut working_cac = cac.to_vec();
let mut boundaries = Vec::with_capacity(num_regimes);
for _ in 0..num_regimes {
let (best_idx, &best_val) = match working_cac
.iter()
.enumerate()
.filter(|(_, v)| **v < 1.0)
.min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
{
Some(pair) => pair,
None => break,
};
if best_val >= 1.0 {
break;
}
boundaries.push(best_idx);
apply_exclusion_zone(&mut working_cac, best_idx, ez);
}
boundaries
}
pub fn fluss(mp: &MatrixProfile, num_regimes: usize) -> SegmentationResult {
let n = mp.profile_index.len();
let m = mp.m;
let excl_factor = 5;
let arc_counts = compute_arc_counts(&mp.profile_index, n);
let cac = corrected_arc_curve(&arc_counts, n, m, excl_factor);
let ez = excl_factor * m;
let regime_boundaries = find_regime_boundaries(&cac, num_regimes, ez);
SegmentationResult {
cac,
regime_boundaries,
}
}
pub struct Floss {
cac: Vec<f64>,
window_size: usize,
m: usize,
arc_counts: Vec<usize>,
offset: usize,
}
impl Floss {
pub fn new(mp: &MatrixProfile, window_size: usize) -> Self {
let n = mp.right_profile_index.len();
let ws = window_size.min(n);
let arc_counts = Self::compute_right_arc_counts(&mp.right_profile_index, n, ws);
let cac = Self::compute_cac(&arc_counts, ws, mp.m);
Self {
cac,
window_size: ws,
m: mp.m,
arc_counts,
offset: 0,
}
}
pub fn update(&mut self, new_idx: usize, new_right_neighbor: usize) {
let n = self.arc_counts.len();
if n == 0 {
return;
}
let lo = new_idx.min(new_right_neighbor);
let hi = new_idx.max(new_right_neighbor);
let global_start = self.offset;
for p in (lo + 1)..hi {
if p >= global_start && p - global_start < n {
self.arc_counts[p - global_start] += 1;
}
}
self.cac = Self::compute_cac(&self.arc_counts, self.window_size, self.m);
}
pub fn cac(&self) -> &[f64] {
&self.cac
}
fn compute_right_arc_counts(
right_profile_index: &[usize],
n: usize,
window_size: usize,
) -> Vec<usize> {
let start = n.saturating_sub(window_size);
let ws = n - start;
let mut counts = vec![0usize; ws];
for (global_i, &j) in right_profile_index.iter().enumerate().take(n).skip(start) {
if j <= global_i {
continue; }
let lo = global_i;
for p in (lo + 1)..j {
if p >= start && p - start < ws {
counts[p - start] += 1;
}
}
}
counts
}
fn compute_cac(arc_counts: &[usize], window_size: usize, m: usize) -> Vec<f64> {
let n = arc_counts.len();
let excl_width = 5 * m;
let mut cac = vec![1.0; n];
for p in 0..n {
let max_arcs = (p + 1).min(window_size.saturating_sub(p));
if max_arcs == 0 {
cac[p] = 1.0;
} else {
cac[p] = 1.0 - arc_counts[p] as f64 / max_arcs as f64;
}
}
for v in cac.iter_mut().take(excl_width.min(n)) {
*v = 1.0;
}
for v in cac.iter_mut().take(n).skip(n.saturating_sub(excl_width)) {
*v = 1.0;
}
cac
}
}
#[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_arc_counts_simple() {
let pi = vec![2, 3, 0, 1];
let counts = compute_arc_counts(&pi, 4);
assert_eq!(counts[0], 2); assert_eq!(counts[1], 4); assert_eq!(counts[2], 2); assert_eq!(counts[3], 0);
}
#[test]
fn test_arc_counts_adjacent() {
let pi = vec![1, 0, 3, 2];
let counts = compute_arc_counts(&pi, 4);
assert_eq!(counts[0], 2);
assert_eq!(counts[1], 0);
assert_eq!(counts[2], 2);
assert_eq!(counts[3], 0);
}
#[test]
fn test_cac_bounds() {
let arc_counts = vec![0, 5, 10, 5, 0];
let cac = corrected_arc_curve(&arc_counts, 5, 1, 0);
for (i, &v) in cac.iter().enumerate() {
assert!(
(0.0..=1.0).contains(&v),
"CAC value out of [0,1] at {i}: {v}"
);
}
}
#[test]
fn test_fluss_regime_change() {
let n = 500;
let m = 10;
let mut ts: Vec<f64> = Vec::with_capacity(n);
for i in 0..250 {
ts.push((i as f64 * std::f64::consts::TAU / 20.0).sin());
}
for i in 250..500 {
let phase = (i - 250) % 15;
ts.push(phase as f64 / 15.0 * 2.0 - 1.0);
}
let config = MatrixProfileConfig::new(m);
let mp = stomp::<ZNormalizedEuclidean>(&ts, &config);
let result = fluss(&mp, 1);
assert_eq!(result.cac.len(), mp.profile_index.len());
for (i, &v) in result.cac.iter().enumerate() {
assert!((0.0..=1.0).contains(&v), "CAC[{i}] out of [0,1]: {v}");
}
assert!(
!result.regime_boundaries.is_empty(),
"Should detect at least one regime boundary"
);
let boundary = result.regime_boundaries[0];
let excl_width = 5 * m;
let n_subs = n - m + 1;
assert!(
boundary >= excl_width && boundary < n_subs - excl_width,
"Regime boundary at {boundary} should be in the valid CAC zone"
);
}
#[test]
fn test_fluss_cac_edge_nullification() {
let ts: Vec<f64> = (0..100).map(|i| (i as f64 * 0.2).sin()).collect();
let m = 8;
let config = MatrixProfileConfig::new(m);
let mp = stomp::<ZNormalizedEuclidean>(&ts, &config);
let result = fluss(&mp, 1);
let excl_width = 5 * m;
for i in 0..excl_width.min(result.cac.len()) {
assert!(
(result.cac[i] - 1.0).abs() < 1e-10,
"Left edge CAC[{i}] should be 1.0, got {}",
result.cac[i]
);
}
}
#[test]
fn test_floss_creation() {
let ts: Vec<f64> = (0..100).map(|i| (i as f64 * 0.2).sin()).collect();
let config = MatrixProfileConfig::new(8);
let mp = stomp::<ZNormalizedEuclidean>(&ts, &config);
let floss = Floss::new(&mp, 50);
assert!(!floss.cac().is_empty());
for &v in floss.cac() {
assert!(
(0.0..=1.0).contains(&v),
"FLOSS CAC value out of [0,1]: {v}"
);
}
}
}