const AMPD_SCALE_MAX: usize = 21;
const AMPD_THRESHOLD: usize = 15;
const MIN_PROMINENCE: f32 = 20.0; const CLUSTER_WINDOW: usize = 3;
fn ampd_core(signal: &[f32], scale_max: usize, threshold: usize, find_peaks: bool) -> Vec<usize> {
let n = signal.len();
if n < 3 || scale_max == 0 {
return vec![];
}
let effective = scale_max.min(n / 2);
if effective == 0 {
return vec![];
}
let mut counts = vec![0u8; n];
for scale in 1..=effective {
if scale * 2 >= n {
break;
}
for i in scale..(n - scale) {
let is_ext = if find_peaks {
signal[i] > signal[i - scale] && signal[i] > signal[i + scale]
} else {
signal[i] < signal[i - scale] && signal[i] < signal[i + scale]
};
if is_ext {
counts[i] = counts[i].saturating_add(1);
}
}
}
counts
.iter()
.enumerate()
.filter(|&(_, &c)| (c as usize) >= threshold)
.map(|(i, _)| i)
.collect()
}
fn cluster_extrema(raw: &[usize], signal: &[f32], find_peaks: bool) -> Vec<usize> {
if raw.is_empty() {
return vec![];
}
let mut clustered = Vec::new();
let mut best_idx = raw[0];
let mut best_val = signal[raw[0]];
for i in 1..raw.len() {
let curr_idx = raw[i];
let prev_idx = raw[i - 1];
if curr_idx - prev_idx <= CLUSTER_WINDOW {
let curr_val = signal[curr_idx];
let better = if find_peaks {
curr_val > best_val
} else {
curr_val < best_val
};
if better {
best_val = curr_val;
best_idx = curr_idx;
}
} else {
clustered.push(best_idx);
best_idx = curr_idx;
best_val = signal[curr_idx];
}
}
clustered.push(best_idx);
clustered
}
fn extremum_prominence(signal: &[f32], idx: usize, find_peaks: bool) -> f32 {
let v = signal[idx];
let mut left_col = v;
let mut i = idx;
while i > 0 {
i -= 1;
let s = signal[i];
if find_peaks {
if s > v {
break;
}
if s < left_col {
left_col = s;
}
} else {
if s < v {
break;
}
if s > left_col {
left_col = s;
}
}
}
let mut right_col = v;
let mut j = idx;
while j + 1 < signal.len() {
j += 1;
let s = signal[j];
if find_peaks {
if s > v {
break;
}
if s < right_col {
right_col = s;
}
} else {
if s < v {
break;
}
if s > right_col {
right_col = s;
}
}
}
if find_peaks {
v - left_col.max(right_col)
} else {
left_col.min(right_col) - v
}
}
fn filter_by_prominence(
candidates: &[usize],
signal: &[f32],
min_prominence: f32,
find_peaks: bool,
) -> Vec<usize> {
if candidates.is_empty() || min_prominence <= 0.0 {
return candidates.to_vec();
}
candidates
.iter()
.filter(|&&idx| extremum_prominence(signal, idx, find_peaks) >= min_prominence)
.copied()
.collect()
}
pub fn find_peaks(signal: &[f32]) -> Vec<usize> {
if signal.len() < 3 {
return vec![];
}
let raw = ampd_core(signal, AMPD_SCALE_MAX, AMPD_THRESHOLD, true);
let clustered = cluster_extrema(&raw, signal, true);
filter_by_prominence(&clustered, signal, MIN_PROMINENCE, true)
}
pub fn find_valleys(signal: &[f32]) -> Vec<usize> {
if signal.len() < 3 {
return vec![];
}
let raw = ampd_core(signal, AMPD_SCALE_MAX, AMPD_THRESHOLD, false);
let clustered = cluster_extrema(&raw, signal, false);
filter_by_prominence(&clustered, signal, MIN_PROMINENCE, false)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn flat_signal_has_no_peaks() {
let signal = vec![2.0f32; 10];
let peaks = find_peaks(&signal);
assert!(peaks.is_empty());
}
#[test]
fn flat_signal_has_no_valleys() {
let signal = vec![3.0f32; 10];
let valleys = find_valleys(&signal);
assert!(valleys.is_empty());
}
#[test]
fn signal_too_short_returns_empty() {
let signal = vec![1.0f32, 5.0, 2.0];
let peaks = find_peaks(&signal);
assert!(peaks.len() <= 1);
let valleys = find_valleys(&signal);
assert!(valleys.len() <= 1);
}
#[test]
fn prominence_filter_drops_low_prominence() {
let signal = vec![0.0f32, 50.0, 100.0, 60.0, 40.0, 55.0, 70.0, 30.0, 0.0];
let candidates = vec![2usize, 6];
let kept = filter_by_prominence(&candidates, &signal, 35.0, true);
assert_eq!(kept, vec![2]);
}
#[test]
fn prominent_peaks_detected() {
let mut signal = vec![100.0f32; 60];
signal[10] = 180.0;
signal[40] = 170.0;
let peaks = find_peaks(&signal);
assert!(!peaks.is_empty());
}
#[test]
fn peaks_and_valleys_are_symmetric_on_alternating_signal() {
let signal: Vec<f32> = (0..30)
.map(|i| if i % 2 == 0 { 200.0 } else { 100.0 })
.collect();
let raw_peaks = ampd_core(&signal, 2, 1, true);
let raw_valleys = ampd_core(&signal, 2, 1, false);
assert_eq!(raw_peaks.len(), raw_valleys.len());
}
#[test]
fn extremum_prominence_correct_for_dominant_peak() {
let signal = vec![0.0f32, 50.0, 100.0, 60.0, 40.0, 55.0, 70.0, 30.0, 0.0];
let p = extremum_prominence(&signal, 2, true);
assert!((p - 100.0).abs() < 0.001, "prominence={}", p);
}
#[test]
fn extremum_prominence_correct_for_secondary_peak() {
let signal = vec![0.0f32, 50.0, 100.0, 60.0, 40.0, 55.0, 70.0, 30.0, 0.0];
let p = extremum_prominence(&signal, 6, true);
assert!((p - 30.0).abs() < 0.001, "prominence={}", p);
}
}