use numra_core::Scalar;
#[derive(Clone, Debug, Default)]
pub struct PeakOptions<S: Scalar> {
pub min_height: Option<S>,
pub min_distance: Option<usize>,
pub min_prominence: Option<S>,
}
impl<S: Scalar> PeakOptions<S> {
pub fn height(mut self, h: S) -> Self {
self.min_height = Some(h);
self
}
pub fn distance(mut self, d: usize) -> Self {
self.min_distance = Some(d);
self
}
pub fn prominence(mut self, p: S) -> Self {
self.min_prominence = Some(p);
self
}
}
pub fn find_peaks<S: Scalar>(x: &[S], opts: &PeakOptions<S>) -> Vec<usize> {
let n = x.len();
if n < 3 {
return vec![];
}
let mut candidates: Vec<usize> = Vec::new();
for i in 1..n - 1 {
if x[i] > x[i - 1] && x[i] > x[i + 1] {
candidates.push(i);
}
}
if let Some(min_h) = opts.min_height {
candidates.retain(|&i| x[i] >= min_h);
}
if let Some(min_prom) = opts.min_prominence {
candidates.retain(|&i| {
let prom = compute_prominence(x, i);
prom >= min_prom
});
}
if let Some(min_dist) = opts.min_distance {
if min_dist > 0 {
candidates = filter_by_distance(x, &candidates, min_dist);
}
}
candidates
}
fn compute_prominence<S: Scalar>(x: &[S], idx: usize) -> S {
let n = x.len();
let peak_val = x[idx];
let mut left_min = peak_val;
for i in (0..idx).rev() {
if x[i] > peak_val {
break;
}
if x[i] < left_min {
left_min = x[i];
}
}
let mut right_min = peak_val;
for i in (idx + 1)..n {
if x[i] > peak_val {
break;
}
if x[i] < right_min {
right_min = x[i];
}
}
let ref_level = if left_min > right_min {
left_min
} else {
right_min
};
peak_val - ref_level
}
fn filter_by_distance<S: Scalar>(x: &[S], peaks: &[usize], min_dist: usize) -> Vec<usize> {
if peaks.is_empty() {
return vec![];
}
let mut sorted: Vec<usize> = peaks.to_vec();
sorted.sort_by(|&a, &b| {
x[b].to_f64()
.partial_cmp(&x[a].to_f64())
.unwrap_or(std::cmp::Ordering::Equal)
});
let mut keep = vec![false; x.len()];
let mut result = Vec::new();
for &idx in &sorted {
let lo = idx.saturating_sub(min_dist);
let hi = (idx + min_dist).min(x.len() - 1);
let mut conflict = false;
for j in lo..=hi {
if keep[j] && j != idx {
conflict = true;
break;
}
}
if !conflict {
keep[idx] = true;
result.push(idx);
}
}
result.sort();
result
}
#[cfg(test)]
mod tests {
use super::*;
use core::f64::consts::PI;
#[test]
fn test_find_peaks_basic() {
let x = vec![0.0, 1.0, 0.0, 2.0, 0.0, 1.5, 0.0];
let peaks = find_peaks(&x, &PeakOptions::default());
assert_eq!(peaks, vec![1, 3, 5]);
}
#[test]
fn test_find_peaks_with_height() {
let x = vec![0.0, 1.0, 0.0, 2.0, 0.0, 1.5, 0.0];
let peaks = find_peaks(&x, &PeakOptions::default().height(1.8));
assert_eq!(peaks, vec![3]);
}
#[test]
fn test_find_peaks_with_distance() {
let x = vec![0.0, 3.0, 0.0, 2.0, 0.0, 1.0, 0.0];
let all = find_peaks(&x, &PeakOptions::default());
assert_eq!(all, vec![1, 3, 5]);
let peaks = find_peaks(&x, &PeakOptions::default().distance(3));
assert_eq!(peaks, vec![1, 5]);
}
#[test]
fn test_find_peaks_with_prominence() {
let x = vec![0.0, 5.0, 4.5, 4.8, 0.0];
let peaks = find_peaks(&x, &PeakOptions::default().prominence(1.0));
assert_eq!(peaks, vec![1]);
}
#[test]
fn test_find_peaks_sine() {
let n = 200;
let pi2 = 2.0 * PI;
let x: Vec<f64> = (0..n)
.map(|i| (pi2 * 5.0 * i as f64 / n as f64).sin())
.collect();
let peaks = find_peaks(&x, &PeakOptions::default());
assert_eq!(peaks.len(), 5);
}
#[test]
fn test_find_peaks_empty() {
assert!(find_peaks::<f64>(&[], &PeakOptions::default()).is_empty());
assert!(find_peaks(&[1.0], &PeakOptions::default()).is_empty());
assert!(find_peaks(&[1.0, 2.0], &PeakOptions::default()).is_empty());
}
#[test]
fn test_find_peaks_monotonic() {
let x: Vec<f64> = (0..10).map(|i| i as f64).collect();
assert!(find_peaks(&x, &PeakOptions::default()).is_empty());
}
#[test]
fn test_find_peaks_plateau() {
let x = vec![0.0, 1.0, 1.0, 0.0];
assert!(find_peaks(&x, &PeakOptions::default()).is_empty());
}
#[test]
fn test_prominence_calculation() {
let x = vec![0.0, 0.0, 5.0, 0.0, 0.0];
let prom = compute_prominence(&x, 2);
assert!((prom - 5.0).abs() < 1e-10);
let x = vec![0.0, 2.0, 3.0, 2.0, 4.0];
let prom = compute_prominence(&x, 2);
assert!((prom - 1.0).abs() < 1e-10);
}
}