const EPSILON: f32 = 1e-10;
pub fn find_peaks(
signal: &[f32],
threshold: f32,
min_distance: usize,
) -> Vec<(usize, f32)> {
log::debug!(
"Finding peaks in signal of length {}, threshold={:.3}, min_distance={}",
signal.len(),
threshold,
min_distance
);
if signal.is_empty() {
return vec![];
}
if signal.len() < 3 {
return vec![];
}
let max_value = signal.iter().copied().fold(0.0f32, f32::max);
let actual_threshold = if threshold > 0.0 && threshold < 1.0 && max_value > 0.0 {
max_value * threshold
} else {
threshold
};
if max_value < EPSILON {
return vec![];
}
let mut peaks = Vec::new();
for i in 1..(signal.len() - 1) {
let value = signal[i];
if value > signal[i - 1] && value > signal[i + 1] {
if value >= actual_threshold {
peaks.push((i, value));
}
}
}
if signal.len() >= 2 {
if signal[0] > signal[1] && signal[0] >= actual_threshold {
peaks.push((0, signal[0]));
}
let last_idx = signal.len() - 1;
if signal[last_idx] > signal[last_idx - 1] && signal[last_idx] >= actual_threshold {
peaks.push((last_idx, signal[last_idx]));
}
}
if min_distance > 0 && peaks.len() > 1 {
peaks.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
let mut filtered_peaks = Vec::new();
for (idx, value) in peaks {
let too_close = filtered_peaks.iter().any(|(existing_idx, _)| {
(idx as i32 - *existing_idx as i32).abs() < min_distance as i32
});
if !too_close {
filtered_peaks.push((idx, value));
}
}
peaks = filtered_peaks;
}
peaks.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
log::debug!("Found {} peaks", peaks.len());
peaks
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_peaks_basic() {
let signal = vec![0.0, 0.5, 1.0, 0.7, 0.3, 0.9, 0.2];
let peaks = find_peaks(&signal, 0.5, 2);
assert!(!peaks.is_empty());
assert!(peaks.iter().any(|(idx, _)| *idx == 2));
assert!(peaks.iter().any(|(idx, _)| *idx == 5));
}
#[test]
fn test_find_peaks_empty() {
let signal = vec![];
let peaks = find_peaks(&signal, 0.5, 2);
assert!(peaks.is_empty());
}
#[test]
fn test_find_peaks_too_short() {
let signal = vec![1.0, 2.0];
let peaks = find_peaks(&signal, 0.5, 2);
assert!(peaks.is_empty());
}
#[test]
fn test_find_peaks_threshold() {
let signal = vec![0.1, 0.2, 0.3, 0.4, 0.3, 0.2, 0.1];
let peaks = find_peaks(&signal, 0.5, 1);
assert!(!peaks.is_empty());
let peaks = find_peaks(&signal, 0.5, 1);
assert!(!peaks.is_empty());
}
#[test]
fn test_find_peaks_min_distance() {
let signal = vec![0.0, 0.5, 1.0, 0.8, 0.9, 0.3, 0.1];
let peaks = find_peaks(&signal, 0.3, 3);
assert!(!peaks.is_empty());
}
#[test]
fn test_find_peaks_edge_cases() {
let signal = vec![1.0, 0.5, 0.3];
let peaks = find_peaks(&signal, 0.5, 1);
assert!(peaks.iter().any(|(idx, _)| *idx == 0));
let signal = vec![0.3, 0.5, 1.0];
let peaks = find_peaks(&signal, 0.5, 1);
assert!(peaks.iter().any(|(idx, _)| *idx == 2));
}
#[test]
fn test_find_peaks_all_below_threshold() {
let signal = vec![0.1, 0.2, 0.3, 0.2, 0.1];
let peaks = find_peaks(&signal, 1.0, 1); assert!(peaks.is_empty());
}
#[test]
fn test_find_peaks_sorted() {
let signal = vec![0.0, 0.5, 1.0, 0.7, 0.3, 0.9, 0.2];
let peaks = find_peaks(&signal, 0.3, 1);
for i in 1..peaks.len() {
assert!(
peaks[i - 1].1 >= peaks[i].1,
"Peaks should be sorted by value"
);
}
}
}