use scirs2_core::ndarray::{s, Array1};
use sklears_core::error::SklearsError;
use std::collections::HashMap;
use std::f64::consts::SQRT_2;
pub type WaveletResult<T> = Result<T, WaveletError>;
#[derive(Debug, Clone)]
pub enum WaveletError {
InvalidInput(String),
UnsupportedWavelet(String),
InvalidLevel(String),
IncompatibleLength(String),
ReconstructionError(String),
ComputationError(String),
}
impl std::fmt::Display for WaveletError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
WaveletError::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
WaveletError::UnsupportedWavelet(msg) => write!(f, "Unsupported wavelet: {}", msg),
WaveletError::InvalidLevel(msg) => write!(f, "Invalid level: {}", msg),
WaveletError::IncompatibleLength(msg) => write!(f, "Incompatible length: {}", msg),
WaveletError::ReconstructionError(msg) => write!(f, "Reconstruction error: {}", msg),
WaveletError::ComputationError(msg) => write!(f, "Computation error: {}", msg),
}
}
}
impl std::error::Error for WaveletError {}
impl From<SklearsError> for WaveletError {
fn from(err: SklearsError) -> Self {
WaveletError::ComputationError(format!("Sklears error: {}", err))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum WaveletType {
Haar,
Daubechies2,
Daubechies4,
Daubechies8,
Daubechies16,
Biorthogonal22,
Biorthogonal44,
Biorthogonal68,
Coiflets2,
Coiflets4,
Coiflets6,
}
impl WaveletType {
pub fn filter_length(&self) -> usize {
match self {
WaveletType::Haar => 2,
WaveletType::Daubechies2 => 4,
WaveletType::Daubechies4 => 8,
WaveletType::Daubechies8 => 16,
WaveletType::Daubechies16 => 32,
WaveletType::Biorthogonal22 => 6,
WaveletType::Biorthogonal44 => 10,
WaveletType::Biorthogonal68 => 18,
WaveletType::Coiflets2 => 6,
WaveletType::Coiflets4 => 12,
WaveletType::Coiflets6 => 18,
}
}
pub fn is_orthogonal(&self) -> bool {
matches!(
self,
WaveletType::Haar
| WaveletType::Daubechies2
| WaveletType::Daubechies4
| WaveletType::Daubechies8
| WaveletType::Daubechies16
| WaveletType::Coiflets2
| WaveletType::Coiflets4
| WaveletType::Coiflets6
)
}
pub fn name(&self) -> &'static str {
match self {
WaveletType::Haar => "Haar",
WaveletType::Daubechies2 => "Daubechies-2",
WaveletType::Daubechies4 => "Daubechies-4",
WaveletType::Daubechies8 => "Daubechies-8",
WaveletType::Daubechies16 => "Daubechies-16",
WaveletType::Biorthogonal22 => "Biorthogonal-2.2",
WaveletType::Biorthogonal44 => "Biorthogonal-4.4",
WaveletType::Biorthogonal68 => "Biorthogonal-6.8",
WaveletType::Coiflets2 => "Coiflets-2",
WaveletType::Coiflets4 => "Coiflets-4",
WaveletType::Coiflets6 => "Coiflets-6",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WaveletBoundary {
Zero,
Symmetric,
Periodic,
Constant,
}
impl WaveletBoundary {
pub fn name(&self) -> &'static str {
match self {
WaveletBoundary::Zero => "zero",
WaveletBoundary::Symmetric => "symmetric",
WaveletBoundary::Periodic => "periodic",
WaveletBoundary::Constant => "constant",
}
}
}
#[derive(Debug, Clone)]
pub struct WaveletTransform {
wavelet_type: WaveletType,
boundary: WaveletBoundary,
levels: usize,
normalize: bool,
}
impl WaveletTransform {
pub fn new() -> Self {
Self {
wavelet_type: WaveletType::Daubechies4,
boundary: WaveletBoundary::Symmetric,
levels: 4,
normalize: true,
}
}
pub fn wavelet_type(mut self, wavelet_type: WaveletType) -> Self {
self.wavelet_type = wavelet_type;
self
}
pub fn boundary(mut self, boundary: WaveletBoundary) -> Self {
self.boundary = boundary;
self
}
pub fn levels(mut self, levels: usize) -> Self {
self.levels = levels;
self
}
pub fn normalize(mut self, normalize: bool) -> Self {
self.normalize = normalize;
self
}
pub fn dwt(&self, signal: &Array1<f64>) -> WaveletResult<WaveletDecomposition> {
self.validate_input(signal)?;
let mut current_signal = signal.clone();
let mut details = Vec::new();
for _level in 0..self.levels {
if current_signal.len() < self.wavelet_type.filter_length() {
break;
}
let (approx, detail) = self.single_level_dwt(¤t_signal)?;
details.push(detail);
current_signal = approx;
}
let levels_computed = details.len();
Ok(WaveletDecomposition {
approximation: current_signal,
details,
wavelet_type: self.wavelet_type.clone(),
boundary: self.boundary,
levels_computed,
})
}
pub fn idwt(&self, decomposition: &WaveletDecomposition) -> WaveletResult<Array1<f64>> {
let mut current_signal = decomposition.approximation.clone();
for detail in decomposition.details.iter().rev() {
current_signal = self.single_level_idwt(¤t_signal, detail)?;
}
Ok(current_signal)
}
fn single_level_dwt(&self, signal: &Array1<f64>) -> WaveletResult<(Array1<f64>, Array1<f64>)> {
let filters = self.get_wavelet_filters()?;
let extended_signal = self.extend_signal(signal)?;
let mut approx = self.convolve_downsample(&extended_signal, &filters.0)?;
let mut detail = self.convolve_downsample(&extended_signal, &filters.1)?;
let min_len = approx.len().min(detail.len());
if approx.len() > min_len {
approx = approx.slice(s![0..min_len]).to_owned();
}
if detail.len() > min_len {
detail = detail.slice(s![0..min_len]).to_owned();
}
Ok((approx, detail))
}
fn single_level_idwt(
&self,
approx: &Array1<f64>,
detail: &Array1<f64>,
) -> WaveletResult<Array1<f64>> {
if approx.len() != detail.len() {
return Err(WaveletError::IncompatibleLength(format!(
"Approximation ({}) and detail ({}) coefficients must have same length",
approx.len(),
detail.len()
)));
}
let filters = self.get_reconstruction_filters()?;
let upsampled_approx = self.upsample_convolve(approx, &filters.0)?;
let upsampled_detail = self.upsample_convolve(detail, &filters.1)?;
let mut result = Array1::zeros(upsampled_approx.len());
for i in 0..result.len() {
result[i] = upsampled_approx[i] + upsampled_detail[i];
}
if self.normalize {
let norm_factor = 1.0 / SQRT_2;
result *= norm_factor;
}
Ok(result)
}
#[allow(clippy::excessive_precision)]
fn get_wavelet_filters(&self) -> WaveletResult<(Array1<f64>, Array1<f64>)> {
match self.wavelet_type {
WaveletType::Haar => {
let h = Array1::from_vec(vec![1.0 / SQRT_2, 1.0 / SQRT_2]);
let g = Array1::from_vec(vec![1.0 / SQRT_2, -1.0 / SQRT_2]);
Ok((h, g))
}
WaveletType::Daubechies2 => {
let sqrt_3 = 3.0_f64.sqrt();
let h = Array1::from_vec(vec![
(1.0 + sqrt_3) / (4.0 * SQRT_2),
(3.0 + sqrt_3) / (4.0 * SQRT_2),
(3.0 - sqrt_3) / (4.0 * SQRT_2),
(1.0 - sqrt_3) / (4.0 * SQRT_2),
]);
let g = Array1::from_vec(vec![h[3], -h[2], h[1], -h[0]]);
Ok((h, g))
}
WaveletType::Daubechies4 => {
let h = Array1::from_vec(vec![
0.23037781330885523,
0.7148465705525415,
0.6308807679295904,
-0.02798376941698385,
-0.18703481171888114,
0.030841381835986965,
0.032883011666982945,
-0.010597401784997278,
]);
let mut g = Array1::zeros(h.len());
for (i, &val) in h.iter().enumerate() {
g[i] = if (h.len() - 1 - i) % 2 == 0 {
val
} else {
-val
};
}
Ok((h, g))
}
WaveletType::Daubechies8 => {
let h = Array1::from_vec(vec![
0.05441584224308161,
0.31287159091470997,
0.6756307362980128,
0.5853546836548691,
-0.015829105256023893,
-0.28401554296242809,
0.00047248457399797254,
0.128747426620186,
-0.017369301002456417,
-0.04408825393106472,
0.013981027917015516,
0.008746094047015655,
-0.004870352993451574,
-0.0003917403729959771,
0.0006754494059985568,
-0.00011747678400228192,
]);
let mut g = Array1::zeros(h.len());
for (i, &val) in h.iter().enumerate() {
g[i] = if (h.len() - 1 - i) % 2 == 0 {
val
} else {
-val
};
}
Ok((h, g))
}
WaveletType::Coiflets2 => {
let h = Array1::from_vec(vec![
-0.01565572813546454,
-0.0727326195128539,
0.38486484686420286,
0.8525720202122554,
0.3378976624578092,
-0.07273261951285390,
]);
let mut g = Array1::zeros(h.len());
for (i, &val) in h.iter().enumerate() {
g[i] = if (h.len() - 1 - i) % 2 == 0 {
val
} else {
-val
};
}
Ok((h, g))
}
_ => Err(WaveletError::UnsupportedWavelet(format!(
"Wavelet type {:?} not yet implemented",
self.wavelet_type
))),
}
}
fn get_reconstruction_filters(&self) -> WaveletResult<(Array1<f64>, Array1<f64>)> {
let (h, g) = self.get_wavelet_filters()?;
if self.wavelet_type.is_orthogonal() {
Ok((h, g))
} else {
Ok((h, g))
}
}
fn extend_signal(&self, signal: &Array1<f64>) -> WaveletResult<Array1<f64>> {
if !signal.len().is_multiple_of(2) {
let mut padded = Array1::zeros(signal.len() + 1);
for i in 0..signal.len() {
padded[i] = signal[i];
}
padded[signal.len()] = signal[signal.len() - 1];
Ok(padded)
} else {
Ok(signal.clone())
}
}
fn convolve_downsample(
&self,
signal: &Array1<f64>,
filter: &Array1<f64>,
) -> WaveletResult<Array1<f64>> {
let n = signal.len();
let filter_len = filter.len();
let output_len = n / 2;
let mut result = Array1::zeros(output_len);
for i in 0..output_len {
let start_idx = 2 * i;
let mut sum = 0.0;
for j in 0..filter_len {
let signal_idx = start_idx + j;
if signal_idx < n {
sum += signal[signal_idx] * filter[j]; }
}
result[i] = sum;
}
if self.normalize {
result *= SQRT_2;
}
Ok(result)
}
fn upsample_convolve(
&self,
signal: &Array1<f64>,
filter: &Array1<f64>,
) -> WaveletResult<Array1<f64>> {
let upsampled_len = 2 * signal.len();
let filter_len = filter.len();
let mut upsampled = Array1::zeros(upsampled_len);
for i in 0..signal.len() {
upsampled[2 * i] = signal[i];
}
let output_len = upsampled_len;
let mut result = Array1::zeros(output_len);
for i in 0..output_len {
let mut sum = 0.0;
for j in 0..filter_len {
let idx = if i >= j { i - j } else { continue };
if idx < upsampled_len {
sum += upsampled[idx] * filter[j];
}
}
result[i] = sum;
}
Ok(result)
}
fn validate_input(&self, signal: &Array1<f64>) -> WaveletResult<()> {
if signal.is_empty() {
return Err(WaveletError::InvalidInput(
"Signal cannot be empty".to_string(),
));
}
for &val in signal.iter() {
if !val.is_finite() {
return Err(WaveletError::InvalidInput(
"Signal contains non-finite values".to_string(),
));
}
}
let min_length = self.wavelet_type.filter_length();
if signal.len() < min_length {
return Err(WaveletError::InvalidInput(format!(
"Signal length ({}) must be at least filter length ({})",
signal.len(),
min_length
)));
}
Ok(())
}
pub fn extract_energy_features(&self, signal: &Array1<f64>) -> WaveletResult<Array1<f64>> {
let decomposition = self.dwt(signal)?;
let mut features = Vec::new();
for detail in &decomposition.details {
let energy: f64 = detail.iter().map(|x| x * x).sum();
features.push(energy);
}
let approx_energy: f64 = decomposition.approximation.iter().map(|x| x * x).sum();
features.push(approx_energy);
let total_energy: f64 = features.iter().sum();
if total_energy > 0.0 {
for feature in &mut features {
*feature /= total_energy;
}
}
features.push(total_energy);
for i in 1..decomposition.details.len() {
let ratio = if features[i] > 0.0 {
features[i - 1] / features[i]
} else {
0.0
};
features.push(ratio);
}
Ok(Array1::from_vec(features))
}
}
impl Default for WaveletTransform {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct WaveletDecomposition {
approximation: Array1<f64>,
details: Vec<Array1<f64>>,
wavelet_type: WaveletType,
boundary: WaveletBoundary,
levels_computed: usize,
}
impl WaveletDecomposition {
pub fn approximation(&self) -> &Array1<f64> {
&self.approximation
}
pub fn details(&self) -> &[Array1<f64>] {
&self.details
}
pub fn detail_level(&self, level: usize) -> Option<&Array1<f64>> {
self.details.get(level)
}
pub fn wavelet_type(&self) -> &WaveletType {
&self.wavelet_type
}
pub fn boundary(&self) -> WaveletBoundary {
self.boundary
}
pub fn levels_computed(&self) -> usize {
self.levels_computed
}
pub fn energy_distribution(&self) -> Array1<f64> {
let mut energies = Vec::new();
for detail in &self.details {
let energy: f64 = detail.iter().map(|x| x * x).sum();
energies.push(energy);
}
let approx_energy: f64 = self.approximation.iter().map(|x| x * x).sum();
energies.push(approx_energy);
let total_energy: f64 = energies.iter().sum();
if total_energy > 0.0 {
for energy in &mut energies {
*energy /= total_energy;
}
}
Array1::from_vec(energies)
}
pub fn soft_threshold(&mut self, threshold: f64) {
for detail in &mut self.details {
for coeff in detail.iter_mut() {
if coeff.abs() <= threshold {
*coeff = 0.0;
} else {
*coeff = coeff.signum() * (coeff.abs() - threshold);
}
}
}
}
pub fn hard_threshold(&mut self, threshold: f64) {
for detail in &mut self.details {
for coeff in detail.iter_mut() {
if coeff.abs() <= threshold {
*coeff = 0.0;
}
}
}
}
pub fn sparsity(&self) -> f64 {
let mut total_coeffs = 0;
let mut zero_coeffs = 0;
for detail in &self.details {
for &coeff in detail.iter() {
total_coeffs += 1;
if coeff.abs() < 1e-15 {
zero_coeffs += 1;
}
}
}
for &coeff in self.approximation.iter() {
total_coeffs += 1;
if coeff.abs() < 1e-15 {
zero_coeffs += 1;
}
}
if total_coeffs > 0 {
zero_coeffs as f64 / total_coeffs as f64
} else {
0.0
}
}
pub fn level_statistics(&self) -> Vec<HashMap<String, f64>> {
let mut stats = Vec::new();
for detail in &self.details {
let mut level_stats = HashMap::new();
let mean: f64 = detail.iter().sum::<f64>() / detail.len() as f64;
let variance: f64 =
detail.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / detail.len() as f64;
let std: f64 = variance.sqrt();
let max = detail.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let min = detail.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let energy: f64 = detail.iter().map(|x| x * x).sum();
level_stats.insert("mean".to_string(), mean);
level_stats.insert("std".to_string(), std);
level_stats.insert("min".to_string(), min);
level_stats.insert("max".to_string(), max);
level_stats.insert("energy".to_string(), energy);
stats.push(level_stats);
}
let mut approx_stats = HashMap::new();
let mean: f64 = self.approximation.iter().sum::<f64>() / self.approximation.len() as f64;
let variance: f64 = self
.approximation
.iter()
.map(|x| (x - mean).powi(2))
.sum::<f64>()
/ self.approximation.len() as f64;
let std: f64 = variance.sqrt();
let max = self
.approximation
.iter()
.fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let min = self
.approximation
.iter()
.fold(f64::INFINITY, |a, &b| a.min(b));
let energy: f64 = self.approximation.iter().map(|x| x * x).sum();
approx_stats.insert("mean".to_string(), mean);
approx_stats.insert("std".to_string(), std);
approx_stats.insert("min".to_string(), min);
approx_stats.insert("max".to_string(), max);
approx_stats.insert("energy".to_string(), energy);
stats.push(approx_stats);
stats
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::array;
use std::f64::consts::PI;
fn generate_test_signal(n: usize) -> Array1<f64> {
Array1::from_iter((0..n).map(|i| {
let t = i as f64 / n as f64;
(2.0 * PI * 5.0 * t).sin() + 0.5 * (2.0 * PI * 20.0 * t).sin()
}))
}
fn generate_chirp_signal(n: usize) -> Array1<f64> {
Array1::from_iter((0..n).map(|i| {
let t = i as f64 / n as f64;
let freq = 1.0 + 10.0 * t; (2.0 * PI * freq * t).sin()
}))
}
#[test]
fn test_wavelet_type_properties() {
assert_eq!(WaveletType::Haar.filter_length(), 2);
assert_eq!(WaveletType::Daubechies4.filter_length(), 8);
assert!(WaveletType::Haar.is_orthogonal());
assert!(WaveletType::Daubechies4.is_orthogonal());
assert_eq!(WaveletType::Haar.name(), "Haar");
}
#[test]
fn test_boundary_condition_names() {
assert_eq!(WaveletBoundary::Zero.name(), "zero");
assert_eq!(WaveletBoundary::Symmetric.name(), "symmetric");
assert_eq!(WaveletBoundary::Periodic.name(), "periodic");
assert_eq!(WaveletBoundary::Constant.name(), "constant");
}
#[test]
fn test_haar_wavelet_transform() {
let signal = array![1.0, 2.0, 3.0, 4.0, 3.0, 2.0, 1.0, 0.0];
let wavelet = WaveletTransform::new()
.wavelet_type(WaveletType::Haar)
.levels(2);
let result = wavelet.dwt(&signal).expect("operation should succeed");
assert_eq!(result.levels_computed(), 2);
assert!(!result.approximation().is_empty());
assert_eq!(result.details().len(), 2);
let reconstructed = wavelet.idwt(&result).expect("operation should succeed");
let min_len = signal.len().min(reconstructed.len());
for i in 0..min_len {
let diff = (signal[i] - reconstructed[i]).abs();
assert!(
diff < 1e-10,
"Reconstruction error at index {}: {}",
i,
diff
);
}
}
#[test]
fn test_daubechies_wavelets() {
let signal = generate_test_signal(64);
let wavelets = vec![
WaveletType::Daubechies2,
WaveletType::Daubechies4,
WaveletType::Daubechies8,
];
for wavelet_type in &wavelets {
let wavelet = WaveletTransform::new()
.wavelet_type(wavelet_type.clone())
.levels(3);
let result = wavelet.dwt(&signal).expect("operation should succeed");
assert!(result.levels_computed() > 0);
let original_energy: f64 = signal.iter().map(|x| x * x).sum();
let mut decomp_energy = result.approximation().iter().map(|x| x * x).sum::<f64>();
for detail in result.details() {
decomp_energy += detail.iter().map(|x| x * x).sum::<f64>();
}
let energy_ratio = decomp_energy / original_energy;
assert!(
energy_ratio > 0.1 && energy_ratio < 50.0,
"Energy ratio should be in reasonable range for {:?}: ratio = {}",
wavelet_type,
energy_ratio
);
}
}
#[test]
fn test_boundary_conditions() {
let signal = generate_test_signal(32);
let boundaries = vec![
WaveletBoundary::Zero,
WaveletBoundary::Symmetric,
WaveletBoundary::Periodic,
WaveletBoundary::Constant,
];
for boundary in boundaries {
let wavelet = WaveletTransform::new()
.wavelet_type(WaveletType::Daubechies4)
.boundary(boundary)
.levels(3);
let result = wavelet.dwt(&signal).expect("operation should succeed");
assert!(result.levels_computed() > 0);
let reconstructed = wavelet.idwt(&result).expect("operation should succeed");
assert!(!reconstructed.is_empty());
}
}
#[test]
fn test_perfect_reconstruction() {
let signal = array![1.0, 4.0, 2.0, 8.0, 3.0, 6.0, 1.0, 5.0];
let wavelet = WaveletTransform::new()
.wavelet_type(WaveletType::Haar)
.boundary(WaveletBoundary::Periodic)
.levels(3);
let decomposition = wavelet.dwt(&signal).expect("operation should succeed");
let reconstructed = wavelet
.idwt(&decomposition)
.expect("operation should succeed");
let reconstruction_error = signal
.iter()
.zip(reconstructed.iter())
.map(|(a, b)| (a - b).abs())
.fold(0.0, f64::max);
assert!(
reconstruction_error < 1e-10,
"Perfect reconstruction failed: max error = {}",
reconstruction_error
);
}
#[test]
fn test_energy_features() {
let signal = generate_chirp_signal(128);
let wavelet = WaveletTransform::new()
.wavelet_type(WaveletType::Daubechies4)
.levels(4);
let features = wavelet
.extract_energy_features(&signal)
.expect("operation should succeed");
assert!(features.len() > 4);
for &feature in features.iter() {
assert!(
feature >= 0.0,
"Energy feature should be non-negative: {}",
feature
);
assert!(
feature.is_finite(),
"Energy feature should be finite: {}",
feature
);
}
}
#[test]
fn test_wavelet_decomposition_methods() {
let signal = generate_test_signal(64);
let wavelet = WaveletTransform::new()
.wavelet_type(WaveletType::Daubechies4)
.levels(3);
let mut decomposition = wavelet.dwt(&signal).expect("operation should succeed");
let energy_dist = decomposition.energy_distribution();
let total_energy: f64 = energy_dist.sum();
assert!(
(total_energy - 1.0).abs() < 1e-10,
"Energy distribution should sum to 1"
);
let original_sparsity = decomposition.sparsity();
decomposition.soft_threshold(0.1);
let new_sparsity = decomposition.sparsity();
assert!(
new_sparsity >= original_sparsity,
"Sparsity should increase after thresholding"
);
let stats = decomposition.level_statistics();
assert_eq!(stats.len(), decomposition.levels_computed() + 1);
for level_stats in &stats {
assert!(level_stats.contains_key("mean"));
assert!(level_stats.contains_key("std"));
assert!(level_stats.contains_key("energy"));
}
}
#[test]
fn test_error_handling() {
let wavelet = WaveletTransform::new();
let empty_signal = Array1::zeros(0);
assert!(wavelet.dwt(&empty_signal).is_err());
let bad_signal = array![1.0, 2.0, f64::NAN, 4.0];
assert!(wavelet.dwt(&bad_signal).is_err());
let short_signal = array![1.0];
assert!(wavelet.dwt(&short_signal).is_err());
}
#[test]
fn test_coiflets_wavelets() {
let signal = generate_test_signal(64);
let wavelet = WaveletTransform::new()
.wavelet_type(WaveletType::Coiflets2)
.levels(2);
let result = wavelet.dwt(&signal).expect("operation should succeed");
assert!(result.levels_computed() > 0);
let energy_dist = result.energy_distribution();
assert!(energy_dist.iter().all(|&x| x.is_finite()));
}
#[test]
fn test_wavelet_properties_consistency() {
let wavelets = vec![
WaveletType::Haar,
WaveletType::Daubechies2,
WaveletType::Daubechies4,
WaveletType::Coiflets2,
];
let signal = generate_test_signal(32);
for wavelet_type in &wavelets {
let wavelet = WaveletTransform::new()
.wavelet_type(wavelet_type.clone())
.levels(2);
let result = wavelet.dwt(&signal);
assert!(result.is_ok(), "Wavelet {:?} should work", wavelet_type);
let decomposition = result.expect("operation should succeed");
assert_eq!(decomposition.wavelet_type(), wavelet_type);
let reconstructed = wavelet.idwt(&decomposition);
assert!(
reconstructed.is_ok(),
"Reconstruction should work for {:?}",
wavelet_type
);
}
}
#[test]
fn test_multi_level_decomposition() {
let signal = generate_test_signal(256);
for levels in 1..6 {
let wavelet = WaveletTransform::new()
.wavelet_type(WaveletType::Daubechies4)
.levels(levels);
let result = wavelet.dwt(&signal).expect("operation should succeed");
assert!(result.levels_computed() <= levels);
assert!(result.levels_computed() > 0);
for detail in result.details() {
assert!(
!detail.is_empty(),
"Detail coefficients should not be empty"
);
assert!(
detail.len() <= signal.len(),
"Detail length should not exceed original signal"
);
}
}
}
#[test]
fn test_normalization_effects() {
let signal = array![1.0, 2.0, 3.0, 4.0, 3.0, 2.0, 1.0, 0.0];
let wavelet_normalized = WaveletTransform::new()
.wavelet_type(WaveletType::Haar)
.normalize(true)
.levels(2);
let wavelet_unnormalized = WaveletTransform::new()
.wavelet_type(WaveletType::Haar)
.normalize(false)
.levels(2);
let result_norm = wavelet_normalized
.dwt(&signal)
.expect("operation should succeed");
let result_unnorm = wavelet_unnormalized
.dwt(&signal)
.expect("operation should succeed");
assert_eq!(
result_norm.levels_computed(),
result_unnorm.levels_computed()
);
let norm_energy = result_norm
.approximation()
.iter()
.map(|x| x * x)
.sum::<f64>();
let unnorm_energy = result_unnorm
.approximation()
.iter()
.map(|x| x * x)
.sum::<f64>();
assert!(
(norm_energy - unnorm_energy).abs() > 1e-10,
"Normalization should affect coefficient magnitudes"
);
}
}