#![allow(clippy::indexing_slicing)]
use crate::curves::{Curve, Point2D};
use crate::error::CurveError;
use crate::geometrics::GeometricObject;
use crate::model::decimal::{d_add, d_div, d_mul, d_sub};
use rust_decimal::Decimal;
use std::collections::BTreeSet;
use tracing::warn;
fn sampling_err(err: crate::error::decimal::DecimalError) -> CurveError {
CurveError::ConstructionError(err.to_string())
}
pub fn create_linear_curve(
start: Decimal,
end: Decimal,
slope: Decimal,
) -> Result<Curve, CurveError> {
let steps = 10;
let op = "create_linear_curve";
let span = d_sub(end, start, op).map_err(sampling_err)?;
let step_size = d_div(span, Decimal::from(steps), op).map_err(sampling_err)?;
let mut points: Vec<Point2D> = Vec::new();
for i in 0..=steps {
let offset = d_mul(step_size, Decimal::from(i), op).map_err(sampling_err)?;
let x = d_add(start, offset, op).map_err(sampling_err)?;
let y = d_mul(slope, x, op).map_err(sampling_err)?;
points.push(Point2D::new(x, y));
}
Ok(Curve::from_vector(points.iter().collect()))
}
pub fn create_constant_curve(
start: Decimal,
end: Decimal,
value: Decimal,
) -> Result<Curve, CurveError> {
let steps = 10;
let op = "create_constant_curve";
let span = d_sub(end, start, op).map_err(sampling_err)?;
let step_size = d_div(span, Decimal::from(steps), op).map_err(sampling_err)?;
let mut point_values: Vec<Point2D> = Vec::new();
for i in 0..=steps {
let offset = d_mul(step_size, Decimal::from(i), op).map_err(sampling_err)?;
let x = d_add(start, offset, op).map_err(sampling_err)?;
point_values.push(Point2D::new(x, value));
}
let points: Vec<&Point2D> = point_values.iter().collect();
Ok(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));
}
}