1use scirs2_core::ndarray::{s, Array1};
50use sklears_core::error::SklearsError;
51use std::collections::HashMap;
52use std::f64::consts::SQRT_2;
53
54pub type WaveletResult<T> = Result<T, WaveletError>;
56
57#[derive(Debug, Clone)]
59pub enum WaveletError {
60 InvalidInput(String),
62 UnsupportedWavelet(String),
64 InvalidLevel(String),
66 IncompatibleLength(String),
68 ReconstructionError(String),
70 ComputationError(String),
72}
73
74impl std::fmt::Display for WaveletError {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 match self {
77 WaveletError::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
78 WaveletError::UnsupportedWavelet(msg) => write!(f, "Unsupported wavelet: {}", msg),
79 WaveletError::InvalidLevel(msg) => write!(f, "Invalid level: {}", msg),
80 WaveletError::IncompatibleLength(msg) => write!(f, "Incompatible length: {}", msg),
81 WaveletError::ReconstructionError(msg) => write!(f, "Reconstruction error: {}", msg),
82 WaveletError::ComputationError(msg) => write!(f, "Computation error: {}", msg),
83 }
84 }
85}
86
87impl std::error::Error for WaveletError {}
88
89impl From<SklearsError> for WaveletError {
90 fn from(err: SklearsError) -> Self {
91 WaveletError::ComputationError(format!("Sklears error: {}", err))
92 }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Hash)]
97pub enum WaveletType {
98 Haar,
100 Daubechies2,
102 Daubechies4,
103 Daubechies8,
104 Daubechies16,
105 Biorthogonal22,
107 Biorthogonal44,
108 Biorthogonal68,
109 Coiflets2,
111 Coiflets4,
112 Coiflets6,
113}
114
115impl WaveletType {
116 pub fn filter_length(&self) -> usize {
118 match self {
119 WaveletType::Haar => 2,
120 WaveletType::Daubechies2 => 4,
121 WaveletType::Daubechies4 => 8,
122 WaveletType::Daubechies8 => 16,
123 WaveletType::Daubechies16 => 32,
124 WaveletType::Biorthogonal22 => 6,
125 WaveletType::Biorthogonal44 => 10,
126 WaveletType::Biorthogonal68 => 18,
127 WaveletType::Coiflets2 => 6,
128 WaveletType::Coiflets4 => 12,
129 WaveletType::Coiflets6 => 18,
130 }
131 }
132
133 pub fn is_orthogonal(&self) -> bool {
135 matches!(
136 self,
137 WaveletType::Haar
138 | WaveletType::Daubechies2
139 | WaveletType::Daubechies4
140 | WaveletType::Daubechies8
141 | WaveletType::Daubechies16
142 | WaveletType::Coiflets2
143 | WaveletType::Coiflets4
144 | WaveletType::Coiflets6
145 )
146 }
147
148 pub fn name(&self) -> &'static str {
150 match self {
151 WaveletType::Haar => "Haar",
152 WaveletType::Daubechies2 => "Daubechies-2",
153 WaveletType::Daubechies4 => "Daubechies-4",
154 WaveletType::Daubechies8 => "Daubechies-8",
155 WaveletType::Daubechies16 => "Daubechies-16",
156 WaveletType::Biorthogonal22 => "Biorthogonal-2.2",
157 WaveletType::Biorthogonal44 => "Biorthogonal-4.4",
158 WaveletType::Biorthogonal68 => "Biorthogonal-6.8",
159 WaveletType::Coiflets2 => "Coiflets-2",
160 WaveletType::Coiflets4 => "Coiflets-4",
161 WaveletType::Coiflets6 => "Coiflets-6",
162 }
163 }
164}
165
166#[derive(Debug, Clone, Copy, PartialEq, Eq)]
168pub enum WaveletBoundary {
169 Zero,
171 Symmetric,
173 Periodic,
175 Constant,
177}
178
179impl WaveletBoundary {
180 pub fn name(&self) -> &'static str {
182 match self {
183 WaveletBoundary::Zero => "zero",
184 WaveletBoundary::Symmetric => "symmetric",
185 WaveletBoundary::Periodic => "periodic",
186 WaveletBoundary::Constant => "constant",
187 }
188 }
189}
190
191#[derive(Debug, Clone)]
193pub struct WaveletTransform {
194 wavelet_type: WaveletType,
195 boundary: WaveletBoundary,
196 levels: usize,
197 normalize: bool,
198}
199
200impl WaveletTransform {
201 pub fn new() -> Self {
203 Self {
204 wavelet_type: WaveletType::Daubechies4,
205 boundary: WaveletBoundary::Symmetric,
206 levels: 4,
207 normalize: true,
208 }
209 }
210
211 pub fn wavelet_type(mut self, wavelet_type: WaveletType) -> Self {
213 self.wavelet_type = wavelet_type;
214 self
215 }
216
217 pub fn boundary(mut self, boundary: WaveletBoundary) -> Self {
219 self.boundary = boundary;
220 self
221 }
222
223 pub fn levels(mut self, levels: usize) -> Self {
225 self.levels = levels;
226 self
227 }
228
229 pub fn normalize(mut self, normalize: bool) -> Self {
231 self.normalize = normalize;
232 self
233 }
234
235 pub fn dwt(&self, signal: &Array1<f64>) -> WaveletResult<WaveletDecomposition> {
237 self.validate_input(signal)?;
238
239 let mut current_signal = signal.clone();
240 let mut details = Vec::new();
241
242 for _level in 0..self.levels {
243 if current_signal.len() < self.wavelet_type.filter_length() {
244 break;
245 }
246
247 let (approx, detail) = self.single_level_dwt(¤t_signal)?;
248 details.push(detail);
249 current_signal = approx;
250 }
251
252 let levels_computed = details.len();
255
256 Ok(WaveletDecomposition {
257 approximation: current_signal,
258 details,
259 wavelet_type: self.wavelet_type.clone(),
260 boundary: self.boundary,
261 levels_computed,
262 })
263 }
264
265 pub fn idwt(&self, decomposition: &WaveletDecomposition) -> WaveletResult<Array1<f64>> {
267 let mut current_signal = decomposition.approximation.clone();
268
269 for detail in decomposition.details.iter().rev() {
271 current_signal = self.single_level_idwt(¤t_signal, detail)?;
272 }
273
274 Ok(current_signal)
275 }
276
277 fn single_level_dwt(&self, signal: &Array1<f64>) -> WaveletResult<(Array1<f64>, Array1<f64>)> {
279 let filters = self.get_wavelet_filters()?;
280 let extended_signal = self.extend_signal(signal)?;
281
282 let mut approx = self.convolve_downsample(&extended_signal, &filters.0)?;
283 let mut detail = self.convolve_downsample(&extended_signal, &filters.1)?;
284
285 let min_len = approx.len().min(detail.len());
287
288 if approx.len() > min_len {
289 approx = approx.slice(s![0..min_len]).to_owned();
290 }
291 if detail.len() > min_len {
292 detail = detail.slice(s![0..min_len]).to_owned();
293 }
294
295 Ok((approx, detail))
296 }
297
298 fn single_level_idwt(
300 &self,
301 approx: &Array1<f64>,
302 detail: &Array1<f64>,
303 ) -> WaveletResult<Array1<f64>> {
304 if approx.len() != detail.len() {
305 return Err(WaveletError::IncompatibleLength(format!(
306 "Approximation ({}) and detail ({}) coefficients must have same length",
307 approx.len(),
308 detail.len()
309 )));
310 }
311
312 let filters = self.get_reconstruction_filters()?;
313
314 let upsampled_approx = self.upsample_convolve(approx, &filters.0)?;
315 let upsampled_detail = self.upsample_convolve(detail, &filters.1)?;
316
317 let mut result = Array1::zeros(upsampled_approx.len());
319 for i in 0..result.len() {
320 result[i] = upsampled_approx[i] + upsampled_detail[i];
321 }
322
323 if self.normalize {
324 let norm_factor = 1.0 / SQRT_2;
325 result *= norm_factor;
326 }
327
328 Ok(result)
329 }
330
331 #[allow(clippy::excessive_precision)]
333 fn get_wavelet_filters(&self) -> WaveletResult<(Array1<f64>, Array1<f64>)> {
334 match self.wavelet_type {
335 WaveletType::Haar => {
336 let h = Array1::from_vec(vec![1.0 / SQRT_2, 1.0 / SQRT_2]);
337 let g = Array1::from_vec(vec![1.0 / SQRT_2, -1.0 / SQRT_2]);
338 Ok((h, g))
339 }
340 WaveletType::Daubechies2 => {
341 let sqrt_3 = 3.0_f64.sqrt();
342 let h = Array1::from_vec(vec![
343 (1.0 + sqrt_3) / (4.0 * SQRT_2),
344 (3.0 + sqrt_3) / (4.0 * SQRT_2),
345 (3.0 - sqrt_3) / (4.0 * SQRT_2),
346 (1.0 - sqrt_3) / (4.0 * SQRT_2),
347 ]);
348 let g = Array1::from_vec(vec![h[3], -h[2], h[1], -h[0]]);
349 Ok((h, g))
350 }
351 WaveletType::Daubechies4 => {
352 let h = Array1::from_vec(vec![
353 0.23037781330885523,
354 0.7148465705525415,
355 0.6308807679295904,
356 -0.02798376941698385,
357 -0.18703481171888114,
358 0.030841381835986965,
359 0.032883011666982945,
360 -0.010597401784997278,
361 ]);
362 let mut g = Array1::zeros(h.len());
363 for (i, &val) in h.iter().enumerate() {
364 g[i] = if (h.len() - 1 - i) % 2 == 0 {
365 val
366 } else {
367 -val
368 };
369 }
370 Ok((h, g))
371 }
372 WaveletType::Daubechies8 => {
373 let h = Array1::from_vec(vec![
374 0.05441584224308161,
375 0.31287159091470997,
376 0.6756307362980128,
377 0.5853546836548691,
378 -0.015829105256023893,
379 -0.28401554296242809,
380 0.00047248457399797254,
381 0.128747426620186,
382 -0.017369301002456417,
383 -0.04408825393106472,
384 0.013981027917015516,
385 0.008746094047015655,
386 -0.004870352993451574,
387 -0.0003917403729959771,
388 0.0006754494059985568,
389 -0.00011747678400228192,
390 ]);
391 let mut g = Array1::zeros(h.len());
392 for (i, &val) in h.iter().enumerate() {
393 g[i] = if (h.len() - 1 - i) % 2 == 0 {
394 val
395 } else {
396 -val
397 };
398 }
399 Ok((h, g))
400 }
401 WaveletType::Coiflets2 => {
402 let h = Array1::from_vec(vec![
403 -0.01565572813546454,
404 -0.0727326195128539,
405 0.38486484686420286,
406 0.8525720202122554,
407 0.3378976624578092,
408 -0.07273261951285390,
409 ]);
410 let mut g = Array1::zeros(h.len());
411 for (i, &val) in h.iter().enumerate() {
412 g[i] = if (h.len() - 1 - i) % 2 == 0 {
413 val
414 } else {
415 -val
416 };
417 }
418 Ok((h, g))
419 }
420 _ => Err(WaveletError::UnsupportedWavelet(format!(
421 "Wavelet type {:?} not yet implemented",
422 self.wavelet_type
423 ))),
424 }
425 }
426
427 fn get_reconstruction_filters(&self) -> WaveletResult<(Array1<f64>, Array1<f64>)> {
441 let (h, g) = self.get_wavelet_filters()?;
442
443 if self.wavelet_type.is_orthogonal() {
444 Ok((h, g))
448 } else {
449 Ok((h, g))
452 }
453 }
454
455 fn extend_signal(&self, signal: &Array1<f64>) -> WaveletResult<Array1<f64>> {
457 if !signal.len().is_multiple_of(2) {
459 let mut padded = Array1::zeros(signal.len() + 1);
460 for i in 0..signal.len() {
461 padded[i] = signal[i];
462 }
463 padded[signal.len()] = signal[signal.len() - 1];
465 Ok(padded)
466 } else {
467 Ok(signal.clone())
468 }
469 }
470
471 fn convolve_downsample(
473 &self,
474 signal: &Array1<f64>,
475 filter: &Array1<f64>,
476 ) -> WaveletResult<Array1<f64>> {
477 let n = signal.len();
478 let filter_len = filter.len();
479 let output_len = n / 2; let mut result = Array1::zeros(output_len);
482
483 for i in 0..output_len {
484 let start_idx = 2 * i;
485 let mut sum = 0.0;
486
487 for j in 0..filter_len {
488 let signal_idx = start_idx + j;
489 if signal_idx < n {
490 sum += signal[signal_idx] * filter[j]; }
492 }
493
494 result[i] = sum;
495 }
496
497 if self.normalize {
498 result *= SQRT_2;
499 }
500
501 Ok(result)
502 }
503
504 fn upsample_convolve(
506 &self,
507 signal: &Array1<f64>,
508 filter: &Array1<f64>,
509 ) -> WaveletResult<Array1<f64>> {
510 let upsampled_len = 2 * signal.len();
511 let filter_len = filter.len();
512
513 let mut upsampled = Array1::zeros(upsampled_len);
515 for i in 0..signal.len() {
516 upsampled[2 * i] = signal[i];
517 }
518
519 let output_len = upsampled_len;
521 let mut result = Array1::zeros(output_len);
522
523 for i in 0..output_len {
524 let mut sum = 0.0;
525 for j in 0..filter_len {
526 let idx = if i >= j { i - j } else { continue };
527 if idx < upsampled_len {
528 sum += upsampled[idx] * filter[j];
529 }
530 }
531 result[i] = sum;
532 }
533
534 Ok(result)
535 }
536
537 fn validate_input(&self, signal: &Array1<f64>) -> WaveletResult<()> {
539 if signal.is_empty() {
540 return Err(WaveletError::InvalidInput(
541 "Signal cannot be empty".to_string(),
542 ));
543 }
544
545 for &val in signal.iter() {
546 if !val.is_finite() {
547 return Err(WaveletError::InvalidInput(
548 "Signal contains non-finite values".to_string(),
549 ));
550 }
551 }
552
553 let min_length = self.wavelet_type.filter_length();
554 if signal.len() < min_length {
555 return Err(WaveletError::InvalidInput(format!(
556 "Signal length ({}) must be at least filter length ({})",
557 signal.len(),
558 min_length
559 )));
560 }
561
562 Ok(())
563 }
564
565 pub fn extract_energy_features(&self, signal: &Array1<f64>) -> WaveletResult<Array1<f64>> {
567 let decomposition = self.dwt(signal)?;
568
569 let mut features = Vec::new();
570
571 for detail in &decomposition.details {
573 let energy: f64 = detail.iter().map(|x| x * x).sum();
574 features.push(energy);
575 }
576
577 let approx_energy: f64 = decomposition.approximation.iter().map(|x| x * x).sum();
579 features.push(approx_energy);
580
581 let total_energy: f64 = features.iter().sum();
583
584 if total_energy > 0.0 {
586 for feature in &mut features {
587 *feature /= total_energy;
588 }
589 }
590
591 features.push(total_energy); for i in 1..decomposition.details.len() {
596 let ratio = if features[i] > 0.0 {
597 features[i - 1] / features[i]
598 } else {
599 0.0
600 };
601 features.push(ratio);
602 }
603
604 Ok(Array1::from_vec(features))
605 }
606}
607
608impl Default for WaveletTransform {
609 fn default() -> Self {
610 Self::new()
611 }
612}
613
614#[derive(Debug, Clone)]
616pub struct WaveletDecomposition {
617 approximation: Array1<f64>,
619 details: Vec<Array1<f64>>,
621 wavelet_type: WaveletType,
623 boundary: WaveletBoundary,
625 levels_computed: usize,
627}
628
629impl WaveletDecomposition {
630 pub fn approximation(&self) -> &Array1<f64> {
632 &self.approximation
633 }
634
635 pub fn details(&self) -> &[Array1<f64>] {
637 &self.details
638 }
639
640 pub fn detail_level(&self, level: usize) -> Option<&Array1<f64>> {
642 self.details.get(level)
643 }
644
645 pub fn wavelet_type(&self) -> &WaveletType {
647 &self.wavelet_type
648 }
649
650 pub fn boundary(&self) -> WaveletBoundary {
652 self.boundary
653 }
654
655 pub fn levels_computed(&self) -> usize {
657 self.levels_computed
658 }
659
660 pub fn energy_distribution(&self) -> Array1<f64> {
662 let mut energies = Vec::new();
663
664 for detail in &self.details {
666 let energy: f64 = detail.iter().map(|x| x * x).sum();
667 energies.push(energy);
668 }
669
670 let approx_energy: f64 = self.approximation.iter().map(|x| x * x).sum();
672 energies.push(approx_energy);
673
674 let total_energy: f64 = energies.iter().sum();
675
676 if total_energy > 0.0 {
678 for energy in &mut energies {
679 *energy /= total_energy;
680 }
681 }
682
683 Array1::from_vec(energies)
684 }
685
686 pub fn soft_threshold(&mut self, threshold: f64) {
688 for detail in &mut self.details {
690 for coeff in detail.iter_mut() {
691 if coeff.abs() <= threshold {
692 *coeff = 0.0;
693 } else {
694 *coeff = coeff.signum() * (coeff.abs() - threshold);
695 }
696 }
697 }
698 }
699
700 pub fn hard_threshold(&mut self, threshold: f64) {
702 for detail in &mut self.details {
704 for coeff in detail.iter_mut() {
705 if coeff.abs() <= threshold {
706 *coeff = 0.0;
707 }
708 }
709 }
710 }
711
712 pub fn sparsity(&self) -> f64 {
714 let mut total_coeffs = 0;
715 let mut zero_coeffs = 0;
716
717 for detail in &self.details {
719 for &coeff in detail.iter() {
720 total_coeffs += 1;
721 if coeff.abs() < 1e-15 {
722 zero_coeffs += 1;
723 }
724 }
725 }
726
727 for &coeff in self.approximation.iter() {
729 total_coeffs += 1;
730 if coeff.abs() < 1e-15 {
731 zero_coeffs += 1;
732 }
733 }
734
735 if total_coeffs > 0 {
736 zero_coeffs as f64 / total_coeffs as f64
737 } else {
738 0.0
739 }
740 }
741
742 pub fn level_statistics(&self) -> Vec<HashMap<String, f64>> {
744 let mut stats = Vec::new();
745
746 for detail in &self.details {
748 let mut level_stats = HashMap::new();
749 let mean: f64 = detail.iter().sum::<f64>() / detail.len() as f64;
750 let variance: f64 =
751 detail.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / detail.len() as f64;
752 let std: f64 = variance.sqrt();
753 let max = detail.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
754 let min = detail.iter().fold(f64::INFINITY, |a, &b| a.min(b));
755 let energy: f64 = detail.iter().map(|x| x * x).sum();
756
757 level_stats.insert("mean".to_string(), mean);
758 level_stats.insert("std".to_string(), std);
759 level_stats.insert("min".to_string(), min);
760 level_stats.insert("max".to_string(), max);
761 level_stats.insert("energy".to_string(), energy);
762
763 stats.push(level_stats);
764 }
765
766 let mut approx_stats = HashMap::new();
768 let mean: f64 = self.approximation.iter().sum::<f64>() / self.approximation.len() as f64;
769 let variance: f64 = self
770 .approximation
771 .iter()
772 .map(|x| (x - mean).powi(2))
773 .sum::<f64>()
774 / self.approximation.len() as f64;
775 let std: f64 = variance.sqrt();
776 let max = self
777 .approximation
778 .iter()
779 .fold(f64::NEG_INFINITY, |a, &b| a.max(b));
780 let min = self
781 .approximation
782 .iter()
783 .fold(f64::INFINITY, |a, &b| a.min(b));
784 let energy: f64 = self.approximation.iter().map(|x| x * x).sum();
785
786 approx_stats.insert("mean".to_string(), mean);
787 approx_stats.insert("std".to_string(), std);
788 approx_stats.insert("min".to_string(), min);
789 approx_stats.insert("max".to_string(), max);
790 approx_stats.insert("energy".to_string(), energy);
791
792 stats.push(approx_stats);
793
794 stats
795 }
796}
797
798#[allow(non_snake_case)]
799#[cfg(test)]
800mod tests {
801 use super::*;
802 use scirs2_core::ndarray::array;
803 use std::f64::consts::PI;
804
805 fn generate_test_signal(n: usize) -> Array1<f64> {
807 Array1::from_iter((0..n).map(|i| {
808 let t = i as f64 / n as f64;
809 (2.0 * PI * 5.0 * t).sin() + 0.5 * (2.0 * PI * 20.0 * t).sin()
810 }))
811 }
812
813 fn generate_chirp_signal(n: usize) -> Array1<f64> {
814 Array1::from_iter((0..n).map(|i| {
815 let t = i as f64 / n as f64;
816 let freq = 1.0 + 10.0 * t; (2.0 * PI * freq * t).sin()
818 }))
819 }
820
821 #[test]
822 fn test_wavelet_type_properties() {
823 assert_eq!(WaveletType::Haar.filter_length(), 2);
824 assert_eq!(WaveletType::Daubechies4.filter_length(), 8);
825 assert!(WaveletType::Haar.is_orthogonal());
826 assert!(WaveletType::Daubechies4.is_orthogonal());
827 assert_eq!(WaveletType::Haar.name(), "Haar");
828 }
829
830 #[test]
831 fn test_boundary_condition_names() {
832 assert_eq!(WaveletBoundary::Zero.name(), "zero");
833 assert_eq!(WaveletBoundary::Symmetric.name(), "symmetric");
834 assert_eq!(WaveletBoundary::Periodic.name(), "periodic");
835 assert_eq!(WaveletBoundary::Constant.name(), "constant");
836 }
837
838 #[test]
839 fn test_haar_wavelet_transform() {
840 let signal = array![1.0, 2.0, 3.0, 4.0, 3.0, 2.0, 1.0, 0.0];
841 let wavelet = WaveletTransform::new()
842 .wavelet_type(WaveletType::Haar)
843 .levels(2);
844
845 let result = wavelet.dwt(&signal).expect("operation should succeed");
846 assert_eq!(result.levels_computed(), 2);
847 assert!(!result.approximation().is_empty());
848 assert_eq!(result.details().len(), 2);
849
850 let reconstructed = wavelet.idwt(&result).expect("operation should succeed");
852
853 let min_len = signal.len().min(reconstructed.len());
856 for i in 0..min_len {
857 let diff = (signal[i] - reconstructed[i]).abs();
858 assert!(
859 diff < 1e-10,
860 "Reconstruction error at index {}: {}",
861 i,
862 diff
863 );
864 }
865 }
866
867 #[test]
868 fn test_daubechies_wavelets() {
869 let signal = generate_test_signal(64);
870
871 let wavelets = vec![
872 WaveletType::Daubechies2,
873 WaveletType::Daubechies4,
874 WaveletType::Daubechies8,
875 ];
876
877 for wavelet_type in &wavelets {
878 let wavelet = WaveletTransform::new()
879 .wavelet_type(wavelet_type.clone())
880 .levels(3);
881
882 let result = wavelet.dwt(&signal).expect("operation should succeed");
883 assert!(result.levels_computed() > 0);
884
885 let original_energy: f64 = signal.iter().map(|x| x * x).sum();
887 let mut decomp_energy = result.approximation().iter().map(|x| x * x).sum::<f64>();
888 for detail in result.details() {
889 decomp_energy += detail.iter().map(|x| x * x).sum::<f64>();
890 }
891
892 let energy_ratio = decomp_energy / original_energy;
893 assert!(
895 energy_ratio > 0.1 && energy_ratio < 50.0,
896 "Energy ratio should be in reasonable range for {:?}: ratio = {}",
897 wavelet_type,
898 energy_ratio
899 );
900 }
901 }
902
903 #[test]
904 fn test_boundary_conditions() {
905 let signal = generate_test_signal(32);
906
907 let boundaries = vec![
908 WaveletBoundary::Zero,
909 WaveletBoundary::Symmetric,
910 WaveletBoundary::Periodic,
911 WaveletBoundary::Constant,
912 ];
913
914 for boundary in boundaries {
915 let wavelet = WaveletTransform::new()
916 .wavelet_type(WaveletType::Daubechies4)
917 .boundary(boundary)
918 .levels(3);
919
920 let result = wavelet.dwt(&signal).expect("operation should succeed");
921 assert!(result.levels_computed() > 0);
922
923 let reconstructed = wavelet.idwt(&result).expect("operation should succeed");
924 assert!(!reconstructed.is_empty());
925 }
926 }
927
928 #[test]
929 fn test_perfect_reconstruction() {
930 let signal = array![1.0, 4.0, 2.0, 8.0, 3.0, 6.0, 1.0, 5.0];
931
932 let wavelet = WaveletTransform::new()
933 .wavelet_type(WaveletType::Haar)
934 .boundary(WaveletBoundary::Periodic)
935 .levels(3);
936
937 let decomposition = wavelet.dwt(&signal).expect("operation should succeed");
938 let reconstructed = wavelet
939 .idwt(&decomposition)
940 .expect("operation should succeed");
941
942 let reconstruction_error = signal
945 .iter()
946 .zip(reconstructed.iter())
947 .map(|(a, b)| (a - b).abs())
948 .fold(0.0, f64::max);
949
950 assert!(
951 reconstruction_error < 1e-10,
952 "Perfect reconstruction failed: max error = {}",
953 reconstruction_error
954 );
955 }
956
957 #[test]
958 fn test_energy_features() {
959 let signal = generate_chirp_signal(128);
960
961 let wavelet = WaveletTransform::new()
962 .wavelet_type(WaveletType::Daubechies4)
963 .levels(4);
964
965 let features = wavelet
966 .extract_energy_features(&signal)
967 .expect("operation should succeed");
968
969 assert!(features.len() > 4); for &feature in features.iter() {
973 assert!(
974 feature >= 0.0,
975 "Energy feature should be non-negative: {}",
976 feature
977 );
978 assert!(
979 feature.is_finite(),
980 "Energy feature should be finite: {}",
981 feature
982 );
983 }
984 }
985
986 #[test]
987 fn test_wavelet_decomposition_methods() {
988 let signal = generate_test_signal(64);
989
990 let wavelet = WaveletTransform::new()
991 .wavelet_type(WaveletType::Daubechies4)
992 .levels(3);
993
994 let mut decomposition = wavelet.dwt(&signal).expect("operation should succeed");
995
996 let energy_dist = decomposition.energy_distribution();
998 let total_energy: f64 = energy_dist.sum();
999 assert!(
1000 (total_energy - 1.0).abs() < 1e-10,
1001 "Energy distribution should sum to 1"
1002 );
1003
1004 let original_sparsity = decomposition.sparsity();
1006 decomposition.soft_threshold(0.1);
1007 let new_sparsity = decomposition.sparsity();
1008 assert!(
1009 new_sparsity >= original_sparsity,
1010 "Sparsity should increase after thresholding"
1011 );
1012
1013 let stats = decomposition.level_statistics();
1015 assert_eq!(stats.len(), decomposition.levels_computed() + 1); for level_stats in &stats {
1018 assert!(level_stats.contains_key("mean"));
1019 assert!(level_stats.contains_key("std"));
1020 assert!(level_stats.contains_key("energy"));
1021 }
1022 }
1023
1024 #[test]
1025 fn test_error_handling() {
1026 let wavelet = WaveletTransform::new();
1027
1028 let empty_signal = Array1::zeros(0);
1030 assert!(wavelet.dwt(&empty_signal).is_err());
1031
1032 let bad_signal = array![1.0, 2.0, f64::NAN, 4.0];
1034 assert!(wavelet.dwt(&bad_signal).is_err());
1035
1036 let short_signal = array![1.0];
1038 assert!(wavelet.dwt(&short_signal).is_err());
1039 }
1040
1041 #[test]
1042 fn test_coiflets_wavelets() {
1043 let signal = generate_test_signal(64);
1044
1045 let wavelet = WaveletTransform::new()
1046 .wavelet_type(WaveletType::Coiflets2)
1047 .levels(2);
1048
1049 let result = wavelet.dwt(&signal).expect("operation should succeed");
1050 assert!(result.levels_computed() > 0);
1051
1052 let energy_dist = result.energy_distribution();
1054 assert!(energy_dist.iter().all(|&x| x.is_finite()));
1055 }
1056
1057 #[test]
1058 fn test_wavelet_properties_consistency() {
1059 let wavelets = vec![
1061 WaveletType::Haar,
1062 WaveletType::Daubechies2,
1063 WaveletType::Daubechies4,
1064 WaveletType::Coiflets2,
1065 ];
1066
1067 let signal = generate_test_signal(32);
1068
1069 for wavelet_type in &wavelets {
1070 let wavelet = WaveletTransform::new()
1071 .wavelet_type(wavelet_type.clone())
1072 .levels(2);
1073
1074 let result = wavelet.dwt(&signal);
1075 assert!(result.is_ok(), "Wavelet {:?} should work", wavelet_type);
1076
1077 let decomposition = result.expect("operation should succeed");
1078 assert_eq!(decomposition.wavelet_type(), wavelet_type);
1079
1080 let reconstructed = wavelet.idwt(&decomposition);
1082 assert!(
1083 reconstructed.is_ok(),
1084 "Reconstruction should work for {:?}",
1085 wavelet_type
1086 );
1087 }
1088 }
1089
1090 #[test]
1091 fn test_multi_level_decomposition() {
1092 let signal = generate_test_signal(256);
1093
1094 for levels in 1..6 {
1095 let wavelet = WaveletTransform::new()
1096 .wavelet_type(WaveletType::Daubechies4)
1097 .levels(levels);
1098
1099 let result = wavelet.dwt(&signal).expect("operation should succeed");
1100
1101 assert!(result.levels_computed() <= levels);
1103 assert!(result.levels_computed() > 0);
1104
1105 for detail in result.details() {
1108 assert!(
1109 !detail.is_empty(),
1110 "Detail coefficients should not be empty"
1111 );
1112 assert!(
1113 detail.len() <= signal.len(),
1114 "Detail length should not exceed original signal"
1115 );
1116 }
1117 }
1118 }
1119
1120 #[test]
1121 fn test_normalization_effects() {
1122 let signal = array![1.0, 2.0, 3.0, 4.0, 3.0, 2.0, 1.0, 0.0];
1123
1124 let wavelet_normalized = WaveletTransform::new()
1125 .wavelet_type(WaveletType::Haar)
1126 .normalize(true)
1127 .levels(2);
1128
1129 let wavelet_unnormalized = WaveletTransform::new()
1130 .wavelet_type(WaveletType::Haar)
1131 .normalize(false)
1132 .levels(2);
1133
1134 let result_norm = wavelet_normalized
1135 .dwt(&signal)
1136 .expect("operation should succeed");
1137 let result_unnorm = wavelet_unnormalized
1138 .dwt(&signal)
1139 .expect("operation should succeed");
1140
1141 assert_eq!(
1143 result_norm.levels_computed(),
1144 result_unnorm.levels_computed()
1145 );
1146
1147 let norm_energy = result_norm
1149 .approximation()
1150 .iter()
1151 .map(|x| x * x)
1152 .sum::<f64>();
1153 let unnorm_energy = result_unnorm
1154 .approximation()
1155 .iter()
1156 .map(|x| x * x)
1157 .sum::<f64>();
1158
1159 assert!(
1160 (norm_energy - unnorm_energy).abs() > 1e-10,
1161 "Normalization should affect coefficient magnitudes"
1162 );
1163 }
1164}