use crate::algorithms::decomposition::count_to_f64;
#[must_use]
pub fn pelt_l2(signal: &[f64], penalty: f64, min_size: usize) -> Vec<usize> {
let n = signal.len();
if n == 0 {
return Vec::new();
}
let min_size = min_size.max(1);
let beta = penalty.max(0.0);
let (prefix, prefix_sq) = prefix_sums(signal);
let mut best_cost = vec![f64::INFINITY; n + 1];
if let Some(slot) = best_cost.first_mut() {
*slot = -beta;
}
let mut last_change = vec![0_usize; n + 1];
let mut candidates: Vec<usize> = vec![0];
for end in 1..=n {
if end < min_size {
continue;
}
let mut best = f64::INFINITY;
let mut best_start = 0_usize;
for &start in &candidates {
if end - start < min_size {
continue;
}
let prior = best_cost.get(start).copied().unwrap_or(f64::INFINITY);
if !prior.is_finite() {
continue;
}
let cost = prior + segment_cost(&prefix, &prefix_sq, start, end) + beta;
if cost < best {
best = cost;
best_start = start;
}
}
if let Some(slot) = best_cost.get_mut(end) {
*slot = best;
}
if let Some(slot) = last_change.get_mut(end) {
*slot = best_start;
}
prune(
&mut candidates,
&best_cost,
&prefix,
&prefix_sq,
end,
beta,
min_size,
);
}
backtrack(&last_change, n)
}
fn prefix_sums(signal: &[f64]) -> (Vec<f64>, Vec<f64>) {
let n = signal.len();
let mut prefix = vec![0.0_f64; n + 1];
let mut prefix_sq = vec![0.0_f64; n + 1];
let mut running = 0.0_f64;
let mut running_sq = 0.0_f64;
for (i, &x) in signal.iter().enumerate() {
running += x;
running_sq = x.mul_add(x, running_sq);
if let Some(slot) = prefix.get_mut(i + 1) {
*slot = running;
}
if let Some(slot) = prefix_sq.get_mut(i + 1) {
*slot = running_sq;
}
}
(prefix, prefix_sq)
}
fn segment_cost(prefix: &[f64], prefix_sq: &[f64], start: usize, end: usize) -> f64 {
let len = count_to_f64(end - start);
if len <= 0.0 {
return 0.0;
}
let sum = prefix.get(end).copied().unwrap_or(0.0) - prefix.get(start).copied().unwrap_or(0.0);
let sum_sq =
prefix_sq.get(end).copied().unwrap_or(0.0) - prefix_sq.get(start).copied().unwrap_or(0.0);
sum_sq - (sum * sum) / len
}
fn prune(
candidates: &mut Vec<usize>,
best_cost: &[f64],
prefix: &[f64],
prefix_sq: &[f64],
end: usize,
beta: f64,
min_size: usize,
) {
let reference = best_cost.get(end).copied().unwrap_or(f64::INFINITY);
candidates.retain(|&start| {
let prior = best_cost.get(start).copied().unwrap_or(f64::INFINITY);
if !prior.is_finite() {
return false;
}
prior + segment_cost(prefix, prefix_sq, start, end) <= reference + beta
});
if end + min_size <= prefix.len().saturating_sub(1) || end >= min_size {
candidates.push(end);
}
}
fn backtrack(last_change: &[usize], n: usize) -> Vec<usize> {
let mut breakpoints = Vec::new();
let mut end = n;
while end > 0 {
breakpoints.push(end);
let prev = last_change.get(end).copied().unwrap_or(0);
if prev >= end {
break;
}
end = prev;
}
breakpoints.reverse();
breakpoints
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn single_step_is_detected() {
let signal = [0.0, 0.0, 0.0, 10.0, 10.0, 10.0];
let bkps = pelt_l2(&signal, 1.0, 1);
assert_eq!(bkps, vec![3, 6], "breakpoints were {bkps:?}");
}
#[test]
fn constant_signal_has_no_interior_change_point() {
let signal = [4.0, 4.0, 4.0, 4.0, 4.0];
let bkps = pelt_l2(&signal, 5.0, 1);
assert_eq!(bkps, vec![5], "breakpoints were {bkps:?}");
}
#[test]
fn empty_signal_is_empty() {
assert!(pelt_l2(&[], 1.0, 1).is_empty());
}
#[test]
fn high_penalty_suppresses_weak_change_points() {
let signal = [0.0, 0.0, 0.1, 0.0, 0.0];
let bkps = pelt_l2(&signal, 100.0, 1);
assert_eq!(bkps, vec![5], "breakpoints were {bkps:?}");
}
}