use ndarray::{Array, Array1, Array2, ArrayBase, Data, Dimension};
use num_traits::{Float, FromPrimitive, Num, NumCast};
use std::fmt::Debug;
pub fn is_close<F: Float>(a: F, b: F, abs_tol: F, rel_tol: F) -> bool {
let abs_diff = (a - b).abs();
if abs_diff <= abs_tol {
return true;
}
let abs_a = a.abs();
let abs_b = b.abs();
let max_abs = if abs_a > abs_b { abs_a } else { abs_b };
abs_diff <= max_abs * rel_tol
}
pub fn points_equal<T>(point1: &[T], point2: &[T], tol: Option<T>) -> bool
where
T: PartialOrd + std::ops::Sub<Output = T> + Copy + FromPrimitive,
{
let tol = match tol {
Some(t) => t,
None => match T::from_f64(1e-8) {
Some(t) => t,
None => panic!("Could not convert 1e-8 to generic type"),
},
};
if point1.len() != point2.len() {
return false;
}
for i in 0..point1.len() {
if point1[i] > point2[i] && point1[i] - point2[i] > tol {
return false;
}
if point2[i] > point1[i] && point2[i] - point1[i] > tol {
return false;
}
}
true
}
pub fn arrays_equal<S1, S2, D, T>(
array1: &ArrayBase<S1, D>,
array2: &ArrayBase<S2, D>,
tol: Option<T>,
) -> bool
where
S1: Data<Elem = T>,
S2: Data<Elem = T>,
D: Dimension,
T: PartialOrd + std::ops::Sub<Output = T> + Copy + FromPrimitive,
{
if array1.shape() != array2.shape() {
return false;
}
let points1: Vec<T> = array1.iter().copied().collect();
let points2: Vec<T> = array2.iter().copied().collect();
points_equal(&points1, &points2, tol)
}
pub fn fill_diagonal<T: Clone>(mut a: Array2<T>, val: T) -> Array2<T> {
let min_dim = a.nrows().min(a.ncols());
for i in 0..min_dim {
a[[i, i]] = val.clone();
}
a
}
pub fn prod<I, T>(iter: I) -> T
where
I: IntoIterator<Item = T>,
T: std::ops::Mul<Output = T> + From<u8>,
{
iter.into_iter().fold(T::from(1), |a, b| a * b)
}
pub fn arange<F: Float + std::iter::Sum>(start: F, stop: F, step: F) -> Vec<F> {
if step == F::zero() {
panic!("Step size cannot be zero");
}
let mut result = Vec::new();
let mut current = start;
if step > F::zero() {
while current < stop {
result.push(current);
current = current + step;
}
} else {
while current > stop {
result.push(current);
current = current + step;
}
}
result
}
pub fn all<I, T, F>(iter: I, predicate: F) -> bool
where
I: IntoIterator<Item = T>,
F: Fn(T) -> bool,
{
iter.into_iter().all(predicate)
}
pub fn any<I, T, F>(iter: I, predicate: F) -> bool
where
I: IntoIterator<Item = T>,
F: Fn(T) -> bool,
{
iter.into_iter().any(predicate)
}
pub fn linspace<F: Float + std::iter::Sum>(start: F, end: F, num: usize) -> Array1<F> {
if num < 2 {
return Array::from_vec(vec![start]);
}
#[cfg(feature = "parallel")]
{
if num >= 1000 {
use crate::parallel::par_linspace;
let start_f64: Option<f64> = NumCast::from(start);
let end_f64: Option<f64> = NumCast::from(end);
if let (Some(start_64), Some(end_64)) = (start_f64, end_f64) {
let result = par_linspace(start_64, end_64, num);
let mut result_f = Array1::zeros(result.raw_dim());
for (i, val) in result.iter().enumerate() {
if let Some(val_f) = NumCast::from(*val) {
result_f[i] = val_f;
} else {
break;
}
}
return result_f;
}
}
}
let step = (end - start) / F::from(num - 1).unwrap();
let mut result = Vec::with_capacity(num);
for i in 0..num {
let value = start + step * F::from(i).unwrap();
result.push(value);
}
if let Some(last) = result.last_mut() {
*last = end;
}
Array::from_vec(result)
}
pub fn logspace<F: Float + std::iter::Sum>(
start: F,
end: F,
num: usize,
base: Option<F>,
) -> Array1<F> {
let base = base.unwrap_or_else(|| F::from(10.0).unwrap());
let linear = linspace(start, end, num);
linear.mapv(|x| base.powf(x))
}
pub fn maximum<S1, S2, D, T>(
a: &ndarray::ArrayBase<S1, D>,
b: &ndarray::ArrayBase<S2, D>,
) -> Array<T, D>
where
S1: ndarray::Data<Elem = T>,
S2: ndarray::Data<Elem = T>,
D: Dimension,
T: Num + PartialOrd + Copy + Send + Sync,
{
assert_eq!(
a.shape(),
b.shape(),
"Arrays must have the same shape for element-wise maximum"
);
#[cfg(feature = "parallel")]
{
use crate::parallel::par_maximum;
if a.len() > 1000 {
return par_maximum(a, b);
}
}
let mut result = a.to_owned();
for (i, elem) in result.iter_mut().enumerate() {
if let Some(b_slice) = b.as_slice() {
let b_val = b_slice[i];
if b_val > *elem {
*elem = b_val;
}
} else {
let b_val = b.iter().nth(i).unwrap();
if *b_val > *elem {
*elem = *b_val;
}
}
}
result
}
pub fn minimum<S1, S2, D, T>(
a: &ndarray::ArrayBase<S1, D>,
b: &ndarray::ArrayBase<S2, D>,
) -> Array<T, D>
where
S1: ndarray::Data<Elem = T>,
S2: ndarray::Data<Elem = T>,
D: Dimension,
T: Num + PartialOrd + Copy + Send + Sync,
{
assert_eq!(
a.shape(),
b.shape(),
"Arrays must have the same shape for element-wise minimum"
);
#[cfg(feature = "parallel")]
{
use crate::parallel::par_minimum;
if a.len() > 1000 {
return par_minimum(a, b);
}
}
let mut result = a.to_owned();
for (i, elem) in result.iter_mut().enumerate() {
if let Some(b_slice) = b.as_slice() {
let b_val = b_slice[i];
if b_val < *elem {
*elem = b_val;
}
} else {
let b_val = b.iter().nth(i).unwrap();
if *b_val < *elem {
*elem = *b_val;
}
}
}
result
}
pub fn normalize<T>(x: &[T], norm: &str) -> Result<Vec<f64>, &'static str>
where
T: Float + NumCast + Debug,
{
if x.is_empty() {
return Err("Input signal is empty");
}
let x_f64: Vec<f64> = x
.iter()
.map(|&val| NumCast::from(val).ok_or("Could not convert value to f64"))
.collect::<Result<Vec<_>, _>>()?;
match norm.to_lowercase().as_str() {
"energy" => {
let sum_of_squares: f64 = x_f64.iter().map(|&x| x * x).sum();
if sum_of_squares.abs() < f64::EPSILON {
return Err("Signal has zero energy, cannot normalize");
}
let scale = 1.0 / sum_of_squares.sqrt();
let normalized = x_f64.iter().map(|&x| x * scale).collect();
Ok(normalized)
}
"peak" => {
let peak = x_f64.iter().fold(0.0, |a, &b| a.max(b.abs()));
if peak.abs() < f64::EPSILON {
return Err("Signal has zero peak, cannot normalize");
}
let scale = 1.0 / peak;
let normalized = x_f64.iter().map(|&x| x * scale).collect();
Ok(normalized)
}
"sum" => {
let sum: f64 = x_f64.iter().sum();
if sum.abs() < f64::EPSILON {
return Err("Signal has zero sum, cannot normalize");
}
let scale = 1.0 / sum;
let normalized = x_f64.iter().map(|&x| x * scale).collect();
Ok(normalized)
}
"max" => {
let max_val = x_f64.iter().fold(0.0, |a, &b| a.max(b.abs()));
if max_val.abs() < f64::EPSILON {
return Err("Signal has zero maximum, cannot normalize");
}
let scale = 1.0 / max_val;
let normalized = x_f64.iter().map(|&x| x * scale).collect();
Ok(normalized)
}
_ => Err("Unknown normalization type. Supported types: 'energy', 'peak', 'sum', 'max'"),
}
}
pub fn pad_array<T, D>(
input: &Array<T, D>,
pad_width: &[(usize, usize)],
mode: &str,
constant_value: Option<T>,
) -> Result<Array<T, D>, String>
where
T: Float + FromPrimitive + Debug + Clone,
D: Dimension,
{
if input.ndim() == 0 {
return Err("Input array cannot be 0-dimensional".to_string());
}
if pad_width.len() != input.ndim() {
return Err(format!(
"Pad width must have same length as input dimensions (got {} expected {})",
pad_width.len(),
input.ndim()
));
}
if pad_width.iter().all(|&(a, b)| a == 0 && b == 0) {
return Ok(input.to_owned());
}
let mut new_shape = Vec::with_capacity(input.ndim());
for (dim, &(pad_before, pad_after)) in pad_width.iter().enumerate().take(input.ndim()) {
new_shape.push(input.shape()[dim] + pad_before + pad_after);
}
let const_val = constant_value.unwrap_or_else(|| T::zero());
let mut output = Array::<T, D>::from_elem(
D::from_dimension(&ndarray::IxDyn(&new_shape))
.expect("Could not create dimension from shape"),
const_val,
);
if input.ndim() == 1 {
let input_array1 = input
.view()
.into_dimensionality::<ndarray::Ix1>()
.map_err(|_| "Failed to convert to 1D array".to_string())?;
let mut output_array1 = output
.view_mut()
.into_dimensionality::<ndarray::Ix1>()
.map_err(|_| "Failed to convert output to 1D array".to_string())?;
let input_len = input_array1.len();
let start = pad_width[0].0;
for i in 0..input_len {
output_array1[start + i] = input_array1[i];
}
match mode.to_lowercase().as_str() {
"constant" => {
}
"edge" => {
for i in 0..pad_width[0].0 {
output_array1[i] = input_array1[0];
}
let offset = start + input_len;
for i in 0..pad_width[0].1 {
output_array1[offset + i] = input_array1[input_len - 1];
}
}
"reflect" => {
for i in 0..pad_width[0].0 {
let src_idx = pad_width[0].0 - i;
if src_idx < input_len {
output_array1[i] = input_array1[src_idx];
}
}
let offset = start + input_len;
for i in 0..pad_width[0].1 {
let src_idx = input_len - 2 - i;
if src_idx < input_len {
output_array1[offset + i] = input_array1[src_idx];
}
}
}
"wrap" => {
for i in 0..pad_width[0].0 {
let src_idx = (input_len - (pad_width[0].0 - i) % input_len) % input_len;
output_array1[i] = input_array1[src_idx];
}
let offset = start + input_len;
for i in 0..pad_width[0].1 {
let src_idx = i % input_len;
output_array1[offset + i] = input_array1[src_idx];
}
}
"maximum" => {
let max_val = input_array1
.iter()
.fold(T::neg_infinity(), |a, &b| a.max(b));
for i in 0..pad_width[0].0 {
output_array1[i] = max_val;
}
let offset = start + input_len;
for i in 0..pad_width[0].1 {
output_array1[offset + i] = max_val;
}
}
"minimum" => {
let min_val = input_array1.iter().fold(T::infinity(), |a, &b| a.min(b));
for i in 0..pad_width[0].0 {
output_array1[i] = min_val;
}
let offset = start + input_len;
for i in 0..pad_width[0].1 {
output_array1[offset + i] = min_val;
}
}
"mean" => {
let sum = input_array1.iter().fold(T::zero(), |a, &b| a + b);
let mean_val = sum / T::from_usize(input_len).unwrap();
for i in 0..pad_width[0].0 {
output_array1[i] = mean_val;
}
let offset = start + input_len;
for i in 0..pad_width[0].1 {
output_array1[offset + i] = mean_val;
}
}
_ => return Err(format!("Unsupported padding mode: {}", mode)),
}
return Ok(output);
}
if mode.to_lowercase() != "constant" {
return Err(format!(
"Padding mode '{}' is not yet implemented for arrays with more than 1 dimension",
mode
));
}
Ok(output)
}
pub fn get_window(window_type: &str, length: usize, periodic: bool) -> Result<Vec<f64>, String> {
if length == 0 {
return Err("Window length must be positive".to_string());
}
let mut window = Vec::with_capacity(length);
let n = if periodic { length + 1 } else { length };
match window_type.to_lowercase().as_str() {
"hamming" => {
for i in 0..length {
let w =
0.54 - 0.46 * (2.0 * std::f64::consts::PI * i as f64 / (n - 1) as f64).cos();
window.push(w);
}
}
"hanning" | "hann" => {
for i in 0..length {
let w =
0.5 * (1.0 - (2.0 * std::f64::consts::PI * i as f64 / (n - 1) as f64).cos());
window.push(w);
}
}
"blackman" => {
for i in 0..length {
let w = 0.42 - 0.5 * (2.0 * std::f64::consts::PI * i as f64 / (n - 1) as f64).cos()
+ 0.08 * (4.0 * std::f64::consts::PI * i as f64 / (n - 1) as f64).cos();
window.push(w);
}
}
"bartlett" => {
let m = (n - 1) as f64 / 2.0;
for i in 0..length {
let w = 1.0 - ((i as f64 - m) / m).abs();
window.push(w);
}
}
"boxcar" | "rectangular" => {
window.extend(std::iter::repeat_n(1.0, length));
}
"triang" => {
let m = (length - 1) as f64 / 2.0;
for i in 0..length {
let w = 1.0 - ((i as f64 - m) / (m + 1.0)).abs();
window.push(w);
}
}
_ => {
return Err(format!("Unknown window type: {}", window_type));
}
}
Ok(window)
}
pub fn differentiate<F, Func>(x: F, h: F, eval_fn: Func) -> Result<F, String>
where
F: Float + FromPrimitive + Debug,
Func: Fn(F) -> Result<F, String>,
{
let f_plus = eval_fn(x + h).map_err(|e| format!("Error evaluating function at x+h: {}", e))?;
let f_minus = eval_fn(x - h).map_err(|e| format!("Error evaluating function at x-h: {}", e))?;
let derivative = (f_plus - f_minus) / (F::from(2.0).unwrap() * h);
Ok(derivative)
}
pub fn integrate<F, Func>(a: F, b: F, n: usize, eval_fn: Func) -> Result<F, String>
where
F: Float + FromPrimitive + Debug,
Func: Fn(F) -> Result<F, String>,
{
if a > b {
return integrate(b, a, n, eval_fn).map(|result| -result);
}
if n < 2 {
return Err("number of intervals must be at least 2".to_string());
}
if n % 2 != 0 {
return Err("number of intervals must be even".to_string());
}
let h = (b - a) / F::from_usize(n).unwrap();
let mut sum = eval_fn(a).map_err(|e| format!("Error evaluating function at a: {}", e))?
+ eval_fn(b).map_err(|e| format!("Error evaluating function at b: {}", e))?;
for i in 1..n {
if i % 2 == 0 {
let x_i = a + F::from_usize(i).unwrap() * h;
sum = sum
+ F::from(2.0).unwrap()
* eval_fn(x_i)
.map_err(|e| format!("Error evaluating function at x_{}: {}", i, e))?;
}
}
for i in 1..n {
if i % 2 == 1 {
let x_i = a + F::from_usize(i).unwrap() * h;
sum = sum
+ F::from(4.0).unwrap()
* eval_fn(x_i)
.map_err(|e| format!("Error evaluating function at x_{}: {}", i, e))?;
}
}
let integral = h * sum / F::from(3.0).unwrap();
Ok(integral)
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
use ndarray::{arr1, array};
#[test]
fn test_is_close() {
assert!(is_close(1.0, 1.0, 1e-10, 1e-10));
assert!(is_close(1.0, 1.0 + 1e-11, 1e-10, 1e-10));
assert!(!is_close(1.0, 1.1, 1e-10, 1e-10));
assert!(is_close(1e10, 1e10 + 1.0, 1e-10, 1e-9));
}
#[test]
fn test_fill_diagonal() {
let a = Array2::<f64>::zeros((3, 3));
let a_diag = fill_diagonal(a, 5.0);
assert_relative_eq!(a_diag[[0, 0]], 5.0);
assert_relative_eq!(a_diag[[1, 1]], 5.0);
assert_relative_eq!(a_diag[[2, 2]], 5.0);
assert_relative_eq!(a_diag[[0, 1]], 0.0);
}
#[test]
fn test_prod() {
assert_eq!(prod(vec![1, 2, 3, 4]), 24);
assert_eq!(prod(vec![2.0, 3.0, 4.0]), 24.0);
}
#[test]
fn test_arange() {
let result = arange(0.0, 5.0, 1.0);
assert_eq!(result, vec![0.0, 1.0, 2.0, 3.0, 4.0]);
let result = arange(5.0, 0.0, -1.0);
assert_eq!(result, vec![5.0, 4.0, 3.0, 2.0, 1.0]);
}
#[test]
#[should_panic]
fn test_arange_zero_step() {
let _result = arange(0.0, 5.0, 0.0);
}
#[test]
fn test_all() {
assert!(all(vec![2, 4, 6, 8], |x| x % 2 == 0));
assert!(!all(vec![2, 4, 5, 8], |x| x % 2 == 0));
}
#[test]
fn test_any() {
assert!(any(vec![1, 2, 3, 4], |x| x % 2 == 0));
assert!(!any(vec![1, 3, 5, 7], |x| x % 2 == 0));
}
#[test]
fn test_linspace() {
let result = linspace(0.0, 1.0, 5);
let expected = arr1(&[0.0, 0.25, 0.5, 0.75, 1.0]);
assert_eq!(result.len(), 5);
for (a, b) in result.iter().zip(expected.iter()) {
assert_relative_eq!(a, b, epsilon = 1e-14);
}
let result = linspace(5.0, 5.0, 1);
assert_eq!(result.len(), 1);
assert_relative_eq!(result[0], 5.0);
let result = linspace(-10.0, 10.0, 5);
assert_relative_eq!(result[0], -10.0);
assert_relative_eq!(result[4], 10.0);
}
#[test]
fn test_logspace() {
let result = logspace(0.0, 3.0, 4, None);
let expected = arr1(&[1.0, 10.0, 100.0, 1000.0]);
assert_eq!(result.len(), 4);
for (a, b) in result.iter().zip(expected.iter()) {
assert_relative_eq!(a, b, epsilon = 1e-10);
}
let result = logspace(0.0, 3.0, 4, Some(2.0));
let expected = arr1(&[1.0, 2.0, 4.0, 8.0]);
for (a, b) in result.iter().zip(expected.iter()) {
assert_relative_eq!(a, b, epsilon = 1e-10);
}
}
#[test]
fn test_maximum() {
let a = array![[1, 2], [3, 4]];
let b = array![[5, 1], [7, 2]];
let result = maximum(&a, &b);
let expected = array![[5, 2], [7, 4]];
assert_eq!(result, expected);
}
#[test]
fn test_minimum() {
let a = array![[1, 2], [3, 4]];
let b = array![[5, 1], [7, 2]];
let result = minimum(&a, &b);
let expected = array![[1, 1], [3, 2]];
assert_eq!(result, expected);
}
#[test]
#[should_panic]
fn test_maximum_different_shapes() {
let a = array![[1, 2], [3, 4]];
let b = array![[5, 1, 2], [7, 2, 3]];
let _result = maximum(&a, &b);
}
#[test]
fn test_points_equal() {
let point1 = [1.0, 2.0, 3.0];
let point2 = [1.0, 2.0, 3.0];
let point3 = [1.0, 2.0, 3.001];
let point4 = [1.0, 2.0, 4.0];
assert!(points_equal(&point1, &point2, None));
assert!(!points_equal(&point1, &point3, None));
assert!(points_equal(&point1, &point3, Some(0.01)));
assert!(!points_equal(&point1, &point4, Some(0.01)));
}
#[test]
fn test_arrays_equal() {
let arr1 = array![[1.0, 2.0], [3.0, 4.0]];
let arr2 = array![[1.0, 2.0], [3.0, 4.0]];
let arr3 = array![[1.0, 2.0], [3.0, 4.001]];
assert!(arrays_equal(&arr1, &arr2, None));
assert!(!arrays_equal(&arr1, &arr3, None));
assert!(arrays_equal(&arr1, &arr3, Some(0.01)));
}
#[test]
fn test_normalize() {
let signal = vec![1.0, 2.0, 3.0, 4.0];
let normalized = normalize(&signal, "energy").unwrap();
let sum_of_squares: f64 = normalized.iter().map(|&x| x * x).sum();
assert_relative_eq!(sum_of_squares, 1.0, epsilon = 1e-10);
let signal = vec![1.0, -2.0, 3.0, -4.0];
let normalized = normalize(&signal, "peak").unwrap();
let peak = normalized.iter().fold(0.0, |a, &b| a.max(b.abs()));
assert_relative_eq!(peak, 1.0, epsilon = 1e-10);
assert_relative_eq!(normalized[3], -1.0, epsilon = 1e-10);
let signal = vec![1.0, 2.0, 3.0, 4.0];
let normalized = normalize(&signal, "sum").unwrap();
let sum: f64 = normalized.iter().sum();
assert_relative_eq!(sum, 1.0, epsilon = 1e-10);
}
#[test]
fn test_pad_array() {
let arr = array![1.0, 2.0, 3.0];
let padded = pad_array(&arr, &[(1, 2)], "constant", Some(0.0)).unwrap();
assert_eq!(padded.shape(), &[6]);
assert_eq!(padded, array![0.0, 1.0, 2.0, 3.0, 0.0, 0.0]);
let arr = array![1.0, 2.0, 3.0];
let padded = pad_array(&arr, &[(2, 2)], "edge", None).unwrap();
assert_eq!(padded.shape(), &[7]);
assert_eq!(padded, array![1.0, 1.0, 1.0, 2.0, 3.0, 3.0, 3.0]);
let arr = array![1.0, 2.0, 3.0];
let padded = pad_array(&arr, &[(1, 1)], "maximum", None).unwrap();
assert_eq!(padded.shape(), &[5]);
assert_eq!(padded, array![3.0, 1.0, 2.0, 3.0, 3.0]);
}
#[test]
fn test_get_window() {
let window = get_window("hamming", 5, false).unwrap();
assert_eq!(window.len(), 5);
assert!(window[0] > 0.0 && window[0] < 0.6); assert!(window[2] > 0.9);
let window = get_window("hann", 5, false).unwrap();
assert_eq!(window.len(), 5);
assert_relative_eq!(window[0], 0.0, epsilon = 1e-10);
assert!(window[2] > 0.9);
let window = get_window("rectangular", 5, false).unwrap();
assert_eq!(window.len(), 5);
assert!(window.iter().all(|&x| (x - 1.0).abs() < 1e-10));
}
#[test]
fn test_differentiate_integrate() {
let f = |x: f64| -> Result<f64, String> { Ok(x * x) };
let derivative = differentiate(3.0, 0.001, f).unwrap();
assert_relative_eq!(derivative, 6.0, epsilon = 1e-3);
let integral = integrate(0.0, 1.0, 100, f).unwrap();
assert_relative_eq!(integral, 1.0 / 3.0, epsilon = 1e-5);
let integral = integrate(0.0, 2.0, 100, f).unwrap();
assert_relative_eq!(integral, 8.0 / 3.0, epsilon = 1e-5); }
}