use scirs2_core::ndarray::{Array, Dimension, IntoDimension};
use scirs2_core::numeric::{Float, FromPrimitive, NumAssign};
use std::fmt::Debug;
use crate::error::{NdimageError, NdimageResult};
#[allow(dead_code)]
fn generate_offsets(offsets: &mut Vec<Vec<isize>>, sizes: &[usize], current: &[isize], dim: usize) {
if dim == sizes.len() {
offsets.push(current.to_vec());
return;
}
let radius = (sizes[dim] / 2) as isize;
for offset in -radius..=radius {
let mut next = current.to_vec();
next.push(offset);
generate_offsets(offsets, sizes, &next, dim + 1);
}
}
#[allow(dead_code)]
pub fn extrema<T, D>(input: &Array<T, D>) -> NdimageResult<(T, T, Vec<usize>, Vec<usize>)>
where
T: Float + FromPrimitive + Debug + NumAssign + PartialOrd + std::ops::DivAssign + 'static,
D: Dimension + 'static,
{
if input.ndim() == 0 {
return Err(NdimageError::InvalidInput(
"Input array cannot be 0-dimensional".into(),
));
}
if input.is_empty() {
return Err(NdimageError::InvalidInput("Input array is empty".into()));
}
let input_dyn = input.clone().into_dyn();
let mut min_val = None;
let mut max_val = None;
let mut min_loc = vec![0; input.ndim()];
let mut max_loc = vec![0; input.ndim()];
for (idx, &value) in input_dyn.indexed_iter() {
let idx_vec: Vec<usize> = idx.as_array_view().to_vec();
match min_val {
None => {
min_val = Some(value);
max_val = Some(value);
min_loc = idx_vec.clone();
max_loc = idx_vec;
}
Some(current_min) => {
if value < current_min {
min_val = Some(value);
min_loc = idx_vec.clone();
}
if let Some(current_max) = max_val {
if value > current_max {
max_val = Some(value);
max_loc = idx_vec;
}
}
}
}
}
match (min_val, max_val) {
(Some(min), Some(max)) => Ok((min, max, min_loc, max_loc)),
_ => {
let origin = vec![0; input.ndim()];
Ok((T::zero(), T::zero(), origin.clone(), origin))
}
}
}
#[allow(dead_code)]
pub fn local_extrema<T, D>(
input: &Array<T, D>,
size: Option<&[usize]>,
mode: Option<&str>,
) -> NdimageResult<(Array<bool, D>, Array<bool, D>)>
where
T: Float + FromPrimitive + Debug + NumAssign + PartialOrd + std::ops::DivAssign + 'static,
D: Dimension + 'static,
for<'a> &'a [usize]: scirs2_core::ndarray::NdIndex<D>,
{
if input.ndim() == 0 {
return Err(NdimageError::InvalidInput(
"Input array cannot be 0-dimensional".into(),
));
}
if input.is_empty() {
return Err(NdimageError::InvalidInput("Input array is empty".into()));
}
if let Some(s) = size {
if s.len() != input.ndim() {
return Err(NdimageError::DimensionError(format!(
"Size must have same length as input dimensions (got {} expected {})",
s.len(),
input.ndim()
)));
}
for &val in s {
if val == 0 || val % 2 == 0 {
return Err(NdimageError::InvalidInput(
"Size values must be positive odd integers".into(),
));
}
}
}
let m = mode.unwrap_or("both");
if m != "min" && m != "max" && m != "both" {
return Err(NdimageError::InvalidInput(format!(
"Mode must be 'min', 'max', or 'both', got '{}'",
m
)));
}
let default_size: Vec<usize> = vec![3; input.ndim()];
let neighborhood_size = size.unwrap_or(&default_size);
let mut minima = Array::<bool, D>::from_elem(input.raw_dim(), false);
let mut maxima = Array::<bool, D>::from_elem(input.raw_dim(), false);
let mut offsets = Vec::new();
generate_offsets(&mut offsets, neighborhood_size, &vec![], 0);
let shape = input.shape().to_vec();
let ndim = shape.len();
let total_elements: usize = shape.iter().product();
let mut strides = vec![1; ndim];
for d in (0..ndim - 1).rev() {
strides[d] = strides[d + 1] * shape[d + 1];
}
for flat_idx in 0..total_elements {
let mut center_idx = vec![0; ndim];
let mut remaining = flat_idx;
for d in 0..ndim {
center_idx[d] = remaining / strides[d];
remaining %= strides[d];
}
let center_value = input[&*center_idx];
let mut is_min = true;
let mut is_max = true;
let mut has_neighbors = false;
for offset in &offsets {
if offset.iter().all(|&x| x == 0) {
continue;
}
let mut neighbor_idx = Vec::new();
let mut valid_neighbor = true;
for (i, (¢er_coord, &offset_val)) in
center_idx.iter().zip(offset.iter()).enumerate()
{
let coord = center_coord as isize + offset_val;
if coord < 0 || coord >= shape[i] as isize {
valid_neighbor = false;
break;
}
neighbor_idx.push(coord as usize);
}
if !valid_neighbor {
continue;
}
let neighbor_value = input[&*neighbor_idx];
has_neighbors = true;
if neighbor_value <= center_value {
is_min = false;
}
if neighbor_value >= center_value {
is_max = false;
}
if !is_min && !is_max {
break;
}
}
if has_neighbors {
match m {
"min" => {
minima[&*center_idx] = is_min;
}
"max" => {
maxima[&*center_idx] = is_max;
}
"both" => {
minima[&*center_idx] = is_min;
maxima[&*center_idx] = is_max;
}
_ => unreachable!(), }
}
}
Ok((minima, maxima))
}
#[allow(dead_code)]
pub fn peak_prominences<T>(
input: &Array<T, scirs2_core::ndarray::Ix1>,
peaks: &[usize],
wlen: Option<usize>,
) -> NdimageResult<Vec<T>>
where
T: Float + FromPrimitive + Debug + NumAssign,
{
if input.is_empty() {
return Err(NdimageError::InvalidInput("Input array is empty".into()));
}
if peaks.is_empty() {
return Ok(Vec::new());
}
for &p in peaks {
if p >= input.len() {
return Err(NdimageError::InvalidInput(format!(
"Peak index {} is out of bounds for array of length {}",
p,
input.len()
)));
}
}
let (prominences, _left_bases, _right_bases) =
compute_prominences_and_bases(input, peaks, wlen);
Ok(prominences)
}
#[allow(dead_code)]
fn compute_prominences_and_bases<T>(
input: &Array<T, scirs2_core::ndarray::Ix1>,
peaks: &[usize],
wlen: Option<usize>,
) -> (Vec<T>, Vec<usize>, Vec<usize>)
where
T: Float + FromPrimitive + Debug + NumAssign,
{
let n = input.len();
let mut prominences = Vec::with_capacity(peaks.len());
let mut left_bases = Vec::with_capacity(peaks.len());
let mut right_bases = Vec::with_capacity(peaks.len());
for &peak in peaks {
let peak_height = input[peak];
let (i_min, i_max) = match wlen {
Some(w) if w >= 2 => {
let half = w / 2;
(peak.saturating_sub(half), (peak + half).min(n - 1))
}
_ => (0, n - 1),
};
let i_min = i_min as isize;
let i_max = i_max as isize;
let peak_idx = peak as isize;
let mut left_base = peak;
let mut left_min = peak_height;
let mut i = peak_idx;
while i >= i_min && input[i as usize] <= peak_height {
if input[i as usize] < left_min {
left_min = input[i as usize];
left_base = i as usize;
}
i -= 1;
}
let mut right_base = peak;
let mut right_min = peak_height;
let mut i = peak_idx;
while i <= i_max && input[i as usize] <= peak_height {
if input[i as usize] < right_min {
right_min = input[i as usize];
right_base = i as usize;
}
i += 1;
}
let reference = if left_min > right_min {
left_min
} else {
right_min
};
prominences.push(peak_height - reference);
left_bases.push(left_base);
right_bases.push(right_base);
}
(prominences, left_bases, right_bases)
}
pub type PeakWidthsResult<T> = (Vec<T>, Vec<T>, Vec<T>, Vec<T>);
#[allow(dead_code)]
pub fn peak_widths<T>(
input: &Array<T, scirs2_core::ndarray::Ix1>,
peaks: &[usize],
rel_height: Option<T>,
) -> NdimageResult<PeakWidthsResult<T>>
where
T: Float + FromPrimitive + Debug + NumAssign,
{
if input.is_empty() {
return Err(NdimageError::InvalidInput("Input array is empty".into()));
}
if peaks.is_empty() {
return Ok((Vec::new(), Vec::new(), Vec::new(), Vec::new()));
}
for &p in peaks {
if p >= input.len() {
return Err(NdimageError::InvalidInput(format!(
"Peak index {} is out of bounds for array of length {}",
p,
input.len()
)));
}
}
let rel_height = rel_height.unwrap_or_else(|| T::from_f64(0.5).expect("Operation failed"));
if rel_height <= T::zero() || rel_height >= T::one() {
return Err(NdimageError::InvalidInput(format!(
"rel_height must be between 0 and 1, got {:?}",
rel_height
)));
}
let (prominences, left_bases, right_bases) = compute_prominences_and_bases(input, peaks, None);
let mut widths = Vec::with_capacity(peaks.len());
let mut width_heights = Vec::with_capacity(peaks.len());
let mut left_ips = Vec::with_capacity(peaks.len());
let mut right_ips = Vec::with_capacity(peaks.len());
for (idx, &peak) in peaks.iter().enumerate() {
let i_min = left_bases[idx];
let i_max = right_bases[idx];
let height = input[peak] - prominences[idx] * rel_height;
let mut i = peak;
while i > i_min && height < input[i] {
i -= 1;
}
let mut left_ip = T::from_usize(i).expect("Operation failed");
if input[i] < height {
if let Some(&prev) = input.get(i + 1) {
let denom = prev - input[i];
if denom != T::zero() {
left_ip += (height - input[i]) / denom;
}
}
}
let mut i = peak;
while i < i_max && height < input[i] {
i += 1;
}
let mut right_ip = T::from_usize(i).expect("Operation failed");
if input[i] < height {
if let Some(j) = i.checked_sub(1) {
if let Some(&prev) = input.get(j) {
let denom = prev - input[i];
if denom != T::zero() {
right_ip -= (height - input[i]) / denom;
}
}
}
}
widths.push(right_ip - left_ip);
width_heights.push(height);
left_ips.push(left_ip);
right_ips.push(right_ip);
}
Ok((widths, width_heights, left_ips, right_ips))
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::{Array1, Array2};
#[test]
fn test_extrema() {
let input: Array2<f64> = Array2::eye(3);
let (min, max, min_loc, max_loc) = extrema(&input).expect("Operation failed");
assert!(max >= min);
assert_eq!(min_loc.len(), input.ndim());
assert_eq!(max_loc.len(), input.ndim());
}
#[test]
fn test_local_extrema() {
let input_2d: Array2<f64> = Array2::eye(3);
let input = input_2d.into_dyn();
assert_eq!(input.ndim(), 2);
let (minima, maxima) = local_extrema(&input, None, None).expect("Operation failed");
assert_eq!(minima.shape(), input.shape());
assert_eq!(maxima.shape(), input.shape());
let mut data_2d = Array2::<f64>::zeros((5, 5));
data_2d[[2, 2]] = 10.0; let data = data_2d.into_dyn();
let (minima, maxima) = local_extrema(&data, None, None).expect("Operation failed");
assert!(maxima[vec![2, 2].as_slice()]); }
#[test]
fn test_peak_prominences_real_values() {
let signal = Array1::from_vec(vec![0.0, 1.0, 0.5, 3.0, 0.2, 2.0, 0.1]);
let peaks = vec![1, 3, 5];
let prominences = peak_prominences(&signal, &peaks, None).expect("Operation failed");
assert_eq!(prominences.len(), 3);
assert!((prominences[0] - 0.5).abs() < 1e-10, "{:?}", prominences);
assert!((prominences[1] - 2.9).abs() < 1e-10, "{:?}", prominences);
assert!((prominences[2] - 1.8).abs() < 1e-10, "{:?}", prominences);
assert!(prominences[1] > prominences[0]);
assert!(prominences[1] > prominences[2]);
}
#[test]
fn test_peak_prominences_out_of_bounds() {
let signal = Array1::from_vec(vec![0.0, 1.0, 0.0]);
let result = peak_prominences(&signal, &[5], None);
assert!(result.is_err());
}
#[test]
fn test_peak_prominences_wlen_restricts_search() {
let signal = Array1::from_vec(vec![-10.0, 1.0, 2.0, 5.0, 2.0, 1.0, -10.0]);
let peaks = vec![3];
let unrestricted = peak_prominences(&signal, &peaks, None).expect("Operation failed");
assert!((unrestricted[0] - 15.0).abs() < 1e-10, "{unrestricted:?}");
let restricted = peak_prominences(&signal, &peaks, Some(3)).expect("Operation failed");
assert!((restricted[0] - 3.0).abs() < 1e-10, "{restricted:?}");
}
#[test]
fn test_peak_widths_real_values() {
let signal = Array1::from_vec(vec![0.0, 1.0, 2.0, 3.0, 2.0, 1.0, 0.0]);
let peaks = vec![3];
let (widths_25, heights_25, left_25, right_25) =
peak_widths(&signal, &peaks, Some(0.25)).expect("Operation failed");
let (widths_75, heights_75, left_75, right_75) =
peak_widths(&signal, &peaks, Some(0.75)).expect("Operation failed");
assert!((heights_25[0] - 2.25).abs() < 1e-10);
assert!((left_25[0] - 2.25).abs() < 1e-10);
assert!((right_25[0] - 3.75).abs() < 1e-10);
assert!((widths_25[0] - 1.5).abs() < 1e-10, "{:?}", widths_25);
assert!((heights_75[0] - 0.75).abs() < 1e-10);
assert!((left_75[0] - 0.75).abs() < 1e-10);
assert!((right_75[0] - 5.25).abs() < 1e-10);
assert!((widths_75[0] - 4.5).abs() < 1e-10, "{:?}", widths_75);
assert!(widths_25[0] < widths_75[0]);
}
#[test]
fn test_peak_widths_rejects_bad_rel_height() {
let signal = Array1::from_vec(vec![0.0, 1.0, 0.0]);
assert!(peak_widths(&signal, &[1], Some(0.0)).is_err());
assert!(peak_widths(&signal, &[1], Some(1.0)).is_err());
}
}