1use num_traits::Float;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
7pub enum Polarity {
8 #[default]
10 Positive,
11 Negative,
13 Both,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub enum DistanceRule {
20 #[default]
24 Scipy,
25 LocallyExclusive,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq)]
33pub struct Interval<T> {
34 pub min: Option<T>,
35 pub max: Option<T>,
36}
37
38impl<T> Default for Interval<T> {
39 fn default() -> Self {
40 Self { min: None, max: None }
41 }
42}
43
44impl<T: Float> Interval<T> {
45 pub fn at_least(min: T) -> Self {
47 Self { min: Some(min), max: None }
48 }
49
50 fn is_set(&self) -> bool {
51 self.min.is_some() || self.max.is_some()
52 }
53
54 fn contains(&self, v: T) -> bool {
55 self.min.is_none_or(|m| m <= v) && self.max.is_none_or(|m| v <= m)
56 }
57}
58
59pub const DEFAULT_REL_HEIGHT: f64 = 0.5;
61
62#[derive(Debug, Clone, Copy, PartialEq)]
65pub struct PeakOptions<T> {
66 pub height: Interval<T>,
68 pub threshold: Interval<T>,
71 pub distance: Option<usize>,
73 pub distance_rule: DistanceRule,
74 pub prominence: Interval<T>,
75 pub width: Interval<T>,
77 pub wlen: Option<usize>,
79 pub rel_height: T,
80}
81
82impl<T: Float> Default for PeakOptions<T> {
83 fn default() -> Self {
84 Self {
85 height: Interval::default(),
86 threshold: Interval::default(),
87 distance: None,
88 distance_rule: DistanceRule::default(),
89 prominence: Interval::default(),
90 width: Interval::default(),
91 wlen: None,
92 rel_height: T::from(DEFAULT_REL_HEIGHT).expect("0.5 is representable"),
93 }
94 }
95}
96
97#[derive(Debug, Clone, PartialEq)]
99pub struct Peaks<T> {
100 pub indices: Vec<usize>,
102 pub signs: Vec<i8>,
104 pub prominences: Option<Vec<T>>,
106 pub left_bases: Option<Vec<usize>>,
107 pub right_bases: Option<Vec<usize>>,
108 pub widths: Option<Vec<T>>,
110 pub left_ips: Option<Vec<T>>,
111 pub right_ips: Option<Vec<T>>,
112}
113
114impl<T> Default for Peaks<T> {
115 fn default() -> Self {
116 Self {
117 indices: Vec::new(),
118 signs: Vec::new(),
119 prominences: None,
120 left_bases: None,
121 right_bases: None,
122 widths: None,
123 left_ips: None,
124 right_ips: None,
125 }
126 }
127}
128
129pub fn local_extrema<T: Float>(x: &[T], polarity: Polarity) -> Vec<(usize, i8)> {
132 let mut out: Vec<(usize, i8)> = Vec::new();
133 for (sign, s) in signs_of(polarity) {
134 out.extend(local_maxima(x, s).into_iter().map(|p| (p, sign)));
135 }
136 out.sort_by_key(|&(p, _)| p);
137 out
138}
139
140fn signs_of<T: Float>(polarity: Polarity) -> Vec<(i8, T)> {
141 match polarity {
142 Polarity::Positive => vec![(1, T::one())],
143 Polarity::Negative => vec![(-1, -T::one())],
144 Polarity::Both => vec![(1, T::one()), (-1, -T::one())],
145 }
146}
147
148fn local_maxima<T: Float>(x: &[T], sign: T) -> Vec<usize> {
149 let s = |i: usize| sign * x[i];
150 let mut out = Vec::new();
151 let n = x.len();
152 if n < 3 {
153 return out;
154 }
155 let i_max = n - 1;
156 let mut i = 1;
157 while i < i_max {
158 if s(i - 1) < s(i) {
159 let mut ahead = i + 1;
160 while ahead < i_max && s(ahead) == s(i) {
161 ahead += 1;
162 }
163 if s(ahead) < s(i) {
164 out.push((i + ahead - 1) / 2);
165 i = ahead;
166 }
167 }
168 i += 1;
169 }
170 out
171}
172
173pub fn select_by_distance<T: Float>(peaks: &[usize], priority: &[T], distance: usize, rule: DistanceRule) -> Vec<bool> {
177 let n = peaks.len();
178 let beats = |a: usize, b: usize| priority[a] > priority[b] || (priority[a] == priority[b] && a < b);
180 if rule == DistanceRule::LocallyExclusive {
181 return (0..n)
182 .map(|j| {
183 let left = (0..j).rev().take_while(|&k| peaks[j] - peaks[k] < distance);
184 let right = (j + 1..n).take_while(|&k| peaks[k] - peaks[j] < distance);
185 !left.chain(right).any(|k| beats(k, j))
186 })
187 .collect();
188 }
189 let mut keep = vec![true; n];
190 let mut order: Vec<usize> = (0..n).collect();
191 order.sort_by(|&a, &b| if beats(a, b) { std::cmp::Ordering::Less } else if beats(b, a) { std::cmp::Ordering::Greater } else { std::cmp::Ordering::Equal });
192 for j in order {
193 if !keep[j] {
194 continue;
195 }
196 let mut k = j;
197 while k > 0 && peaks[j] - peaks[k - 1] < distance {
198 keep[k - 1] = false;
199 k -= 1;
200 }
201 let mut k = j + 1;
202 while k < n && peaks[k] - peaks[j] < distance {
203 keep[k] = false;
204 k += 1;
205 }
206 }
207 keep
208}
209
210fn prominence<T: Float>(s: &dyn Fn(usize) -> T, n: usize, p: usize, wlen: Option<usize>) -> (T, usize, usize) {
212 let (mut i_min, mut i_max) = (0, n - 1);
213 if let Some(w) = wlen.filter(|&w| w >= 2) {
214 i_min = p.saturating_sub(w / 2);
215 i_max = (p + w / 2).min(n - 1);
216 }
217 let (mut left_base, mut left_min) = (p, s(p));
218 let mut i = p as isize;
219 while i >= i_min as isize && s(i as usize) <= s(p) {
220 if s(i as usize) < left_min {
221 left_min = s(i as usize);
222 left_base = i as usize;
223 }
224 i -= 1;
225 }
226 let (mut right_base, mut right_min) = (p, s(p));
227 let mut i = p;
228 while i <= i_max && s(i) <= s(p) {
229 if s(i) < right_min {
230 right_min = s(i);
231 right_base = i;
232 }
233 i += 1;
234 }
235 (s(p) - left_min.max(right_min), left_base, right_base)
236}
237
238fn width<T: Float>(s: &dyn Fn(usize) -> T, p: usize, prom: T, left_base: usize, right_base: usize, rel_height: T) -> (T, T, T) {
241 let height = s(p) - prom * rel_height;
242 let mut i = p;
243 while left_base < i && height < s(i) {
244 i -= 1;
245 }
246 let mut left = T::from(i).expect("index");
247 if s(i) < height {
248 left = left + (height - s(i)) / (s(i + 1) - s(i));
249 }
250 let mut i = p;
251 while i < right_base && height < s(i) {
252 i += 1;
253 }
254 let mut right = T::from(i).expect("index");
255 if s(i) < height {
256 right = right - (height - s(i)) / (s(i - 1) - s(i));
257 }
258 (right - left, left, right)
259}
260
261pub fn find_peaks<T: Float>(x: &[T], polarity: Polarity, options: &PeakOptions<T>) -> Peaks<T> {
266 let n = x.len();
267 let mut cand: Vec<(usize, T)> = Vec::new();
269 for (_, sign) in signs_of::<T>(polarity) {
270 for p in local_maxima(x, sign) {
271 let v = sign * x[p];
272 if !options.height.contains(v) {
273 continue;
274 }
275 if options.threshold.is_set() {
276 let (l, r) = (v - sign * x[p - 1], v - sign * x[p + 1]);
277 if !(options.threshold.min.is_none_or(|m| m <= l.min(r)) && options.threshold.max.is_none_or(|m| l.max(r) <= m)) {
278 continue;
279 }
280 }
281 cand.push((p, sign));
282 }
283 }
284 cand.sort_by_key(|&(p, _)| p);
285
286 if let Some(d) = options.distance.filter(|&d| d > 1) {
287 let idx: Vec<usize> = cand.iter().map(|&(p, _)| p).collect();
288 let priority: Vec<T> = cand.iter().map(|&(p, s)| s * x[p]).collect();
289 let keep = select_by_distance(&idx, &priority, d, options.distance_rule);
290 cand = cand.into_iter().zip(keep).filter_map(|(c, k)| k.then_some(c)).collect();
291 }
292
293 let mut out = Peaks::default();
294 let need_prominence = options.prominence.is_set() || options.width.is_set();
295 let mut props: Vec<(T, usize, usize, T, T, T)> = Vec::new();
296 let mut kept: Vec<(usize, T)> = Vec::new();
297 for (p, sign) in cand {
298 let s = |i: usize| sign * x[i];
299 let (mut prom, mut lb, mut rb) = (T::zero(), p, p);
300 if need_prominence {
301 (prom, lb, rb) = prominence(&s, n, p, options.wlen);
302 if !options.prominence.contains(prom) {
303 continue;
304 }
305 }
306 let (mut w, mut li, mut ri) = (T::zero(), T::zero(), T::zero());
307 if options.width.is_set() {
308 (w, li, ri) = width(&s, p, prom, lb, rb, options.rel_height);
309 if !options.width.contains(w) {
310 continue;
311 }
312 }
313 kept.push((p, sign));
314 props.push((prom, lb, rb, w, li, ri));
315 }
316 out.indices = kept.iter().map(|&(p, _)| p).collect();
317 out.signs = kept.iter().map(|&(_, s)| if s > T::zero() { 1 } else { -1 }).collect();
318 if need_prominence {
319 out.prominences = Some(props.iter().map(|p| p.0).collect());
320 out.left_bases = Some(props.iter().map(|p| p.1).collect());
321 out.right_bases = Some(props.iter().map(|p| p.2).collect());
322 }
323 if options.width.is_set() {
324 out.widths = Some(props.iter().map(|p| p.3).collect());
325 out.left_ips = Some(props.iter().map(|p| p.4).collect());
326 out.right_ips = Some(props.iter().map(|p| p.5).collect());
327 }
328 out
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334
335 fn opts() -> PeakOptions<f64> {
336 PeakOptions::default()
337 }
338
339 #[test]
340 fn local_maxima_and_plateaus() {
341 let x = [0.0, 2.0, 0.0, 3.0, 3.0, 3.0, 0.0, 1.0, 1.0, 0.0];
343 assert_eq!(find_peaks(&x, Polarity::Positive, &opts()).indices, vec![1, 4, 7]);
344 assert!(find_peaks(&[5.0, 1.0, 5.0], Polarity::Positive, &opts()).indices.is_empty());
346 }
347
348 #[test]
349 fn height_threshold_distance() {
350 let x = [0.0, 5.0, 0.0, 3.0, 0.0, 4.0, 3.5, 0.0];
351 let o = PeakOptions { height: Interval::at_least(3.5), ..opts() };
352 assert_eq!(find_peaks(&x, Polarity::Positive, &o).indices, vec![1, 5]);
353 let o = PeakOptions { threshold: Interval::at_least(1.0), ..opts() };
354 assert_eq!(find_peaks(&x, Polarity::Positive, &o).indices, vec![1, 3], "peak 5 is only 0.5 above sample 6");
355 let o = PeakOptions { distance: Some(3), ..opts() };
357 assert_eq!(find_peaks(&x, Polarity::Positive, &o).indices, vec![1, 5]);
358 }
359
360 #[test]
361 fn distance_rules_differ_on_chains() {
362 let (peaks, priority) = ([0usize, 2, 4], [10.0, 9.0, 8.0]);
365 assert_eq!(select_by_distance(&peaks, &priority, 3, DistanceRule::Scipy), vec![true, false, true]);
366 assert_eq!(select_by_distance(&peaks, &priority, 3, DistanceRule::LocallyExclusive), vec![true, false, false]);
367 for rule in [DistanceRule::Scipy, DistanceRule::LocallyExclusive] {
369 assert_eq!(select_by_distance(&[5usize, 6], &[1.0, 1.0], 2, rule), vec![true, false]);
370 }
371 }
372
373 #[test]
374 fn prominence_and_width_match_scipy() {
375 let x = [0.0, 1.0, 0.0, 4.0, 1.0, 2.0, 0.0];
378 let o = PeakOptions { prominence: Interval::at_least(0.0), width: Interval::at_least(0.0), ..opts() };
379 let p = find_peaks(&x, Polarity::Positive, &o);
380 assert_eq!(p.indices, vec![1, 3, 5]);
381 assert_eq!(p.prominences.unwrap(), vec![1.0, 4.0, 1.0]);
382 assert_eq!(p.left_bases.unwrap(), vec![0, 2, 4]);
383 assert_eq!(p.right_bases.unwrap(), vec![2, 6, 6]);
384 let w = p.widths.unwrap();
386 assert!((w[1] - 7.0 / 6.0).abs() < 1e-12, "{w:?}");
387 }
388
389 #[test]
390 fn troughs_and_both_polarities() {
391 let x = [0.0, -5.0, 0.0, 4.0, 0.0, -1.0, 0.0];
392 let neg = find_peaks(&x, Polarity::Negative, &PeakOptions { height: Interval::at_least(2.0), ..opts() });
393 assert_eq!((neg.indices, neg.signs), (vec![1], vec![-1]));
394 let both = find_peaks(&x, Polarity::Both, &PeakOptions { height: Interval::at_least(2.0), ..opts() });
395 assert_eq!((both.indices, both.signs), (vec![1, 3], vec![-1, 1]));
396 let o = PeakOptions { height: Interval::at_least(2.0), distance: Some(3), ..opts() };
398 assert_eq!(find_peaks(&x, Polarity::Both, &o).indices, vec![1]);
399 }
400}