1pub mod filters;
37pub mod regression;
38
39use crate::error::FdarError;
40use filters::FilterBank;
41
42#[derive(Debug, Clone, PartialEq)]
48#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
49#[non_exhaustive]
50pub enum WaveletFamily {
51 Haar,
53 Daubechies(usize),
55}
56
57impl WaveletFamily {
58 pub fn from_db_order(order: usize) -> Result<Self, FdarError> {
66 match order {
67 1 => Ok(WaveletFamily::Haar),
68 2..=10 => Ok(WaveletFamily::Daubechies(order)),
69 _ => Err(FdarError::InvalidParameter {
70 parameter: "order",
71 message: format!(
72 "Daubechies order {order} out of range: supported orders are 1..=10 (1 == Haar)"
73 ),
74 }),
75 }
76 }
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
84#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
85#[non_exhaustive]
86pub enum BoundaryMode {
87 #[default]
89 Periodic,
90 Symmetric,
92}
93
94fn extended_len(n: usize, mode: BoundaryMode) -> usize {
108 match mode {
109 BoundaryMode::Periodic => {
110 if n % 2 == 0 {
111 n
112 } else {
113 n + 1
114 }
115 }
116 BoundaryMode::Symmetric => 2 * n,
117 }
118}
119
120fn extend_signal(signal: &[f64], mode: BoundaryMode) -> Vec<f64> {
122 let n = signal.len();
123 let m = extended_len(n, mode);
124 let mut ext = vec![0.0_f64; m];
125 match mode {
126 BoundaryMode::Periodic => {
127 ext[..n].copy_from_slice(signal);
128 if m > n {
129 ext[n] = signal[n - 1];
131 }
132 }
133 BoundaryMode::Symmetric => {
134 for i in 0..n {
135 ext[i] = signal[i];
136 ext[m - 1 - i] = signal[i];
137 }
138 }
139 }
140 ext
141}
142
143#[inline]
145fn coeff_len(n: usize, mode: BoundaryMode) -> usize {
146 extended_len(n, mode) / 2
147}
148
149fn core_analysis(sig: &[f64], fb: &FilterBank) -> (Vec<f64>, Vec<f64>) {
154 let m = sig.len();
155 debug_assert!(m % 2 == 0 && m > 0);
156 let l = fb.filter_len();
157 let out = m / 2;
158 let mut approx = vec![0.0_f64; out];
159 let mut detail = vec![0.0_f64; out];
160 for t in 0..out {
161 let mut a = 0.0_f64;
162 let mut d = 0.0_f64;
163 for k in 0..l {
164 let idx = (2 * t + k) % m;
165 let s = sig[idx];
166 a += fb.dec_lo[k] * s;
167 d += fb.dec_hi[k] * s;
168 }
169 approx[t] = a;
170 detail[t] = d;
171 }
172 (approx, detail)
173}
174
175fn core_synthesis(approx: &[f64], detail: &[f64], fb: &FilterBank, m: usize) -> Vec<f64> {
181 let l = fb.filter_len();
182 let out = approx.len();
183 let mut signal = vec![0.0_f64; m];
184 for t in 0..out {
185 let a = approx[t];
186 let d = detail[t];
187 for k in 0..l {
188 let idx = (2 * t + k) % m;
189 signal[idx] += fb.dec_lo[k] * a + fb.dec_hi[k] * d;
190 }
191 }
192 signal
193}
194
195#[must_use = "the approximation/detail coefficients are the result of the transform"]
207pub(crate) fn single_level_analysis(
208 signal: &[f64],
209 fb: &FilterBank,
210 mode: BoundaryMode,
211) -> Result<(Vec<f64>, Vec<f64>), FdarError> {
212 if signal.is_empty() {
213 return Err(FdarError::InvalidParameter {
214 parameter: "signal",
215 message: "signal must be non-empty".to_string(),
216 });
217 }
218 let ext = extend_signal(signal, mode);
219 Ok(core_analysis(&ext, fb))
220}
221
222#[must_use = "the reconstructed signal is the result of the inverse transform"]
245pub(crate) fn single_level_synthesis(
246 approx: &[f64],
247 detail: &[f64],
248 fb: &FilterBank,
249 mode: BoundaryMode,
250 output_len: usize,
251) -> Result<Vec<f64>, FdarError> {
252 if output_len == 0 {
253 return Err(FdarError::InvalidParameter {
254 parameter: "output_len",
255 message: "output_len must be non-zero".to_string(),
256 });
257 }
258 if approx.len() != detail.len() {
259 return Err(FdarError::InvalidDimension {
260 parameter: "detail",
261 expected: format!("{} (== approx length)", approx.len()),
262 actual: detail.len().to_string(),
263 });
264 }
265 let expected_coeff_len = coeff_len(output_len, mode);
266 if approx.len() != expected_coeff_len {
267 return Err(FdarError::InvalidDimension {
268 parameter: "approx",
269 expected: format!("{expected_coeff_len} (mode-dependent coefficient length)"),
270 actual: approx.len().to_string(),
271 });
272 }
273 let m = extended_len(output_len, mode);
274 debug_assert!(
280 output_len <= m,
281 "output_len {output_len} > extended length {m}; likely off-by-one in odd-signal path"
282 );
283 let mut signal = core_synthesis(approx, detail, fb, m);
284 signal.truncate(output_len);
285 Ok(signal)
286}
287
288pub fn max_level(signal_len: usize, family: &WaveletFamily) -> Result<usize, FdarError> {
304 if signal_len == 0 {
305 return Err(FdarError::InvalidParameter {
306 parameter: "signal_len",
307 message: "signal length must be non-zero".to_string(),
308 });
309 }
310 let fb = filters::filter_bank(family)?;
311 let filter_len = fb.filter_len();
312 if filter_len <= 1 {
314 return Err(FdarError::InvalidParameter {
315 parameter: "family",
316 message: "filter length must exceed 1".to_string(),
317 });
318 }
319 let ratio = signal_len as f64 / (filter_len - 1) as f64;
320 let level = if ratio < 1.0 {
322 0
323 } else {
324 ratio.log2().floor() as usize
325 };
326 if level < 1 {
327 return Err(FdarError::InvalidParameter {
328 parameter: "signal_len",
329 message: format!(
330 "signal length {signal_len} is too short for even one useful decomposition level \
331 with filter length {filter_len}"
332 ),
333 });
334 }
335 Ok(level)
336}
337
338#[derive(Debug, Clone, PartialEq)]
348#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
349#[non_exhaustive]
350pub struct WaveletCoeffs {
351 pub approx: Vec<f64>,
353 pub details: Vec<Vec<f64>>,
355 pub levels: usize,
357 pub signal_len: usize,
359 pub family: WaveletFamily,
361 pub mode: BoundaryMode,
363 pub(crate) level_lens: Vec<usize>,
369}
370
371impl WaveletCoeffs {
372 #[must_use]
374 pub fn levels(&self) -> usize {
375 self.levels
376 }
377
378 #[must_use]
380 pub fn signal_len(&self) -> usize {
381 self.signal_len
382 }
383
384 #[must_use]
386 pub fn approx(&self) -> &[f64] {
387 &self.approx
388 }
389
390 #[must_use]
394 pub fn detail(&self, level: usize) -> Option<&[f64]> {
395 self.details.get(level).map(Vec::as_slice)
396 }
397
398 #[must_use]
400 pub fn family(&self) -> &WaveletFamily {
401 &self.family
402 }
403
404 #[must_use]
406 pub fn mode(&self) -> BoundaryMode {
407 self.mode
408 }
409}
410
411#[must_use = "the coefficient pyramid is the result of the transform"]
422pub fn decompose(
423 signal: &[f64],
424 family: WaveletFamily,
425 mode: BoundaryMode,
426 level: Option<usize>,
427) -> Result<WaveletCoeffs, FdarError> {
428 if signal.is_empty() {
429 return Err(FdarError::InvalidParameter {
430 parameter: "signal",
431 message: "signal must be non-empty".to_string(),
432 });
433 }
434 let max_lvl = max_level(signal.len(), &family)?;
435 let effective_level = match level {
436 None => max_lvl,
437 Some(0) => {
438 return Err(FdarError::InvalidParameter {
439 parameter: "level",
440 message: "decomposition level must be at least 1".to_string(),
441 });
442 }
443 Some(l) if l > max_lvl => {
444 return Err(FdarError::InvalidParameter {
445 parameter: "level",
446 message: format!(
447 "decomposition level {l} exceeds the maximum useful level {max_lvl} \
448 for signal length {} with this family",
449 signal.len()
450 ),
451 });
452 }
453 Some(l) => l,
454 };
455
456 let fb = filters::filter_bank(&family)?;
457 let mut details: Vec<Vec<f64>> = Vec::with_capacity(effective_level);
458 let mut level_lens: Vec<usize> = Vec::with_capacity(effective_level);
459 let mut current = signal.to_vec();
460 for _ in 0..effective_level {
461 level_lens.push(current.len());
462 let (approx, detail) = single_level_analysis(¤t, &fb, mode)?;
463 details.push(detail);
464 current = approx;
465 }
466
467 Ok(WaveletCoeffs {
468 approx: current,
469 details,
470 levels: effective_level,
471 signal_len: signal.len(),
472 family,
473 mode,
474 level_lens,
475 })
476}
477
478#[must_use = "the reconstructed signal is the result of the inverse transform"]
490pub fn reconstruct(coeffs: &WaveletCoeffs) -> Result<Vec<f64>, FdarError> {
491 if coeffs.details.len() != coeffs.levels || coeffs.level_lens.len() != coeffs.levels {
492 return Err(FdarError::InvalidDimension {
493 parameter: "coeffs",
494 expected: format!("{} detail bands and level lengths", coeffs.levels),
495 actual: format!(
496 "{} detail bands, {} level lengths",
497 coeffs.details.len(),
498 coeffs.level_lens.len()
499 ),
500 });
501 }
502 if coeffs.levels == 0 {
503 return Err(FdarError::InvalidDimension {
504 parameter: "levels",
505 expected: "at least 1".to_string(),
506 actual: "0".to_string(),
507 });
508 }
509 let fb = filters::filter_bank(&coeffs.family)?;
510 let mut approx = coeffs.approx.clone();
511 for lvl in (0..coeffs.levels).rev() {
513 let detail = &coeffs.details[lvl];
514 let target_len = coeffs.level_lens[lvl];
515 approx = single_level_synthesis(&approx, detail, &fb, coeffs.mode, target_len)?;
516 }
517 Ok(approx)
518}
519
520#[must_use = "the per-row coefficient pyramids are the result of the transform"]
532pub fn decompose_matrix(
533 data: &crate::matrix::FdMatrix,
534 family: WaveletFamily,
535 mode: BoundaryMode,
536 level: Option<usize>,
537) -> Result<Vec<WaveletCoeffs>, FdarError> {
538 if data.nrows() == 0 || data.ncols() == 0 {
539 return Err(FdarError::InvalidDimension {
540 parameter: "data",
541 expected: "non-empty matrix (nrows > 0 && ncols > 0)".to_string(),
542 actual: format!("{}x{}", data.nrows(), data.ncols()),
543 });
544 }
545 let mut out = Vec::with_capacity(data.nrows());
546 for i in 0..data.nrows() {
547 let row = data.row(i);
548 out.push(decompose(&row, family.clone(), mode, level)?);
549 }
550 Ok(out)
551}
552
553#[cfg(test)]
554mod tests {
555 use super::*;
556 use crate::wavelet::filters::filter_bank;
557
558 fn pseudo_random(n: usize, seed: u64) -> Vec<f64> {
560 let mut state = seed.wrapping_add(0x9E37_79B9_7F4A_7C15);
561 (0..n)
562 .map(|_| {
563 state = state
564 .wrapping_mul(6_364_136_223_846_793_005)
565 .wrapping_add(1_442_695_040_888_963_407);
566 let u = (state >> 11) as f64 / (1u64 << 53) as f64;
568 2.0 * u - 1.0
569 })
570 .collect()
571 }
572
573 fn rel_err(recon: &[f64], orig: &[f64]) -> f64 {
574 let num: f64 = recon
575 .iter()
576 .zip(orig)
577 .map(|(r, o)| (r - o) * (r - o))
578 .sum::<f64>()
579 .sqrt();
580 let den: f64 = orig.iter().map(|o| o * o).sum::<f64>().sqrt().max(1e-300);
581 num / den
582 }
583
584 fn round_trip_families() -> Vec<WaveletFamily> {
585 vec![
586 WaveletFamily::Haar,
587 WaveletFamily::Daubechies(2),
588 WaveletFamily::Daubechies(4),
589 WaveletFamily::Daubechies(6),
590 WaveletFamily::Daubechies(8),
591 WaveletFamily::Daubechies(10),
592 ]
593 }
594
595 #[test]
598 fn haar_known_answer_coefficients() {
599 let a = 1.5_f64;
601 let b = -0.5;
602 let c = 3.0;
603 let d = 2.0;
604 let signal = [a, b, c, d];
605 let fb = filter_bank(&WaveletFamily::Haar).unwrap();
606 let (approx, detail) = single_level_analysis(&signal, &fb, BoundaryMode::Periodic).unwrap();
607 let s2 = std::f64::consts::SQRT_2;
608 assert!((approx[0] - (a + b) / s2).abs() < 1e-12);
609 assert!((approx[1] - (c + d) / s2).abs() < 1e-12);
610 assert!((detail[0] - (a - b) / s2).abs() < 1e-12);
611 assert!((detail[1] - (c - d) / s2).abs() < 1e-12);
612 }
613
614 #[test]
615 fn haar_even_length_round_trip() {
616 let signal = pseudo_random(8, 42);
617 let fb = filter_bank(&WaveletFamily::Haar).unwrap();
618 let (approx, detail) = single_level_analysis(&signal, &fb, BoundaryMode::Periodic).unwrap();
619 let recon =
620 single_level_synthesis(&approx, &detail, &fb, BoundaryMode::Periodic, signal.len())
621 .unwrap();
622 assert!(rel_err(&recon, &signal) < 1e-10);
623 }
624
625 #[test]
626 fn empty_signal_is_invalid_parameter() {
627 let fb = filter_bank(&WaveletFamily::Haar).unwrap();
628 assert!(matches!(
629 single_level_analysis(&[], &fb, BoundaryMode::Periodic),
630 Err(FdarError::InvalidParameter { .. })
631 ));
632 }
633
634 #[test]
637 fn round_trip_periodic_non_power_of_two() {
638 let signal = pseudo_random(37, 7);
639 for fam in round_trip_families() {
640 let fb = filter_bank(&fam).unwrap();
641 let (approx, detail) =
642 single_level_analysis(&signal, &fb, BoundaryMode::Periodic).unwrap();
643 let recon =
644 single_level_synthesis(&approx, &detail, &fb, BoundaryMode::Periodic, signal.len())
645 .unwrap();
646 let e = rel_err(&recon, &signal);
647 assert!(e < 1e-10, "{fam:?} periodic n=37 rel err {e}");
648 }
649 }
650
651 #[test]
652 fn round_trip_symmetric_non_power_of_two() {
653 let signal = pseudo_random(37, 11);
654 for fam in round_trip_families() {
655 let fb = filter_bank(&fam).unwrap();
656 let (approx, detail) =
657 single_level_analysis(&signal, &fb, BoundaryMode::Symmetric).unwrap();
658 let recon = single_level_synthesis(
659 &approx,
660 &detail,
661 &fb,
662 BoundaryMode::Symmetric,
663 signal.len(),
664 )
665 .unwrap();
666 let e = rel_err(&recon, &signal);
667 assert!(e < 1e-10, "{fam:?} symmetric n=37 rel err {e}");
668 }
669 }
670
671 #[test]
672 fn coefficient_lengths_are_ceil_half() {
673 let fb = filter_bank(&WaveletFamily::Daubechies(4)).unwrap();
674 for n in [36_usize, 37] {
675 let signal = pseudo_random(n, 3);
676 let (approx, detail) =
677 single_level_analysis(&signal, &fb, BoundaryMode::Periodic).unwrap();
678 assert_eq!(approx.len(), n.div_ceil(2));
679 assert_eq!(detail.len(), n.div_ceil(2));
680 let recon =
681 single_level_synthesis(&approx, &detail, &fb, BoundaryMode::Periodic, n).unwrap();
682 assert_eq!(recon.len(), n);
683 }
684 }
685
686 #[test]
687 fn reconstruction_has_no_nan_or_inf() {
688 for &n in &[36_usize, 37] {
689 let signal = pseudo_random(n, 99);
690 for fam in round_trip_families() {
691 let fb = filter_bank(&fam).unwrap();
692 for mode in [BoundaryMode::Periodic, BoundaryMode::Symmetric] {
693 let (approx, detail) = single_level_analysis(&signal, &fb, mode).unwrap();
694 let recon = single_level_synthesis(&approx, &detail, &fb, mode, n).unwrap();
695 assert!(
696 recon.iter().all(|x| x.is_finite()),
697 "{fam:?} {mode:?} n={n} produced non-finite"
698 );
699 }
700 }
701 }
702 }
703
704 #[test]
705 fn db4_even_and_odd_round_trip_both_modes() {
706 let fb = filter_bank(&WaveletFamily::Daubechies(4)).unwrap();
707 for n in [36_usize, 37] {
708 let signal = pseudo_random(n, 5);
709 for mode in [BoundaryMode::Periodic, BoundaryMode::Symmetric] {
710 let (approx, detail) = single_level_analysis(&signal, &fb, mode).unwrap();
711 let recon = single_level_synthesis(&approx, &detail, &fb, mode, n).unwrap();
712 let e = rel_err(&recon, &signal);
713 assert!(e < 1e-10, "db4 n={n} {mode:?} rel err {e}");
714 }
715 }
716 }
717
718 #[test]
719 fn odd_order_daubechies_round_trip_both_modes() {
720 let n = 37_usize; let signal = pseudo_random(n, 202);
727 for order in [3_usize, 5, 7, 9] {
728 let fam = WaveletFamily::from_db_order(order).unwrap();
729 let fb = filter_bank(&fam).unwrap();
730 for mode in [BoundaryMode::Periodic, BoundaryMode::Symmetric] {
731 let (approx, detail) = single_level_analysis(&signal, &fb, mode).unwrap();
732 let recon = single_level_synthesis(&approx, &detail, &fb, mode, n).unwrap();
733 let e = rel_err(&recon, &signal);
734 assert!(e < 1e-10, "db{order} n={n} {mode:?} rel err {e}");
735 assert!(recon.iter().all(|x| x.is_finite()));
736 }
737 }
738 }
739
740 #[test]
741 fn odd_order_daubechies_multi_level_round_trip_both_modes() {
742 let n = 201_usize;
744 let signal = pseudo_random(n, 303);
745 for order in [3_usize, 5, 7, 9] {
746 let fam = WaveletFamily::from_db_order(order).unwrap();
747 for mode in [BoundaryMode::Periodic, BoundaryMode::Symmetric] {
748 let coeffs = decompose(&signal, fam.clone(), mode, None).unwrap();
749 let recon = reconstruct(&coeffs).unwrap();
750 assert_eq!(recon.len(), n);
751 let e = rel_err(&recon, &signal);
752 assert!(
753 e < 1e-10,
754 "db{order} n={n} {mode:?} multi-level rel err {e}"
755 );
756 assert!(recon.iter().all(|x| x.is_finite()));
757 }
758 }
759 }
760
761 #[test]
762 fn synthesis_rejects_mismatched_coefficient_lengths() {
763 let fb = filter_bank(&WaveletFamily::Haar).unwrap();
764 let approx = vec![1.0, 2.0];
765 let detail = vec![1.0];
766 assert!(matches!(
767 single_level_synthesis(&approx, &detail, &fb, BoundaryMode::Periodic, 4),
768 Err(FdarError::InvalidDimension { .. })
769 ));
770 }
771
772 #[test]
773 fn synthesis_rejects_zero_output_len() {
774 let fb = filter_bank(&WaveletFamily::Haar).unwrap();
775 assert!(matches!(
776 single_level_synthesis(&[], &[], &fb, BoundaryMode::Periodic, 0),
777 Err(FdarError::InvalidParameter { .. })
778 ));
779 }
780
781 #[test]
782 fn from_db_order_maps_correctly() {
783 assert_eq!(
784 WaveletFamily::from_db_order(1).unwrap(),
785 WaveletFamily::Haar
786 );
787 assert_eq!(
788 WaveletFamily::from_db_order(2).unwrap(),
789 WaveletFamily::Daubechies(2)
790 );
791 assert_eq!(
792 WaveletFamily::from_db_order(10).unwrap(),
793 WaveletFamily::Daubechies(10)
794 );
795 assert!(matches!(
796 WaveletFamily::from_db_order(0),
797 Err(FdarError::InvalidParameter { .. })
798 ));
799 assert!(matches!(
800 WaveletFamily::from_db_order(11),
801 Err(FdarError::InvalidParameter { .. })
802 ));
803 }
804
805 #[test]
806 fn default_boundary_mode_is_periodic() {
807 assert_eq!(BoundaryMode::default(), BoundaryMode::Periodic);
808 }
809
810 #[test]
813 fn max_level_haar_is_log2_of_n() {
814 assert_eq!(max_level(1024, &WaveletFamily::Haar).unwrap(), 10);
816 }
817
818 #[test]
819 fn max_level_db4_uses_filter_len_minus_one() {
820 let expected = (1024.0_f64 / 7.0).log2().floor() as usize;
822 assert_eq!(
823 max_level(1024, &WaveletFamily::Daubechies(4)).unwrap(),
824 expected
825 );
826 assert_eq!(expected, 7);
827 }
828
829 #[test]
830 fn max_level_rejects_zero_length() {
831 assert!(matches!(
832 max_level(0, &WaveletFamily::Haar),
833 Err(FdarError::InvalidParameter { .. })
834 ));
835 }
836
837 #[test]
838 fn max_level_rejects_too_short_signal() {
839 assert!(matches!(
841 max_level(1, &WaveletFamily::Haar),
842 Err(FdarError::InvalidParameter { .. })
843 ));
844 assert!(matches!(
846 max_level(6, &WaveletFamily::Daubechies(4)),
847 Err(FdarError::InvalidParameter { .. })
848 ));
849 }
850
851 #[test]
852 fn max_level_rejects_unsupported_family() {
853 assert!(matches!(
854 max_level(1024, &WaveletFamily::Daubechies(11)),
855 Err(FdarError::InvalidParameter { .. })
856 ));
857 }
858
859 #[test]
860 fn wavelet_coeffs_partial_eq_on_identical_inputs() {
861 let signal = pseudo_random(64, 21);
862 let a = decompose(
863 &signal,
864 WaveletFamily::Daubechies(4),
865 BoundaryMode::Periodic,
866 Some(3),
867 )
868 .unwrap();
869 let b = decompose(
870 &signal,
871 WaveletFamily::Daubechies(4),
872 BoundaryMode::Periodic,
873 Some(3),
874 )
875 .unwrap();
876 assert_eq!(a, b);
877 assert_eq!(a.levels(), 3);
879 assert_eq!(a.signal_len(), 64);
880 assert_eq!(a.detail(0).unwrap().len(), a.details[0].len());
881 assert!(a.detail(3).is_none());
882 assert_eq!(a.family(), &WaveletFamily::Daubechies(4));
883 assert_eq!(a.mode(), BoundaryMode::Periodic);
884 }
885
886 #[test]
889 fn multi_level_round_trip_periodic_all_families() {
890 let signal = pseudo_random(256, 123);
893 for fam in round_trip_families() {
894 let coeffs = decompose(&signal, fam.clone(), BoundaryMode::Periodic, Some(2)).unwrap();
895 assert_eq!(coeffs.levels, 2);
896 let recon = reconstruct(&coeffs).unwrap();
897 assert_eq!(recon.len(), signal.len());
898 let e = rel_err(&recon, &signal);
899 assert!(e < 1e-10, "{fam:?} periodic L=2 rel err {e}");
900 assert!(recon.iter().all(|x| x.is_finite()));
901 }
902 }
903
904 #[test]
905 fn multi_level_round_trip_symmetric_non_power_of_two() {
906 let signal = pseudo_random(201, 456);
909 for fam in round_trip_families() {
910 let coeffs = decompose(&signal, fam.clone(), BoundaryMode::Symmetric, None).unwrap();
911 assert!(
912 coeffs.levels >= 2,
913 "{fam:?} expected >=2 auto levels for n=201"
914 );
915 let recon = reconstruct(&coeffs).unwrap();
916 assert_eq!(recon.len(), 201);
917 let e = rel_err(&recon, &signal);
918 assert!(e < 1e-10, "{fam:?} symmetric n=201 auto rel err {e}");
919 assert!(recon.iter().all(|x| x.is_finite()));
920 }
921 }
922
923 #[test]
924 fn multi_level_round_trip_periodic_non_power_of_two() {
925 let signal = pseudo_random(201, 789);
927 for fam in round_trip_families() {
928 let coeffs = decompose(&signal, fam.clone(), BoundaryMode::Periodic, None).unwrap();
929 assert!(coeffs.levels >= 2, "{fam:?} expected >=2 auto levels");
930 let recon = reconstruct(&coeffs).unwrap();
931 assert_eq!(recon.len(), 201);
932 let e = rel_err(&recon, &signal);
933 assert!(e < 1e-10, "{fam:?} periodic n=201 rel err {e}");
934 }
935 }
936
937 #[test]
938 fn auto_level_equals_max_level() {
939 let signal = pseudo_random(200, 8);
940 let coeffs = decompose(
941 &signal,
942 WaveletFamily::Daubechies(4),
943 BoundaryMode::Periodic,
944 None,
945 )
946 .unwrap();
947 assert_eq!(
948 coeffs.levels,
949 max_level(200, &WaveletFamily::Daubechies(4)).unwrap()
950 );
951 }
952
953 #[test]
954 fn explicit_level_out_of_range_is_invalid() {
955 let signal = pseudo_random(64, 8);
956 let maxl = max_level(64, &WaveletFamily::Daubechies(4)).unwrap();
957 assert!(matches!(
958 decompose(
959 &signal,
960 WaveletFamily::Daubechies(4),
961 BoundaryMode::Periodic,
962 Some(maxl + 1)
963 ),
964 Err(FdarError::InvalidParameter { .. })
965 ));
966 assert!(matches!(
967 decompose(
968 &signal,
969 WaveletFamily::Daubechies(4),
970 BoundaryMode::Periodic,
971 Some(0)
972 ),
973 Err(FdarError::InvalidParameter { .. })
974 ));
975 }
976
977 #[test]
978 fn decompose_rejects_empty_signal() {
979 assert!(matches!(
980 decompose(&[], WaveletFamily::Haar, BoundaryMode::Periodic, None),
981 Err(FdarError::InvalidParameter { .. })
982 ));
983 }
984
985 #[test]
986 fn signal_len_preserved_odd_and_even() {
987 for &n in &[37_usize, 64] {
988 let signal = pseudo_random(n, n as u64);
989 let coeffs = decompose(
990 &signal,
991 WaveletFamily::Daubechies(2),
992 BoundaryMode::Periodic,
993 Some(2),
994 )
995 .unwrap();
996 let recon = reconstruct(&coeffs).unwrap();
997 assert_eq!(recon.len(), n, "n={n}");
998 }
999 }
1000
1001 #[test]
1002 fn reconstruct_rejects_inconsistent_coeffs() {
1003 let signal = pseudo_random(64, 3);
1004 let mut coeffs = decompose(
1005 &signal,
1006 WaveletFamily::Haar,
1007 BoundaryMode::Periodic,
1008 Some(3),
1009 )
1010 .unwrap();
1011 coeffs.details.pop();
1013 assert!(matches!(
1014 reconstruct(&coeffs),
1015 Err(FdarError::InvalidDimension { .. })
1016 ));
1017 }
1018
1019 #[test]
1022 fn decompose_matrix_matches_per_row_slice_path() {
1023 use crate::matrix::FdMatrix;
1024 let nrows = 5;
1025 let ncols = 48;
1026 let mut flat = vec![0.0_f64; nrows * ncols];
1028 for i in 0..nrows {
1029 let row = pseudo_random(ncols, 1000 + i as u64);
1030 for j in 0..ncols {
1031 flat[i + j * nrows] = row[j];
1032 }
1033 }
1034 let m = FdMatrix::from_column_major(flat, nrows, ncols).unwrap();
1035 let batch = decompose_matrix(
1036 &m,
1037 WaveletFamily::Daubechies(4),
1038 BoundaryMode::Periodic,
1039 None,
1040 )
1041 .unwrap();
1042 assert_eq!(batch.len(), nrows);
1043 for i in 0..nrows {
1044 let per_row = decompose(
1045 &m.row(i),
1046 WaveletFamily::Daubechies(4),
1047 BoundaryMode::Periodic,
1048 None,
1049 )
1050 .unwrap();
1051 assert_eq!(batch[i], per_row, "row {i} batch != per-row");
1052 }
1053 }
1054
1055 #[test]
1056 fn decompose_matrix_round_trip_both_modes() {
1057 use crate::matrix::FdMatrix;
1058 let nrows = 4;
1059 let ncols = 48;
1060 let mut rows = Vec::new();
1061 let mut flat = vec![0.0_f64; nrows * ncols];
1062 for i in 0..nrows {
1063 let row = pseudo_random(ncols, 2000 + i as u64);
1064 for j in 0..ncols {
1065 flat[i + j * nrows] = row[j];
1066 }
1067 rows.push(row);
1068 }
1069 let m = FdMatrix::from_column_major(flat, nrows, ncols).unwrap();
1070 for mode in [BoundaryMode::Periodic, BoundaryMode::Symmetric] {
1071 let batch = decompose_matrix(&m, WaveletFamily::Daubechies(6), mode, None).unwrap();
1072 for (i, coeffs) in batch.iter().enumerate() {
1073 let recon = reconstruct(coeffs).unwrap();
1074 assert_eq!(recon.len(), ncols);
1075 let e = rel_err(&recon, &rows[i]);
1076 assert!(e < 1e-10, "row {i} {mode:?} rel err {e}");
1077 assert!(recon.iter().all(|x| x.is_finite()));
1078 }
1079 }
1080 }
1081
1082 #[test]
1083 fn decompose_matrix_rejects_empty_matrix() {
1084 use crate::matrix::FdMatrix;
1085 let zero_rows = FdMatrix::from_column_major(vec![], 0, 5).unwrap();
1086 assert!(matches!(
1087 decompose_matrix(
1088 &zero_rows,
1089 WaveletFamily::Haar,
1090 BoundaryMode::Periodic,
1091 None
1092 ),
1093 Err(FdarError::InvalidDimension { .. })
1094 ));
1095 let zero_cols = FdMatrix::from_column_major(vec![], 5, 0).unwrap();
1096 assert!(matches!(
1097 decompose_matrix(
1098 &zero_cols,
1099 WaveletFamily::Haar,
1100 BoundaryMode::Periodic,
1101 None
1102 ),
1103 Err(FdarError::InvalidDimension { .. })
1104 ));
1105 }
1106
1107 #[test]
1108 fn unsupported_order_surfaces_invalid_parameter() {
1109 assert!(matches!(
1111 WaveletFamily::from_db_order(11),
1112 Err(FdarError::InvalidParameter { .. })
1113 ));
1114 let signal = pseudo_random(64, 1);
1116 assert!(matches!(
1117 decompose(
1118 &signal,
1119 WaveletFamily::Daubechies(11),
1120 BoundaryMode::Periodic,
1121 None
1122 ),
1123 Err(FdarError::InvalidParameter { .. })
1124 ));
1125 }
1126}