use crate::SpikefitSample;
use crate::err::SpikefitError;
use crate::prominence::peak_prominences_impl;
use crate::widths::peak_widths_impl;
use num_traits::AsPrimitive;
pub(crate) fn find_peaks_impl<T: SpikefitSample>(
x: &[T],
height: Option<f64>,
threshold: Option<f64>,
distance: Option<usize>,
prominence: Option<f64>,
width: Option<f64>,
) -> Result<Vec<usize>, SpikefitError>
where
f64: AsPrimitive<T>,
usize: AsPrimitive<T>,
{
if x.len() < 3 {
return Err(SpikefitError::SignalTooShort { len: 3 }); }
let mut peak_indices = Vec::new();
peak_indices.extend(
x.array_windows::<3>()
.enumerate()
.filter(|(_, w)| w[1] > w[0] && w[1] > w[2])
.map(|(i, _)| i + 1),
);
if x.len() >= 2 && x[x.len() - 1] > x[x.len() - 2] {
peak_indices.push(x.len() - 1);
}
if let Some(h) = height {
let h_f64 = h.as_();
peak_indices.retain(|&idx| x[idx] >= h_f64);
}
if let Some(th) = threshold {
let th_f64 = th.as_();
peak_indices.retain(|&idx| {
if idx == x.len() - 1 {
return idx > 0 && x[idx] - x[idx - 1] >= th_f64;
}
x[idx] - x[idx - 1] >= th_f64 && x[idx] - x[idx + 1] >= th_f64
});
}
if let Some(dist) = distance
&& dist > 0
{
let mut filtered_peaks = Vec::new();
let mut peaks_with_height: Vec<(usize, T)> =
peak_indices.iter().map(|&idx| (idx, x[idx])).collect();
peaks_with_height
.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
let mut excluded = vec![false; x.len()];
for &(idx, _) in &peaks_with_height {
if !excluded[idx] {
filtered_peaks.push(idx);
let start = idx.saturating_sub(dist);
let end = (idx + dist + 1).min(x.len());
for (j, exclude) in excluded.iter_mut().enumerate().take(end).skip(start) {
if j != idx {
*exclude = true;
}
}
}
}
filtered_peaks.sort_unstable();
peak_indices = filtered_peaks;
}
if let Some(prom) = prominence {
let prom_f64 = prom.as_();
let prominences = peak_prominences_impl(x, &peak_indices)?;
let mut filtered_peaks = Vec::new();
for (&prominence, &idx) in prominences.iter().zip(peak_indices.iter()) {
if prominence >= prom_f64 {
filtered_peaks.push(idx);
}
}
peak_indices = filtered_peaks;
}
if let Some(w) = width {
let w_f64 = w.as_();
let widths = peak_widths_impl(x, &peak_indices, None)?;
let mut filtered_peaks = Vec::new();
for (&width, &idx) in widths.widths.iter().zip(peak_indices.iter()) {
if width >= w_f64 {
filtered_peaks.push(idx);
}
}
peak_indices = filtered_peaks;
}
Ok(peak_indices)
}
#[cfg(test)]
mod tests {
use super::*;
fn peaks(x: &[f64]) -> Vec<usize> {
find_peaks_impl(x, None, None, None, None, None).unwrap()
}
#[test]
fn test_single_peak() {
assert_eq!(peaks(&[0.0, 1.0, 0.0]), vec![1]);
}
#[test]
fn test_multiple_peaks() {
assert_eq!(peaks(&[0.0, 2.0, 0.0, 3.0, 0.0, 1.0, 0.0]), vec![1, 3, 5]);
}
#[test]
fn test_no_peaks_flat() {
assert_eq!(peaks(&[1.0, 1.0, 1.0, 1.0]), vec![]);
}
#[test]
fn test_no_peaks_monotone_increasing() {
assert_eq!(peaks(&[1.0, 2.0, 3.0, 4.0]), vec![3]); }
#[test]
fn test_no_peaks_monotone_decreasing() {
assert_eq!(peaks(&[4.0, 3.0, 2.0, 1.0]), vec![]);
}
#[test]
fn test_last_point_peak() {
assert_eq!(peaks(&[0.0, 1.0, 0.5, 2.0]), vec![1, 3]);
}
#[test]
fn test_signal_too_short_returns_error() {
assert!(find_peaks_impl(&[1.0f64, 2.0], None, None, None, None, None).is_err());
assert!(find_peaks_impl(&[1.0f64], None, None, None, None, None).is_err());
assert!(find_peaks_impl(&[] as &[f64], None, None, None, None, None).is_err());
}
#[test]
fn test_exactly_three_points() {
assert_eq!(peaks(&[0.0, 1.0, 0.0]), vec![1]);
assert_eq!(peaks(&[1.0, 0.0, 1.0]), vec![2]);
}
#[test]
fn test_height_filters_low_peaks() {
let x = &[0.0, 1.0, 0.0, 3.0, 0.0, 2.0, 0.0];
let result = find_peaks_impl(x, Some(2.0), None, None, None, None).unwrap();
assert_eq!(result, vec![3, 5]);
}
#[test]
fn test_height_keeps_exact_threshold() {
let x = &[0.0, 2.0, 0.0, 3.0, 0.0];
let result = find_peaks_impl(x, Some(2.0), None, None, None, None).unwrap();
assert_eq!(result, vec![1, 3]);
}
#[test]
fn test_height_filters_all() {
let x = &[0.0, 1.0, 0.0, 2.0, 0.0];
let result = find_peaks_impl(x, Some(5.0), None, None, None, None).unwrap();
assert_eq!(result, vec![]);
}
#[test]
fn test_threshold_basic() {
let x = &[0.0, 1.0, 0.0, 2.0, 0.0];
let result = find_peaks_impl(x, None, Some(1.5), None, None, None).unwrap();
assert_eq!(result, vec![3]);
}
#[test]
fn test_threshold_zero_keeps_all() {
let x = &[0.0, 1.0, 0.0, 2.0, 0.0];
let result = find_peaks_impl(x, None, Some(0.0), None, None, None).unwrap();
assert_eq!(result, vec![1, 3]);
}
#[test]
fn test_distance_keeps_highest() {
let x = &[0.0, 1.0, 0.0, 3.0, 0.0, 2.0, 0.0];
let result = find_peaks_impl(x, None, None, Some(3), None, None).unwrap();
assert_eq!(result, vec![3]);
}
#[test]
fn test_distance_far_enough_keeps_both() {
let x = &[0.0, 2.0, 0.0, 0.0, 0.0, 3.0, 0.0];
let result = find_peaks_impl(x, None, None, Some(3), None, None).unwrap();
assert_eq!(result, vec![1, 5]);
}
#[test]
fn test_distance_zero_keeps_all() {
let x = &[0.0, 1.0, 0.0, 2.0, 0.0];
let result = find_peaks_impl(x, None, None, Some(0), None, None).unwrap();
assert_eq!(result, vec![1, 3]);
}
#[test]
fn test_prominence_filters_shallow_peak() {
let x = &[0.0, 3.0, 4.0, 3.0, 0.0, 5.0, 0.0];
let result = find_peaks_impl(x, None, None, None, Some(3.0), None).unwrap();
assert_eq!(result, vec![2, 5]);
}
#[test]
fn test_prominence_zero_keeps_all() {
let x = &[0.0, 1.0, 0.0, 2.0, 0.0];
let result = find_peaks_impl(x, None, None, None, Some(0.0), None).unwrap();
assert_eq!(result, vec![1, 3]);
}
#[test]
fn test_width_filters_narrow_peak() {
let x = &[0.0, 0.0, 3.0, 0.0, 0.0, 1.0, 2.0, 1.0, 0.0];
let result = find_peaks_impl(x, None, None, None, None, Some(2.0)).unwrap();
assert_eq!(result, vec![6]);
}
#[test]
fn test_all_filters_no_peaks_survive() {
let x = &[0.0, 1.0, 0.0, 2.0, 0.0, 1.5, 0.0];
let result =
find_peaks_impl(x, Some(5.0), Some(2.0), Some(4), Some(3.0), Some(3.0)).unwrap();
assert_eq!(result, vec![]);
}
#[test]
fn test_plateau_not_detected_as_peak() {
assert_eq!(peaks(&[0.0, 1.0, 1.0, 0.0]), vec![]);
}
#[test]
fn test_negative_values() {
assert_eq!(peaks(&[-3.0, -1.0, -3.0]), vec![1]);
}
#[test]
fn test_all_same_value() {
assert_eq!(peaks(&[2.0, 2.0, 2.0, 2.0]), vec![]);
}
}