use crate::{FloatElement, Tensor, TensorElement};
use torsh_core::error::{Result, TorshError};
impl<T> Tensor<T>
where
T: TensorElement
+ FloatElement
+ Copy
+ Default
+ std::ops::Add<Output = T>
+ std::ops::Sub<Output = T>
+ std::ops::Mul<Output = T>
+ std::ops::Div<Output = T>
+ PartialOrd,
{
#[allow(clippy::needless_range_loop)]
pub fn correlate1d(&self, other: &Self, mode: &str) -> Result<Self> {
let input_shape = self.shape();
let other_shape = other.shape();
if input_shape.dims().len() != 1 || other_shape.dims().len() != 1 {
return Err(TorshError::InvalidArgument(format!(
"Expected 1D tensors, got {}D and {}D",
input_shape.dims().len(),
other_shape.dims().len()
)));
}
let input_data = self.data()?;
let other_data = other.data()?;
let input_len = input_shape.dims()[0];
let other_len = other_shape.dims()[0];
let output_len = match mode {
"full" => input_len + other_len - 1,
"valid" => {
if input_len >= other_len {
input_len - other_len + 1
} else {
0
}
}
"same" => input_len,
_ => {
return Err(TorshError::InvalidArgument(
"Mode must be 'full', 'valid', or 'same'".to_string(),
))
}
};
if output_len == 0 {
return Self::from_data(vec![], vec![0], self.device);
}
let mut output_data = vec![T::default(); output_len];
let start_pos = match mode {
"full" => 0,
"valid" => other_len - 1,
"same" => (other_len - 1) / 2,
_ => 0,
};
for i in 0..output_len {
let mut sum = T::default();
let actual_i = i + start_pos;
for j in 0..other_len {
let input_idx = actual_i as isize - j as isize;
if input_idx >= 0 && (input_idx as usize) < input_len {
sum = sum + input_data[input_idx as usize] * other_data[j];
}
}
output_data[i] = sum;
}
Self::from_data(output_data, vec![output_len], self.device)
}
pub fn moving_average(&self, window_size: usize) -> Result<Self> {
let input_shape = self.shape();
if input_shape.dims().len() != 1 {
return Err(TorshError::InvalidArgument(format!(
"Expected 1D tensor, got {}D",
input_shape.dims().len()
)));
}
if window_size == 0 {
return Err(TorshError::InvalidArgument(
"Window size must be greater than 0".to_string(),
));
}
let input_data = self.data()?;
let input_len = input_shape.dims()[0];
if window_size > input_len {
return Err(TorshError::InvalidArgument(format!(
"Window size ({window_size}) cannot be larger than input length ({input_len})"
)));
}
let output_len = input_len - window_size + 1;
let mut output_data = vec![T::default(); output_len];
for i in 0..output_len {
let mut sum = T::default();
for j in 0..window_size {
sum = sum + input_data[i + j];
}
let window_size_t = T::from_f64(window_size as f64).unwrap_or(T::default());
output_data[i] = sum / window_size_t;
}
Self::from_data(output_data, vec![output_len], self.device)
}
pub fn gaussian_filter(&self, sigma: f64, kernel_size: usize) -> Result<Self> {
let input_shape = self.shape();
if input_shape.dims().len() != 1 {
return Err(TorshError::InvalidArgument(format!(
"Expected 1D tensor, got {}D",
input_shape.dims().len()
)));
}
if kernel_size == 0 || kernel_size % 2 == 0 {
return Err(TorshError::InvalidArgument(
"Kernel size must be odd and greater than 0".to_string(),
));
}
if sigma <= 0.0 {
return Err(TorshError::InvalidArgument(
"Sigma must be greater than 0".to_string(),
));
}
let half_size = kernel_size / 2;
let mut kernel_data = vec![T::default(); kernel_size];
let mut kernel_sum = T::default();
for (i, kernel_val) in kernel_data.iter_mut().enumerate() {
let x = i as f64 - half_size as f64;
let gauss_val = (-0.5 * x * x / (sigma * sigma)).exp();
let kernel_val_computed = T::from_f64(gauss_val).unwrap_or(T::default());
*kernel_val = kernel_val_computed;
kernel_sum = kernel_sum + kernel_val_computed;
}
for kernel_val in kernel_data.iter_mut() {
*kernel_val = *kernel_val / kernel_sum;
}
let kernel = Self::from_data(kernel_data, vec![kernel_size], self.device)?;
let padding = half_size;
self.conv1d(&kernel, None, 1, padding, 1, 1)
}
pub fn median_filter(&self, window_size: usize) -> Result<Self> {
let input_shape = self.shape();
if input_shape.dims().len() != 1 {
return Err(TorshError::InvalidArgument(format!(
"Expected 1D tensor, got {}D",
input_shape.dims().len()
)));
}
if window_size == 0 || window_size % 2 == 0 {
return Err(TorshError::InvalidArgument(
"Window size must be odd and greater than 0".to_string(),
));
}
let input_data = self.data()?;
let input_len = input_shape.dims()[0];
if window_size > input_len {
return Err(TorshError::InvalidArgument(format!(
"Window size ({window_size}) cannot be larger than input length ({input_len})"
)));
}
let half_window = window_size / 2;
let mut output_data = vec![T::default(); input_len];
for (i, output_val) in output_data.iter_mut().enumerate() {
let start = i.saturating_sub(half_window);
let end = if i + half_window < input_len {
i + half_window + 1
} else {
input_len
};
let mut window_values: Vec<T> = input_data[start..end].to_vec();
window_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let median_idx = window_values.len() / 2;
*output_val = window_values[median_idx];
}
Self::from_data(output_data, vec![input_len], self.device)
}
pub fn filter1d(&self, kernel: &Self, mode: &str) -> Result<Self> {
let kernel_data = kernel.data()?;
let flipped_kernel_data: Vec<T> = kernel_data.iter().rev().copied().collect();
let flipped_kernel = Self::from_data(
flipped_kernel_data,
kernel.shape().dims().to_vec(),
kernel.device,
)?;
self.correlate1d(&flipped_kernel, mode)
}
pub fn highpass_diff(&self) -> Result<Self> {
let input_shape = self.shape();
if input_shape.dims().len() != 1 {
return Err(TorshError::InvalidArgument(format!(
"Expected 1D tensor, got {}D",
input_shape.dims().len()
)));
}
let input_data = self.data()?;
let input_len = input_shape.dims()[0];
if input_len < 2 {
return Err(TorshError::InvalidArgument(
"Input must have at least 2 elements for difference operation".to_string(),
));
}
let output_len = input_len - 1;
let mut output_data = vec![T::default(); output_len];
for i in 0..output_len {
output_data[i] = input_data[i + 1] - input_data[i];
}
Self::from_data(output_data, vec![output_len], self.device)
}
pub fn lowpass_avg(&self) -> Result<Self> {
let input_shape = self.shape();
if input_shape.dims().len() != 1 {
return Err(TorshError::InvalidArgument(format!(
"Expected 1D tensor, got {}D",
input_shape.dims().len()
)));
}
let input_data = self.data()?;
let input_len = input_shape.dims()[0];
if input_len < 2 {
return Err(TorshError::InvalidArgument(
"Input must have at least 2 elements for averaging operation".to_string(),
));
}
let output_len = input_len - 1;
let mut output_data = vec![T::default(); output_len];
let two = T::from_f64(2.0).unwrap_or_else(|| <T as TensorElement>::one() + <T as TensorElement>::one());
for i in 0..output_len {
output_data[i] = (input_data[i] + input_data[i + 1]) / two;
}
Self::from_data(output_data, vec![output_len], self.device)
}
}
pub mod utils {
use super::*;
pub fn gaussian_kernel<T: FloatElement + TensorElement + Default>(
size: usize,
sigma: f64,
device: torsh_core::device::DeviceType,
) -> Result<Tensor<T>> {
if size == 0 || size % 2 == 0 {
return Err(TorshError::InvalidArgument(
"Kernel size must be odd and greater than 0".to_string(),
));
}
if sigma <= 0.0 {
return Err(TorshError::InvalidArgument(
"Sigma must be greater than 0".to_string(),
));
}
let half_size = size / 2;
let mut kernel_data = vec![T::default(); size];
let mut kernel_sum = T::default();
for (i, kernel_val) in kernel_data.iter_mut().enumerate() {
let x = i as f64 - half_size as f64;
let gauss_val = (-0.5 * x * x / (sigma * sigma)).exp();
let kernel_val_computed = T::from_f64(gauss_val).unwrap_or(T::default());
*kernel_val = kernel_val_computed;
kernel_sum = kernel_sum + kernel_val_computed;
}
for kernel_val in kernel_data.iter_mut() {
*kernel_val = *kernel_val / kernel_sum;
}
Tensor::from_data(kernel_data, vec![size], device)
}
pub fn box_kernel<T: FloatElement + TensorElement + Default>(
size: usize,
device: torsh_core::device::DeviceType,
) -> Result<Tensor<T>> {
if size == 0 {
return Err(TorshError::InvalidArgument(
"Kernel size must be greater than 0".to_string(),
));
}
let kernel_val = T::from_f64(1.0 / size as f64).unwrap_or(T::default());
let kernel_data = vec![kernel_val; size];
Tensor::from_data(kernel_data, vec![size], device)
}
pub fn derivative_kernel<T: FloatElement + TensorElement + Default>(
device: torsh_core::device::DeviceType,
) -> Result<Tensor<T>> {
let kernel_data = vec![
T::from_f64(-1.0).unwrap_or(T::default()),
<T as TensorElement>::zero(),
T::from_f64(1.0).unwrap_or(T::default()),
];
Tensor::from_data(kernel_data, vec![3], device)
}
}
#[cfg(test)]
mod tests {
use super::*;
use torsh_core::device::DeviceType;
#[test]
fn test_correlate1d_full() {
let signal = Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0], vec![4], DeviceType::Cpu).expect("operation should succeed");
let kernel = Tensor::from_data(vec![1.0f32, 0.5], vec![2], DeviceType::Cpu).expect("operation should succeed");
let result = signal.correlate1d(&kernel, "full").expect("operation should succeed");
let data = result.data().expect("data retrieval should succeed");
assert_eq!(data.len(), 5);
}
#[test]
fn test_correlate1d_valid() {
let signal = Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0], vec![4], DeviceType::Cpu).expect("operation should succeed");
let kernel = Tensor::from_data(vec![1.0f32, 0.5], vec![2], DeviceType::Cpu).expect("operation should succeed");
let result = signal.correlate1d(&kernel, "valid").expect("operation should succeed");
let data = result.data().expect("data retrieval should succeed");
assert_eq!(data.len(), 3);
}
#[test]
fn test_correlate1d_same() {
let signal = Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0], vec![4], DeviceType::Cpu).expect("operation should succeed");
let kernel = Tensor::from_data(vec![1.0f32, 0.5], vec![2], DeviceType::Cpu).expect("operation should succeed");
let result = signal.correlate1d(&kernel, "same").expect("operation should succeed");
let data = result.data().expect("data retrieval should succeed");
assert_eq!(data.len(), 4);
}
#[test]
fn test_moving_average() {
let signal = Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0, 5.0], vec![5], DeviceType::Cpu).expect("operation should succeed");
let result = signal.moving_average(3).expect("operation should succeed");
let data = result.data().expect("data retrieval should succeed");
assert_eq!(data.len(), 3);
assert!((data[0] - 2.0).abs() < 1e-6);
assert!((data[1] - 3.0).abs() < 1e-6);
assert!((data[2] - 4.0).abs() < 1e-6);
}
#[test]
fn test_gaussian_filter() {
let signal = Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0, 5.0], vec![5], DeviceType::Cpu).expect("operation should succeed");
let result = signal.gaussian_filter(1.0, 3);
assert!(result.is_ok());
let smoothed = result.expect("operation should succeed");
assert_eq!(smoothed.data().expect("data retrieval should succeed").len(), signal.shape().numel());
}
#[test]
fn test_median_filter() {
let signal = Tensor::from_data(vec![1.0f32, 100.0, 2.0, 3.0, 200.0, 4.0], vec![6], DeviceType::Cpu).expect("operation should succeed");
let result = signal.median_filter(3).expect("operation should succeed");
let data = result.data().expect("data retrieval should succeed");
assert_eq!(data.len(), 6);
assert!(data[1] < 50.0); assert!(data[4] < 50.0); }
#[test]
fn test_highpass_diff() {
let signal = Tensor::from_data(vec![1.0f32, 3.0, 2.0, 5.0], vec![4], DeviceType::Cpu).expect("operation should succeed");
let result = signal.highpass_diff().expect("highpass diff should succeed");
let data = result.data().expect("data retrieval should succeed");
assert_eq!(data.len(), 3);
assert!((data[0] - 2.0).abs() < 1e-6);
assert!((data[1] - (-1.0)).abs() < 1e-6);
assert!((data[2] - 3.0).abs() < 1e-6);
}
#[test]
fn test_lowpass_avg() {
let signal = Tensor::from_data(vec![1.0f32, 3.0, 2.0, 4.0], vec![4], DeviceType::Cpu).expect("operation should succeed");
let result = signal.lowpass_avg().expect("lowpass avg should succeed");
let data = result.data().expect("data retrieval should succeed");
assert_eq!(data.len(), 3);
assert!((data[0] - 2.0).abs() < 1e-6);
assert!((data[1] - 2.5).abs() < 1e-6);
assert!((data[2] - 3.0).abs() < 1e-6);
}
#[test]
fn test_signal_processing_error_cases() {
let signal_2d = Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0], vec![2, 2], DeviceType::Cpu).expect("operation should succeed");
assert!(signal_2d.moving_average(2).is_err());
assert!(signal_2d.median_filter(3).is_err());
let signal_1d = Tensor::from_data(vec![1.0f32, 2.0, 3.0], vec![3], DeviceType::Cpu).expect("operation should succeed");
assert!(signal_1d.moving_average(0).is_err()); assert!(signal_1d.moving_average(5).is_err()); assert!(signal_1d.median_filter(4).is_err()); assert!(signal_1d.gaussian_filter(0.0, 3).is_err()); assert!(signal_1d.gaussian_filter(1.0, 4).is_err()); }
#[test]
fn test_utils_gaussian_kernel() {
let kernel = utils::gaussian_kernel::<f32>(5, 1.0, DeviceType::Cpu).expect("operation should succeed");
let data = kernel.data().expect("data retrieval should succeed");
let sum: f32 = data.iter().sum();
assert!((sum - 1.0).abs() < 1e-6);
assert!((data[0] - data[4]).abs() < 1e-6);
assert!((data[1] - data[3]).abs() < 1e-6);
assert!(data[2] >= data[0]);
assert!(data[2] >= data[1]);
}
#[test]
fn test_utils_box_kernel() {
let kernel = utils::box_kernel::<f32>(3, DeviceType::Cpu).expect("operation should succeed");
let data = kernel.data().expect("data retrieval should succeed");
for &val in data.iter() {
assert!((val - 1.0 / 3.0).abs() < 1e-6);
}
}
#[test]
fn test_utils_derivative_kernel() {
let kernel = utils::derivative_kernel::<f32>(DeviceType::Cpu).expect("operation should succeed");
let data = kernel.data().expect("data retrieval should succeed");
assert_eq!(data.len(), 3);
assert!((data[0] - (-1.0)).abs() < 1e-6);
assert!((data[1] - 0.0).abs() < 1e-6);
assert!((data[2] - 1.0).abs() < 1e-6);
}
}