#![allow(clippy::indexing_slicing)]
use crate::curves::{Curve, Point2D};
use crate::geometrics::GeometricObject;
use rust_decimal::Decimal;
use std::collections::BTreeSet;
use tracing::warn;
#[must_use]
pub fn create_linear_curve(start: Decimal, end: Decimal, slope: Decimal) -> Curve {
let steps = 10;
let step_size = (end - start) / Decimal::from(steps);
let points: Vec<Point2D> = (0..=steps)
.map(|i| {
let x = start + step_size * Decimal::from(i);
let y = slope * x;
Point2D::new(x, y)
})
.collect();
Curve::from_vector(points.iter().collect())
}
#[must_use]
pub fn create_constant_curve(start: Decimal, end: Decimal, value: Decimal) -> Curve {
let steps = 10;
let step_size = (end - start) / Decimal::from(steps);
let point_values: Vec<Point2D> = (0..=steps)
.map(|i| {
let x = start + step_size * Decimal::from(i);
Point2D::new(x, value)
})
.collect();
let points: Vec<&Point2D> = point_values.iter().collect();
Curve::from_vector(points)
}
pub fn detect_peaks_and_valleys(
points: &BTreeSet<Point2D>,
min_prominence: Decimal,
window_size: usize,
) -> (Vec<Point2D>, Vec<Point2D>) {
let points_vec: Vec<Point2D> = points.iter().cloned().collect();
let mut peaks = Vec::new();
let mut valleys = Vec::new();
if points_vec.len() < 2 * window_size + 1 {
warn!(
"Not enough points to detect peaks and valleys with window size {}. Need at least {} points, but got {}.",
window_size,
2 * window_size + 1,
points_vec.len()
);
return (peaks, valleys);
}
for i in window_size..points_vec.len() - window_size {
let current = &points_vec[i];
let mut is_peak = true;
let mut is_valley = true;
for j in 1..=window_size {
let before = &points_vec[i - j];
let after = &points_vec[i + j];
if current.y <= before.y || current.y <= after.y {
is_peak = false;
}
if current.y >= before.y || current.y >= after.y {
is_valley = false;
}
if !is_peak && !is_valley {
break;
}
}
if is_peak {
let prominence = calculate_prominence(&points_vec, i, true);
if prominence >= min_prominence {
peaks.push(*current);
}
} else if is_valley {
let prominence = calculate_prominence(&points_vec, i, false);
if prominence >= min_prominence {
valleys.push(*current);
}
}
}
(peaks, valleys)
}
fn calculate_prominence(points: &[Point2D], index: usize, is_peak: bool) -> Decimal {
let current = points[index].y;
let left_bound = if is_peak {
points
.iter()
.take(index)
.map(|p| p.y)
.min()
.unwrap_or(Decimal::MAX)
} else {
points
.iter()
.take(index)
.map(|p| p.y)
.max()
.unwrap_or(Decimal::MIN)
};
let right_bound = if is_peak {
points
.iter()
.skip(index + 1)
.map(|p| p.y)
.min()
.unwrap_or(Decimal::MAX)
} else {
points
.iter()
.skip(index + 1)
.map(|p| p.y)
.max()
.unwrap_or(Decimal::MIN)
};
if is_peak {
current - Decimal::max(left_bound, right_bound)
} else {
Decimal::min(left_bound, right_bound) - current
}
}
#[cfg(test)]
mod tests_utils {
use crate::curves::Point2D;
use crate::curves::utils::{calculate_prominence, detect_peaks_and_valleys};
use rust_decimal_macros::dec;
use std::collections::BTreeSet;
#[test]
fn test_detect_peaks_and_valleys_insufficient_points() {
let points = BTreeSet::from_iter(vec![
Point2D::new(dec!(1.0), dec!(2.0)),
Point2D::new(dec!(2.0), dec!(3.0)),
]);
let (peaks, valleys) = detect_peaks_and_valleys(&points, dec!(0.1), 1);
assert!(peaks.is_empty());
assert!(valleys.is_empty());
}
#[test]
fn test_calculate_prominence() {
let points = vec![
Point2D::new(dec!(0.0), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(2.0)), Point2D::new(dec!(2.0), dec!(1.0)),
Point2D::new(dec!(3.0), dec!(-1.0)), Point2D::new(dec!(4.0), dec!(0.0)),
];
let peak_prominence = calculate_prominence(&points, 1, true);
assert_eq!(peak_prominence, dec!(2.0));
let valley_prominence = calculate_prominence(&points, 3, false);
assert_eq!(valley_prominence, dec!(1.0));
}
#[test]
fn test_detect_peaks_and_valleys_with_prominence() {
let points = BTreeSet::from_iter(vec![
Point2D::new(dec!(0.0), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(3.0)),
Point2D::new(dec!(2.0), dec!(-2.0)),
Point2D::new(dec!(3.0), dec!(2.0)),
Point2D::new(dec!(4.0), dec!(-1.0)),
Point2D::new(dec!(5.0), dec!(0.0)),
]);
let (peaks, valleys) = detect_peaks_and_valleys(&points, dec!(0.1), 1);
assert_eq!(peaks.len(), 2);
assert_eq!(valleys.len(), 2);
let (peaks, valleys) = detect_peaks_and_valleys(&points, dec!(4.0), 1);
assert!(peaks.is_empty());
assert!(!valleys.is_empty());
let (peaks, valleys) = detect_peaks_and_valleys(&points, dec!(2.0), 1);
assert_eq!(peaks.len(), 2);
assert_eq!(valleys.len(), 1);
assert_eq!(peaks[0].y, dec!(3.0));
assert_eq!(valleys[0].y, dec!(-2.0));
}
}