Skip to main content

oxiphysics_core/
wavelets.rs

1#![allow(clippy::needless_range_loop, clippy::ptr_arg)]
2// Copyright 2026 COOLJAPAN OU (Team KitaSan)
3// SPDX-License-Identifier: Apache-2.0
4
5//! Wavelet transforms for signal analysis in physics simulations.
6//!
7//! Provides Haar, Daubechies-4, Symlet-4, continuous wavelet transforms (CWT),
8//! discrete wavelet transforms (DWT), wavelet-domain denoising,
9//! multi-resolution analysis, wavelet packet decomposition with best-basis
10//! selection, wavelet compression, and physics-specific wavelet tools.
11
12#![allow(dead_code)]
13
14// ─────────────────────────────────────────────────────────────────────────────
15// HaarWavelet
16// ─────────────────────────────────────────────────────────────────────────────
17
18/// Haar wavelet transform (single level and multi-level decomposition).
19///
20/// The Haar wavelet is the simplest orthogonal wavelet, based on piecewise
21/// constant functions.
22pub struct HaarWavelet;
23
24impl HaarWavelet {
25    /// Compute one level of the forward Haar transform.
26    ///
27    /// Returns `(approx, detail)` where each output has length `signal.len() / 2`.
28    /// The input length should be even; if odd the last sample is dropped.
29    pub fn forward(signal: &[f64]) -> (Vec<f64>, Vec<f64>) {
30        let n = signal.len() / 2;
31        let mut approx = Vec::with_capacity(n);
32        let mut detail = Vec::with_capacity(n);
33        let sqrt2_inv = 1.0 / std::f64::consts::SQRT_2;
34        for i in 0..n {
35            let a = signal[2 * i];
36            let b = signal[2 * i + 1];
37            approx.push((a + b) * sqrt2_inv);
38            detail.push((a - b) * sqrt2_inv);
39        }
40        (approx, detail)
41    }
42
43    /// Reconstruct a signal from its Haar approximation and detail coefficients.
44    ///
45    /// `approx` and `detail` must have the same length.
46    pub fn inverse(approx: &[f64], detail: &[f64]) -> Vec<f64> {
47        let n = approx.len().min(detail.len());
48        let mut out = vec![0.0; n * 2];
49        let sqrt2_inv = 1.0 / std::f64::consts::SQRT_2;
50        for i in 0..n {
51            let a = approx[i];
52            let d = detail[i];
53            out[2 * i] = (a + d) * sqrt2_inv;
54            out[2 * i + 1] = (a - d) * sqrt2_inv;
55        }
56        out
57    }
58
59    /// Decompose a signal into multiple levels.
60    ///
61    /// Returns a vector of length `levels + 1` where index 0 is the final
62    /// approximation and indices 1..=levels are detail coefficients at each
63    /// level (finest first).
64    pub fn decompose_level(signal: &[f64], levels: usize) -> Vec<Vec<f64>> {
65        let mut result = Vec::with_capacity(levels + 1);
66        let mut approx = signal.to_vec();
67        for _ in 0..levels {
68            if approx.len() < 2 {
69                break;
70            }
71            let (a, d) = Self::forward(&approx);
72            result.push(d);
73            approx = a;
74        }
75        result.push(approx);
76        result.reverse();
77        result
78    }
79}
80
81// ─────────────────────────────────────────────────────────────────────────────
82// Daubechies4
83// ─────────────────────────────────────────────────────────────────────────────
84
85/// Daubechies D4 wavelet with four filter taps.
86///
87/// Uses the standard D4 scaling (low-pass) filter `H` and wavelet (high-pass)
88/// filter `G` derived by the quadrature mirror filter relationship.
89pub struct Daubechies4;
90
91impl Daubechies4 {
92    /// D4 low-pass (scaling) filter coefficients.
93    pub const H: [f64; 4] = [
94        0.482962913_14503_f64,
95        0.836516303_73787_f64,
96        0.224143868_04186_f64,
97        -0.12940952_25523_f64,
98    ];
99
100    /// D4 high-pass (wavelet) filter coefficients (QMF of H).
101    pub const G: [f64; 4] = [
102        -0.12940952_25523_f64,
103        -0.224143868_04186_f64,
104        0.836516303_73787_f64,
105        -0.482962913_14503_f64,
106    ];
107
108    /// One-level forward D4 DWT via periodic convolution and downsampling.
109    ///
110    /// Returns `(approx, detail)` each of length `signal.len() / 2`.
111    /// Input length must be at least 4 and even.
112    pub fn forward(signal: &[f64]) -> (Vec<f64>, Vec<f64>) {
113        let n = signal.len();
114        let half = n / 2;
115        let mut approx = vec![0.0; half];
116        let mut detail = vec![0.0; half];
117        for i in 0..half {
118            let mut a = 0.0;
119            let mut d = 0.0;
120            for k in 0..4 {
121                let idx = (2 * i + k) % n;
122                a += Self::H[k] * signal[idx];
123                d += Self::G[k] * signal[idx];
124            }
125            approx[i] = a;
126            detail[i] = d;
127        }
128        (approx, detail)
129    }
130
131    /// One-level inverse D4 DWT (synthesis step).
132    ///
133    /// `approx` and `detail` must have the same length.
134    pub fn inverse(approx: &[f64], detail: &[f64]) -> Vec<f64> {
135        let half = approx.len().min(detail.len());
136        let n = half * 2;
137        let mut out = vec![0.0; n];
138        for i in 0..half {
139            for k in 0..4 {
140                let idx = (2 * i + k) % n;
141                out[idx] += Self::H[k] * approx[i] + Self::G[k] * detail[i];
142            }
143        }
144        out
145    }
146}
147
148// ─────────────────────────────────────────────────────────────────────────────
149// Symlet4
150// ─────────────────────────────────────────────────────────────────────────────
151
152/// Symlet-4 wavelet filter bank (sym4).
153///
154/// Symlets are nearly symmetric wavelets derived from Daubechies filters.
155/// The sym4 filter has 8 coefficients.
156pub struct Symlet4;
157
158impl Symlet4 {
159    /// Sym4 low-pass (scaling) filter coefficients (8 taps).
160    pub const H: [f64; 8] = [
161        -0.075_765_714_789_273_13,
162        -0.02963552764599851,
163        0.49761866763201545,
164        0.803_738_751_805_916_1,
165        0.29785779560527736,
166        -0.099_219_543_576_847_22,
167        -0.012603967262037833,
168        0.032_223_100_604_042_7,
169    ];
170
171    /// Sym4 high-pass (wavelet) filter coefficients (QMF of H).
172    pub const G: [f64; 8] = [
173        -0.032_223_100_604_042_7,
174        -0.012603967262037833,
175        0.099_219_543_576_847_22,
176        0.29785779560527736,
177        -0.803_738_751_805_916_1,
178        0.49761866763201545,
179        0.02963552764599851,
180        -0.075_765_714_789_273_13,
181    ];
182
183    /// One-level forward Sym4 DWT via periodic convolution and downsampling.
184    ///
185    /// Returns `(approx, detail)` each of length `signal.len() / 2`.
186    pub fn forward(signal: &[f64]) -> (Vec<f64>, Vec<f64>) {
187        let n = signal.len();
188        if n < 2 {
189            return (signal.to_vec(), vec![]);
190        }
191        let half = n / 2;
192        let filter_len = 8;
193        let mut approx = vec![0.0; half];
194        let mut detail = vec![0.0; half];
195        for i in 0..half {
196            let mut a = 0.0;
197            let mut d = 0.0;
198            for k in 0..filter_len {
199                let idx = (2 * i + k) % n;
200                a += Self::H[k] * signal[idx];
201                d += Self::G[k] * signal[idx];
202            }
203            approx[i] = a;
204            detail[i] = d;
205        }
206        (approx, detail)
207    }
208
209    /// One-level inverse Sym4 DWT (synthesis step).
210    ///
211    /// `approx` and `detail` must have the same length.
212    pub fn inverse(approx: &[f64], detail: &[f64]) -> Vec<f64> {
213        let half = approx.len().min(detail.len());
214        let n = half * 2;
215        if n == 0 {
216            return vec![];
217        }
218        let filter_len = 8;
219        let mut out = vec![0.0; n];
220        for i in 0..half {
221            for k in 0..filter_len {
222                let idx = (2 * i + k) % n;
223                out[idx] += Self::H[k] * approx[i] + Self::G[k] * detail[i];
224            }
225        }
226        out
227    }
228}
229
230// ─────────────────────────────────────────────────────────────────────────────
231// WaveletTransform — unified DWT interface
232// ─────────────────────────────────────────────────────────────────────────────
233
234/// Wavelet family selector for `WaveletTransform`.
235#[derive(Debug, Clone, Copy, PartialEq, Eq)]
236pub enum WaveletFamily {
237    /// Haar wavelet (2 filter taps).
238    Haar,
239    /// Daubechies D4 wavelet (4 filter taps).
240    Daubechies4,
241    /// Symlet-4 wavelet (8 filter taps).
242    Symlet4,
243}
244
245/// 1-D Discrete Wavelet Transform with pluggable filter bank.
246///
247/// Supports Haar, Daubechies-4, and Symlet-4 wavelets for single-level and
248/// multi-level decomposition/reconstruction.
249#[derive(Debug, Clone)]
250pub struct WaveletTransform {
251    /// The wavelet family used by this transform.
252    pub family: WaveletFamily,
253}
254
255impl WaveletTransform {
256    /// Create a new `WaveletTransform` using the given wavelet family.
257    pub fn new(family: WaveletFamily) -> Self {
258        Self { family }
259    }
260
261    /// Perform one level of the forward DWT.
262    ///
263    /// Returns `(approximation, detail)` coefficient vectors.
264    pub fn forward_one_level(&self, signal: &[f64]) -> (Vec<f64>, Vec<f64>) {
265        match self.family {
266            WaveletFamily::Haar => HaarWavelet::forward(signal),
267            WaveletFamily::Daubechies4 => Daubechies4::forward(signal),
268            WaveletFamily::Symlet4 => Symlet4::forward(signal),
269        }
270    }
271
272    /// Perform one level of the inverse DWT (reconstruction).
273    ///
274    /// Returns the reconstructed signal from approximation and detail coefficients.
275    pub fn inverse_one_level(&self, approx: &[f64], detail: &[f64]) -> Vec<f64> {
276        match self.family {
277            WaveletFamily::Haar => HaarWavelet::inverse(approx, detail),
278            WaveletFamily::Daubechies4 => Daubechies4::inverse(approx, detail),
279            WaveletFamily::Symlet4 => Symlet4::inverse(approx, detail),
280        }
281    }
282
283    /// Compute the energy (sum of squared coefficients) of a coefficient slice.
284    pub fn energy(coeffs: &[f64]) -> f64 {
285        coeffs.iter().map(|x| x * x).sum()
286    }
287
288    /// Compute the Shannon entropy of coefficient energies (normalized).
289    ///
290    /// Returns `- Σ p_i ln(p_i)` where `p_i = c_i² / total_energy`.
291    pub fn entropy(coeffs: &[f64]) -> f64 {
292        let total: f64 = coeffs.iter().map(|x| x * x).sum();
293        if total < 1e-30 {
294            return 0.0;
295        }
296        -coeffs
297            .iter()
298            .map(|x| {
299                let p = x * x / total;
300                if p < 1e-30 { 0.0 } else { p * p.ln() }
301            })
302            .sum::<f64>()
303    }
304}
305
306// ─────────────────────────────────────────────────────────────────────────────
307// MultilevelDwt
308// ─────────────────────────────────────────────────────────────────────────────
309
310/// Multi-level DWT decomposition and perfect reconstruction.
311///
312/// Stores approximation and detail coefficients at every decomposition level,
313/// allowing both analysis and synthesis (reconstruction).
314#[derive(Debug, Clone)]
315pub struct MultilevelDwt {
316    /// Wavelet transform used for all levels.
317    pub transform: WaveletTransform,
318    /// Number of decomposition levels requested.
319    pub levels: usize,
320    /// Approximation coefficients at the coarsest level (`approx[0]` = coarsest).
321    pub approx: Vec<f64>,
322    /// Detail coefficients at each level, coarsest first (`details[0]` = coarsest).
323    pub details: Vec<Vec<f64>>,
324}
325
326impl MultilevelDwt {
327    /// Decompose a signal into `levels` levels using the given wavelet family.
328    ///
329    /// After construction, `approx` holds the coarsest approximation and
330    /// `details` holds detail vectors from coarsest (index 0) to finest.
331    pub fn decompose(signal: &[f64], levels: usize, family: WaveletFamily) -> Self {
332        let transform = WaveletTransform::new(family);
333        let mut approx = signal.to_vec();
334        let mut details = Vec::with_capacity(levels);
335
336        for _ in 0..levels {
337            if approx.len() < 2 {
338                break;
339            }
340            let (a, d) = transform.forward_one_level(&approx);
341            details.push(d);
342            approx = a;
343        }
344        // Store details finest-first (last pushed = finest level detail)
345        details.reverse();
346
347        Self {
348            transform,
349            levels,
350            approx,
351            details,
352        }
353    }
354
355    /// Reconstruct the original signal from stored approximation and details.
356    ///
357    /// Applies inverse DWT from coarsest to finest level.
358    pub fn reconstruct(&self) -> Vec<f64> {
359        let mut signal = self.approx.clone();
360        // details[0] = coarsest → iterate forward from coarsest to finest
361        for detail in self.details.iter() {
362            signal = self.transform.inverse_one_level(&signal, detail);
363        }
364        signal
365    }
366
367    /// Return the total energy stored in all coefficient vectors.
368    pub fn total_energy(&self) -> f64 {
369        let approx_e: f64 = self.approx.iter().map(|x| x * x).sum();
370        let detail_e: f64 = self
371            .details
372            .iter()
373            .flat_map(|d| d.iter())
374            .map(|x| x * x)
375            .sum();
376        approx_e + detail_e
377    }
378
379    /// Return entropy at each decomposition level (approximation + details).
380    pub fn level_entropies(&self) -> Vec<f64> {
381        let mut entropies = vec![WaveletTransform::entropy(&self.approx)];
382        for d in &self.details {
383            entropies.push(WaveletTransform::entropy(d));
384        }
385        entropies
386    }
387}
388
389// ─────────────────────────────────────────────────────────────────────────────
390// WaveletPacket
391// ─────────────────────────────────────────────────────────────────────────────
392
393/// Wavelet packet decomposition tree.
394///
395/// Stores all sub-band coefficients produced by full wavelet packet
396/// decomposition up to a specified number of levels.
397#[derive(Debug, Clone)]
398pub struct WaveletPacket {
399    /// All node coefficient vectors in the packet tree (breadth-first order).
400    pub coefficients: Vec<Vec<f64>>,
401    /// Number of decomposition levels.
402    pub levels: usize,
403}
404
405impl WaveletPacket {
406    /// Build a wavelet packet decomposition using the Haar transform.
407    ///
408    /// After construction, `coefficients` contains 2^levels leaf nodes.
409    pub fn new_haar(signal: &[f64], levels: usize) -> Self {
410        let mut nodes: Vec<Vec<f64>> = vec![signal.to_vec()];
411        for _ in 0..levels {
412            let mut next = Vec::with_capacity(nodes.len() * 2);
413            for node in &nodes {
414                if node.len() >= 2 {
415                    let (a, d) = HaarWavelet::forward(node);
416                    next.push(a);
417                    next.push(d);
418                } else {
419                    next.push(node.clone());
420                    next.push(Vec::new());
421                }
422            }
423            nodes = next;
424        }
425        Self {
426            coefficients: nodes,
427            levels,
428        }
429    }
430
431    /// Build a wavelet packet decomposition using a specified wavelet family.
432    ///
433    /// Uses the given `WaveletFamily` for all decomposition steps.
434    pub fn new(signal: &[f64], levels: usize, family: WaveletFamily) -> Self {
435        let transform = WaveletTransform::new(family);
436        let mut nodes: Vec<Vec<f64>> = vec![signal.to_vec()];
437        for _ in 0..levels {
438            let mut next = Vec::with_capacity(nodes.len() * 2);
439            for node in &nodes {
440                if node.len() >= 2 {
441                    let (a, d) = transform.forward_one_level(node);
442                    next.push(a);
443                    next.push(d);
444                } else {
445                    next.push(node.clone());
446                    next.push(Vec::new());
447                }
448            }
449            nodes = next;
450        }
451        Self {
452            coefficients: nodes,
453            levels,
454        }
455    }
456
457    /// Return the index of the leaf node with highest energy (best basis heuristic).
458    pub fn best_basis(&self) -> Vec<usize> {
459        let energies = self.energy_distribution();
460        let max_e = energies.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
461        energies
462            .iter()
463            .enumerate()
464            .filter(|&(_, &e)| (e - max_e).abs() < 1e-12 * max_e.abs().max(1.0))
465            .map(|(i, _)| i)
466            .collect()
467    }
468
469    /// Select the best basis using minimum Shannon entropy criterion.
470    ///
471    /// Returns the indices of the leaf nodes forming the minimum-entropy basis.
472    /// This implements a greedy search over the leaf nodes of the full packet tree.
473    pub fn best_basis_entropy(&self) -> Vec<usize> {
474        let entropies: Vec<f64> = self
475            .coefficients
476            .iter()
477            .map(|c| WaveletTransform::entropy(c))
478            .collect();
479        let min_e = entropies.iter().cloned().fold(f64::INFINITY, f64::min);
480        entropies
481            .iter()
482            .enumerate()
483            .filter(|&(_, &e)| (e - min_e).abs() < 1e-12 * min_e.abs().max(1.0))
484            .map(|(i, _)| i)
485            .collect()
486    }
487
488    /// Compute the energy (sum of squares) in each leaf node.
489    pub fn energy_distribution(&self) -> Vec<f64> {
490        self.coefficients
491            .iter()
492            .map(|c| c.iter().map(|x| x * x).sum())
493            .collect()
494    }
495
496    /// Compute the Shannon entropy of each leaf node's coefficients.
497    pub fn entropy_distribution(&self) -> Vec<f64> {
498        self.coefficients
499            .iter()
500            .map(|c| WaveletTransform::entropy(c))
501            .collect()
502    }
503
504    /// Return the total energy across all leaf nodes.
505    pub fn total_energy(&self) -> f64 {
506        self.energy_distribution().iter().sum()
507    }
508}
509
510// ─────────────────────────────────────────────────────────────────────────────
511// ContinuousWavelet — CWT with Morlet/Mexican Hat/Paul wavelets
512// ─────────────────────────────────────────────────────────────────────────────
513
514/// Continuous wavelet transform (CWT) with multiple mother wavelets.
515///
516/// Supports Morlet, Mexican Hat (Ricker), and Paul wavelets for scalogram
517/// computation and instantaneous frequency estimation.
518pub struct ContinuousWavelet;
519
520impl ContinuousWavelet {
521    /// Evaluate the (real part of the) Morlet wavelet at position `t`.
522    ///
523    /// `sigma` controls the time-frequency trade-off (bandwidth parameter).
524    pub fn morlet(t: f64, sigma: f64) -> f64 {
525        let gauss = (-t * t / (2.0 * sigma * sigma)).exp();
526        let wave = (t / sigma).cos();
527        gauss * wave
528    }
529
530    /// Evaluate the Mexican-hat (Ricker) wavelet at position `t`.
531    ///
532    /// Defined as `(1 - t²/σ²) exp(-t²/2σ²)`.
533    pub fn mexican_hat(t: f64, sigma: f64) -> f64 {
534        let s2 = sigma * sigma;
535        let u = t * t / s2;
536        (1.0 - u) * (-t * t / (2.0 * s2)).exp()
537    }
538
539    /// Evaluate the Paul wavelet of order `m` at position `t`.
540    ///
541    /// The real part is proportional to `cos(m * atan2(t, 1)) / (1 + t²)^((m+1)/2)`.
542    pub fn paul(t: f64, m: u32) -> f64 {
543        let denom = (1.0 + t * t).powf((m as f64 + 1.0) / 2.0);
544        let angle = (m as f64) * t.atan2(1.0);
545        angle.cos() / denom.max(1e-30)
546    }
547
548    /// Compute the CWT scalogram using the Morlet wavelet.
549    ///
550    /// Returns a matrix `scalogram[scale_idx][time_idx]` where each row
551    /// corresponds to one entry in `scales`.
552    ///
553    /// - `signal` — input time series.
554    /// - `dt` — sampling interval.
555    /// - `scales` — desired wavelet scales (dilation parameters).
556    pub fn cwt_morlet(signal: &[f64], dt: f64, scales: &[f64]) -> Vec<Vec<f64>> {
557        let n = signal.len();
558        let mut scalogram = Vec::with_capacity(scales.len());
559        let sigma = 1.0_f64;
560        for &scale in scales {
561            let mut row = Vec::with_capacity(n);
562            for t_idx in 0..n {
563                let mut val = 0.0;
564                for tau in 0..n {
565                    let time = (t_idx as f64 - tau as f64) * dt / scale.max(1e-12);
566                    val += signal[tau] * Self::morlet(time, sigma);
567                }
568                row.push(val * dt / scale.sqrt().max(1e-12));
569            }
570            scalogram.push(row);
571        }
572        scalogram
573    }
574
575    /// Compute the CWT scalogram using the Mexican-hat wavelet.
576    ///
577    /// Returns a matrix `scalogram[scale_idx][time_idx]`.
578    pub fn cwt_mexican_hat(signal: &[f64], dt: f64, scales: &[f64]) -> Vec<Vec<f64>> {
579        let n = signal.len();
580        let mut scalogram = Vec::with_capacity(scales.len());
581        let sigma = 1.0_f64;
582        for &scale in scales {
583            let mut row = Vec::with_capacity(n);
584            for t_idx in 0..n {
585                let mut val = 0.0;
586                for tau in 0..n {
587                    let time = (t_idx as f64 - tau as f64) * dt / scale.max(1e-12);
588                    val += signal[tau] * Self::mexican_hat(time, sigma);
589                }
590                row.push(val * dt / scale.sqrt().max(1e-12));
591            }
592            scalogram.push(row);
593        }
594        scalogram
595    }
596
597    /// Compute the CWT scalogram using the Paul wavelet of order `m`.
598    ///
599    /// Returns a matrix `scalogram[scale_idx][time_idx]`.
600    pub fn cwt_paul(signal: &[f64], dt: f64, scales: &[f64], m: u32) -> Vec<Vec<f64>> {
601        let n = signal.len();
602        let mut scalogram = Vec::with_capacity(scales.len());
603        for &scale in scales {
604            let mut row = Vec::with_capacity(n);
605            for t_idx in 0..n {
606                let mut val = 0.0;
607                for tau in 0..n {
608                    let time = (t_idx as f64 - tau as f64) * dt / scale.max(1e-12);
609                    val += signal[tau] * Self::paul(time, m);
610                }
611                row.push(val * dt / scale.sqrt().max(1e-12));
612            }
613            scalogram.push(row);
614        }
615        scalogram
616    }
617
618    /// Estimate the instantaneous dominant frequency at each time step from a scalogram.
619    ///
620    /// For each time index, finds the scale with maximum CWT coefficient magnitude
621    /// and converts it to a frequency via `f = 1 / (scale * dt)`.
622    pub fn instantaneous_frequency(scalogram: &[Vec<f64>], scales: &[f64], dt: f64) -> Vec<f64> {
623        if scalogram.is_empty() || scales.is_empty() {
624            return Vec::new();
625        }
626        let n_time = scalogram[0].len();
627        let n_scales = scalogram.len().min(scales.len());
628        (0..n_time)
629            .map(|t| {
630                let mut best_scale = scales[0];
631                let mut best_val = scalogram[0][t].abs();
632                for s in 1..n_scales {
633                    let v = scalogram[s][t].abs();
634                    if v > best_val {
635                        best_val = v;
636                        best_scale = scales[s];
637                    }
638                }
639                1.0 / (best_scale * dt).max(1e-30)
640            })
641            .collect()
642    }
643
644    /// Compute total power at each scale (integrate over time).
645    ///
646    /// Returns a vector of `(scale, power)` pairs.
647    pub fn scale_averaged_power(scalogram: &[Vec<f64>], scales: &[f64]) -> Vec<(f64, f64)> {
648        scalogram
649            .iter()
650            .zip(scales.iter())
651            .map(|(row, &scale)| {
652                let power: f64 = row.iter().map(|x| x * x).sum::<f64>() / row.len().max(1) as f64;
653                (scale, power)
654            })
655            .collect()
656    }
657}
658
659// ─────────────────────────────────────────────────────────────────────────────
660// ContinuousWaveletTransform (alias kept for backward compatibility)
661// ─────────────────────────────────────────────────────────────────────────────
662
663/// Continuous wavelet transform (CWT) — backward-compatible alias.
664///
665/// Provides Morlet and Mexican-hat CWT methods identical to [`ContinuousWavelet`].
666pub struct ContinuousWaveletTransform;
667
668impl ContinuousWaveletTransform {
669    /// Evaluate the (real part of the) Morlet wavelet at position `t`.
670    pub fn morlet(t: f64, sigma: f64) -> f64 {
671        ContinuousWavelet::morlet(t, sigma)
672    }
673
674    /// Evaluate the Mexican-hat (Ricker) wavelet at position `t`.
675    pub fn mexican_hat(t: f64, sigma: f64) -> f64 {
676        ContinuousWavelet::mexican_hat(t, sigma)
677    }
678
679    /// Compute the CWT scalogram using the Morlet wavelet.
680    pub fn cwt_morlet(signal: &[f64], dt: f64, scales: &[f64]) -> Vec<Vec<f64>> {
681        ContinuousWavelet::cwt_morlet(signal, dt, scales)
682    }
683
684    /// Estimate the instantaneous dominant frequency at each time step.
685    pub fn instantaneous_frequency(scalogram: &[Vec<f64>], scales: &[f64], dt: f64) -> Vec<f64> {
686        ContinuousWavelet::instantaneous_frequency(scalogram, scales, dt)
687    }
688}
689
690// ─────────────────────────────────────────────────────────────────────────────
691// DiscreteWaveletTransform
692// ─────────────────────────────────────────────────────────────────────────────
693
694/// Collection of discrete wavelet transform (DWT) utility functions.
695///
696/// Includes single-level and multi-level Haar DWT, energy, and entropy
697/// computations.
698pub struct DiscreteWaveletTransform;
699
700impl DiscreteWaveletTransform {
701    /// Compute the full Haar DWT of a signal.
702    ///
703    /// Returns a coefficient vector in the standard DWT layout:
704    /// `[approx_coarsest ... detail_level1]`.
705    pub fn dwt_haar(signal: &[f64]) -> Vec<f64> {
706        let mut coeffs = signal.to_vec();
707        let mut n = signal.len();
708        // In-place lifting
709        while n >= 2 {
710            let half = n / 2;
711            let tmp: Vec<f64> = (0..half)
712                .flat_map(|i| {
713                    let a = coeffs[2 * i];
714                    let b = coeffs[2 * i + 1];
715                    let s = 1.0 / std::f64::consts::SQRT_2;
716                    vec![(a + b) * s, (a - b) * s]
717                })
718                .collect();
719            // Store approx in first half, detail in second
720            for i in 0..half {
721                coeffs[i] = tmp[2 * i];
722                coeffs[half + i] = tmp[2 * i + 1];
723            }
724            n = half;
725        }
726        coeffs
727    }
728
729    /// Invert the Haar DWT produced by [`DiscreteWaveletTransform::dwt_haar`].
730    pub fn idwt_haar(coeffs: &[f64]) -> Vec<f64> {
731        let n = coeffs.len();
732        let mut signal = coeffs.to_vec();
733        let mut level_size = 2usize;
734        // Find the coarsest level
735        while level_size <= n {
736            level_size *= 2;
737        }
738        level_size /= 2;
739        // Reconstruct from coarsest to finest
740        let mut cur = 2usize;
741        while cur <= level_size {
742            let half = cur / 2;
743            let tmp: Vec<f64> = (0..half)
744                .flat_map(|i| {
745                    let a = signal[i];
746                    let d = signal[half + i];
747                    let s = 1.0 / std::f64::consts::SQRT_2;
748                    vec![(a + d) * s, (a - d) * s]
749                })
750                .collect();
751            signal[..cur].copy_from_slice(&tmp[..cur]);
752            cur *= 2;
753        }
754        signal
755    }
756
757    /// Compute the DWT coefficients at a specific decomposition level.
758    ///
759    /// Returns only the approximation coefficients after `level` Haar forward
760    /// steps.
761    pub fn dwt_level(signal: &[f64], level: usize) -> Vec<f64> {
762        let mut approx = signal.to_vec();
763        for _ in 0..level {
764            if approx.len() < 2 {
765                break;
766            }
767            let (a, _d) = HaarWavelet::forward(&approx);
768            approx = a;
769        }
770        approx
771    }
772
773    /// Compute the total energy (sum of squared coefficients) of a wavelet
774    /// coefficient array.
775    pub fn wavelet_energy(coeffs: &[f64]) -> f64 {
776        coeffs.iter().map(|x| x * x).sum()
777    }
778
779    /// Compute the Shannon entropy of a normalized wavelet coefficient array.
780    ///
781    /// Coefficients are normalized by total energy before computing entropy.
782    pub fn wavelet_entropy(coeffs: &[f64]) -> f64 {
783        let energy: f64 = coeffs.iter().map(|x| x * x).sum();
784        if energy < 1e-30 {
785            return 0.0;
786        }
787        -coeffs
788            .iter()
789            .map(|x| {
790                let p = x * x / energy;
791                if p < 1e-30 { 0.0 } else { p * p.ln() }
792            })
793            .sum::<f64>()
794    }
795}
796
797// ─────────────────────────────────────────────────────────────────────────────
798// WaveletDenoising — soft/hard thresholding with VisuShrink and SureShrink
799// ─────────────────────────────────────────────────────────────────────────────
800
801/// Wavelet-domain signal denoising methods.
802///
803/// Provides hard and soft thresholding, and common threshold selection rules
804/// including VisuShrink (universal) and SureShrink (SURE).
805pub struct WaveletDenoising;
806
807impl WaveletDenoising {
808    /// Hard thresholding: set coefficients with absolute value below `threshold` to zero.
809    pub fn hard_threshold(coeffs: &mut Vec<f64>, threshold: f64) {
810        for c in coeffs.iter_mut() {
811            if c.abs() < threshold {
812                *c = 0.0;
813            }
814        }
815    }
816
817    /// Soft thresholding: shrink coefficients toward zero by `threshold`.
818    pub fn soft_threshold(coeffs: &mut Vec<f64>, threshold: f64) {
819        for c in coeffs.iter_mut() {
820            if c.abs() <= threshold {
821                *c = 0.0;
822            } else {
823                *c = c.signum() * (c.abs() - threshold);
824            }
825        }
826    }
827
828    /// Universal (VisuShrink) threshold: σ √(2 ln N).
829    ///
830    /// Estimates noise σ from the median absolute deviation of the detail
831    /// coefficients.
832    pub fn universal_threshold(signal: &[f64]) -> f64 {
833        let n = signal.len();
834        if n == 0 {
835            return 0.0;
836        }
837        // Estimate σ via MAD / 0.6745 from detail coefficients
838        let (_approx, detail) = HaarWavelet::forward(signal);
839        let sigma = Self::mad_sigma(&detail);
840        sigma * (2.0 * (n as f64).ln()).sqrt()
841    }
842
843    /// SURE (Stein's Unbiased Risk Estimator) threshold.
844    ///
845    /// Selects the threshold that minimizes the SURE risk estimate.
846    pub fn sure_threshold(signal: &[f64]) -> f64 {
847        let n = signal.len();
848        if n == 0 {
849            return 0.0;
850        }
851        let mut sorted: Vec<f64> = signal.iter().map(|x| x.abs()).collect();
852        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
853        let mut best_risk = f64::INFINITY;
854        let mut best_thresh = 0.0;
855        let total_sq: f64 = signal.iter().map(|x| x * x).sum();
856        for (i, &thresh) in sorted.iter().enumerate() {
857            // SURE risk for threshold t:  N - 2*(# |x_i| <= t) + sum(min(|x_i|,t)^2)
858            let k = i + 1; // number of coefficients with |x| <= thresh
859            let sum_clamped: f64 = sorted
860                .iter()
861                .map(|&x| if x <= thresh { x * x } else { thresh * thresh })
862                .sum();
863            let risk = (n as f64) - 2.0 * (k as f64) + sum_clamped + (total_sq - sum_clamped);
864            if risk < best_risk {
865                best_risk = risk;
866                best_thresh = thresh;
867            }
868        }
869        best_thresh
870    }
871
872    /// Apply full denoising pipeline: DWT → threshold → IDWT.
873    ///
874    /// Decomposes the signal using the Haar DWT, applies soft thresholding
875    /// with the universal threshold, and reconstructs.
876    pub fn denoise_signal(signal: &[f64]) -> Vec<f64> {
877        if signal.len() < 2 {
878            return signal.to_vec();
879        }
880        let threshold = Self::universal_threshold(signal);
881        let mut coeffs = DiscreteWaveletTransform::dwt_haar(signal);
882        // Keep the approximation (index 0) untouched; threshold details
883        let n_approx = coeffs.len() / 2;
884        let (_, detail_part) = coeffs.split_at_mut(n_approx);
885        let mut detail_vec = detail_part.to_vec();
886        Self::soft_threshold(&mut detail_vec, threshold);
887        for (c, d) in detail_part.iter_mut().zip(detail_vec.iter()) {
888            *c = *d;
889        }
890        DiscreteWaveletTransform::idwt_haar(&coeffs)
891    }
892
893    /// Estimate noise standard deviation from detail coefficients using MAD.
894    fn mad_sigma(detail: &[f64]) -> f64 {
895        if detail.is_empty() {
896            return 1.0;
897        }
898        let mut abs_vals: Vec<f64> = detail.iter().map(|x| x.abs()).collect();
899        abs_vals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
900        let med = if abs_vals.len() % 2 == 1 {
901            abs_vals[abs_vals.len() / 2]
902        } else {
903            let m = abs_vals.len() / 2;
904            (abs_vals[m - 1] + abs_vals[m]) / 2.0
905        };
906        (med / 0.6745).max(1e-12)
907    }
908}
909
910// ─────────────────────────────────────────────────────────────────────────────
911// WaveletCompression
912// ─────────────────────────────────────────────────────────────────────────────
913
914/// Wavelet-based signal compression using coefficient thresholding.
915///
916/// Implements sparse representation, zero-tree coding concept, and
917/// compression ratio estimation.
918#[derive(Debug, Clone)]
919pub struct WaveletCompression {
920    /// Original signal length.
921    pub original_length: usize,
922    /// Number of non-zero coefficients after compression.
923    pub nonzero_count: usize,
924    /// Compressed coefficient vector (zero-tree encoded as sparse indices+values).
925    pub sparse_indices: Vec<usize>,
926    /// Coefficient values at sparse indices.
927    pub sparse_values: Vec<f64>,
928    /// Threshold used for compression.
929    pub threshold: f64,
930}
931
932impl WaveletCompression {
933    /// Compress a signal by retaining only DWT coefficients above `retain_fraction`.
934    ///
935    /// `retain_fraction` is the fraction of energy to preserve (e.g., 0.99 retains
936    /// 99% of signal energy).
937    pub fn compress(signal: &[f64], retain_fraction: f64) -> Self {
938        let n = signal.len();
939        let coeffs = DiscreteWaveletTransform::dwt_haar(signal);
940        let total_energy: f64 = coeffs.iter().map(|x| x * x).sum();
941
942        // Sort coefficients by magnitude descending; find threshold
943        let mut sorted_mags: Vec<f64> = coeffs.iter().map(|x| x.abs()).collect();
944        sorted_mags.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
945
946        let target_energy = retain_fraction.clamp(0.0, 1.0) * total_energy;
947        let mut cumulative = 0.0;
948        let mut threshold = 0.0;
949        for &mag in &sorted_mags {
950            cumulative += mag * mag;
951            if cumulative >= target_energy {
952                threshold = mag;
953                break;
954            }
955        }
956
957        let sparse_indices: Vec<usize> = coeffs
958            .iter()
959            .enumerate()
960            .filter(|&(_, c)| c.abs() >= threshold)
961            .map(|(i, _)| i)
962            .collect();
963        let sparse_values: Vec<f64> = sparse_indices.iter().map(|&i| coeffs[i]).collect();
964        let nonzero_count = sparse_indices.len();
965
966        Self {
967            original_length: n,
968            nonzero_count,
969            sparse_indices,
970            sparse_values,
971            threshold,
972        }
973    }
974
975    /// Reconstruct signal from compressed sparse representation.
976    ///
977    /// Fills a full coefficient vector from the sparse representation and
978    /// applies the inverse DWT.
979    pub fn decompress(&self) -> Vec<f64> {
980        let mut coeffs = vec![0.0f64; self.original_length];
981        for (&idx, &val) in self.sparse_indices.iter().zip(self.sparse_values.iter()) {
982            if idx < coeffs.len() {
983                coeffs[idx] = val;
984            }
985        }
986        DiscreteWaveletTransform::idwt_haar(&coeffs)
987    }
988
989    /// Compute compression ratio = original_length / nonzero_count.
990    ///
991    /// Returns 1.0 if no compression occurred.
992    pub fn compression_ratio(&self) -> f64 {
993        if self.nonzero_count == 0 {
994            return self.original_length as f64;
995        }
996        self.original_length as f64 / self.nonzero_count as f64
997    }
998
999    /// Compute the root-mean-square error between original and reconstructed signal.
1000    pub fn reconstruction_error(&self, original: &[f64]) -> f64 {
1001        let reconstructed = self.decompress();
1002        let n = original.len().min(reconstructed.len());
1003        if n == 0 {
1004            return 0.0;
1005        }
1006        let mse: f64 = original
1007            .iter()
1008            .zip(reconstructed.iter())
1009            .map(|(a, b)| (a - b).powi(2))
1010            .sum::<f64>()
1011            / n as f64;
1012        mse.sqrt()
1013    }
1014
1015    /// Compute the peak signal-to-noise ratio (PSNR) in dB.
1016    ///
1017    /// Uses the formula `PSNR = 20 log10(max_val / RMSE)`.
1018    pub fn psnr(&self, original: &[f64]) -> f64 {
1019        let rmse = self.reconstruction_error(original);
1020        if rmse < 1e-30 {
1021            return f64::INFINITY;
1022        }
1023        let max_val = original.iter().map(|x| x.abs()).fold(0.0_f64, f64::max);
1024        if max_val < 1e-30 {
1025            return 0.0;
1026        }
1027        20.0 * (max_val / rmse).log10()
1028    }
1029}
1030
1031// ─────────────────────────────────────────────────────────────────────────────
1032// MultiResolutionAnalysis
1033// ─────────────────────────────────────────────────────────────────────────────
1034
1035/// Multi-resolution analysis (MRA) using the Haar wavelet.
1036///
1037/// Provides approximate and detail coefficients at any decomposition level,
1038/// and signal reconstruction from a chosen level.
1039pub struct MultiResolutionAnalysis;
1040
1041impl MultiResolutionAnalysis {
1042    /// Extract the approximation coefficients at decomposition `level`.
1043    ///
1044    /// Applies `level` forward Haar transforms, returning the coarsest
1045    /// approximation vector.
1046    pub fn approximate_coefficients(signal: &[f64], level: usize) -> Vec<f64> {
1047        DiscreteWaveletTransform::dwt_level(signal, level)
1048    }
1049
1050    /// Extract the detail coefficients at decomposition `level`.
1051    ///
1052    /// Applies `level - 1` forward Haar transforms and returns the detail
1053    /// coefficients from the final step.
1054    pub fn detail_coefficients(signal: &[f64], level: usize) -> Vec<f64> {
1055        if level == 0 {
1056            return signal.to_vec();
1057        }
1058        let mut approx = signal.to_vec();
1059        for _ in 0..level - 1 {
1060            if approx.len() < 2 {
1061                break;
1062            }
1063            let (a, _) = HaarWavelet::forward(&approx);
1064            approx = a;
1065        }
1066        if approx.len() < 2 {
1067            return approx;
1068        }
1069        let (_, detail) = HaarWavelet::forward(&approx);
1070        detail
1071    }
1072
1073    /// Reconstruct a signal from its approximation and a list of detail arrays.
1074    ///
1075    /// `details` should be ordered from coarsest (index 0) to finest.
1076    pub fn reconstruct_from_level(approx: &[f64], details: &[Vec<f64>]) -> Vec<f64> {
1077        let mut signal = approx.to_vec();
1078        for detail in details {
1079            signal = HaarWavelet::inverse(&signal, detail);
1080        }
1081        signal
1082    }
1083}
1084
1085// ─────────────────────────────────────────────────────────────────────────────
1086// PoddedWavelet — physics-specific wavelet tools
1087// ─────────────────────────────────────────────────────────────────────────────
1088
1089/// Physics-specific wavelet-based analysis tools.
1090///
1091/// Provides turbulence spectrum estimation, shock detection, and
1092/// intermittency measurement using wavelet methods.
1093pub struct PoddedWavelet;
1094
1095impl PoddedWavelet {
1096    /// Estimate the turbulence energy spectrum from a velocity field using the
1097    /// Haar DWT.
1098    ///
1099    /// Returns a list of `(frequency, energy)` pairs, one per DWT level.
1100    /// `dt` is the sampling interval.
1101    pub fn turbulence_spectrum_wavelet(velocity_field: &[f64], dt: f64) -> Vec<(f64, f64)> {
1102        if velocity_field.len() < 2 {
1103            return Vec::new();
1104        }
1105        let decomp = HaarWavelet::decompose_level(velocity_field, 8);
1106        let n_levels = decomp.len();
1107        let mut result = Vec::with_capacity(n_levels);
1108        for (level_idx, coeffs) in decomp.iter().enumerate() {
1109            if coeffs.is_empty() {
1110                continue;
1111            }
1112            let energy: f64 = coeffs.iter().map(|x| x * x).sum::<f64>() / coeffs.len() as f64;
1113            // Frequency: scale = 2^level, frequency = 1 / (scale * dt)
1114            let scale = 2.0_f64.powi(level_idx as i32);
1115            let freq = 1.0 / (scale * dt.max(1e-30));
1116            result.push((freq, energy));
1117        }
1118        result
1119    }
1120
1121    /// Detect discontinuities (shocks) in a signal using first-difference
1122    /// magnitudes as a simple multi-scale shock indicator.
1123    ///
1124    /// Returns the sample indices where the absolute first difference
1125    /// `|signal[i+1] - signal[i]|` exceeds `threshold`.
1126    pub fn shock_detection(signal: &[f64], threshold: f64) -> Vec<usize> {
1127        if signal.len() < 2 {
1128            return Vec::new();
1129        }
1130        signal
1131            .windows(2)
1132            .enumerate()
1133            .filter_map(|(i, w)| {
1134                let diff = (w[1] - w[0]).abs();
1135                if diff > threshold { Some(i) } else { None }
1136            })
1137            .collect()
1138    }
1139
1140    /// Compute a wavelet-based intermittency measure at scale `scale`.
1141    ///
1142    /// Returns the kurtosis of the wavelet coefficients at the given scale,
1143    /// normalized so that a Gaussian signal returns 0.
1144    pub fn intermittency_measure(signal: &[f64], scale: f64) -> f64 {
1145        let n = signal.len();
1146        if n < 4 {
1147            return 0.0;
1148        }
1149        // Compute wavelet coefficients at this scale using Morlet
1150        let dt = 1.0;
1151        let scales = [scale];
1152        let scalogram = ContinuousWavelet::cwt_morlet(signal, dt, &scales);
1153        if scalogram.is_empty() {
1154            return 0.0;
1155        }
1156        let coeffs = &scalogram[0];
1157        let n_c = coeffs.len() as f64;
1158        if n_c < 2.0 {
1159            return 0.0;
1160        }
1161        let mean = coeffs.iter().sum::<f64>() / n_c;
1162        let variance = coeffs.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / n_c;
1163        if variance < 1e-30 {
1164            return 0.0;
1165        }
1166        let kurtosis =
1167            coeffs.iter().map(|x| (x - mean).powi(4)).sum::<f64>() / (n_c * variance * variance);
1168        kurtosis - 3.0 // excess kurtosis
1169    }
1170}
1171
1172// ─────────────────────────────────────────────────────────────────────────────
1173// Tests
1174// ─────────────────────────────────────────────────────────────────────────────
1175
1176#[cfg(test)]
1177mod tests {
1178    use super::*;
1179
1180    // ── HaarWavelet ────────────────────────────────────────────────────────────
1181
1182    #[test]
1183    fn test_haar_forward_basic() {
1184        let signal = vec![1.0, 3.0, 5.0, 11.0];
1185        let (a, d) = HaarWavelet::forward(&signal);
1186        assert_eq!(a.len(), 2);
1187        assert_eq!(d.len(), 2);
1188    }
1189
1190    #[test]
1191    fn test_haar_perfect_reconstruction() {
1192        let signal = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
1193        let (a, d) = HaarWavelet::forward(&signal);
1194        let reconstructed = HaarWavelet::inverse(&a, &d);
1195        for (orig, rec) in signal.iter().zip(reconstructed.iter()) {
1196            assert!((orig - rec).abs() < 1e-10, "{orig} vs {rec}");
1197        }
1198    }
1199
1200    #[test]
1201    fn test_haar_energy_conservation() {
1202        let signal: Vec<f64> = (1..=8).map(|x| x as f64).collect();
1203        let energy_in: f64 = signal.iter().map(|x| x * x).sum();
1204        let (a, d) = HaarWavelet::forward(&signal);
1205        let energy_out: f64 =
1206            a.iter().map(|x| x * x).sum::<f64>() + d.iter().map(|x| x * x).sum::<f64>();
1207        assert!(
1208            (energy_in - energy_out).abs() < 1e-8,
1209            "energy not conserved"
1210        );
1211    }
1212
1213    #[test]
1214    fn test_haar_constant_signal_zero_detail() {
1215        let signal = vec![5.0; 8];
1216        let (_a, d) = HaarWavelet::forward(&signal);
1217        for x in &d {
1218            assert!(x.abs() < 1e-12, "detail should be zero for constant signal");
1219        }
1220    }
1221
1222    #[test]
1223    fn test_haar_decompose_level_count() {
1224        let signal: Vec<f64> = (0..16).map(|x| x as f64).collect();
1225        let levels = HaarWavelet::decompose_level(&signal, 3);
1226        assert_eq!(levels.len(), 4, "should return levels+1 = 4 entries");
1227    }
1228
1229    #[test]
1230    fn test_haar_decompose_level_first_is_coarsest() {
1231        let signal: Vec<f64> = (0..16).map(|x| x as f64).collect();
1232        let levels = HaarWavelet::decompose_level(&signal, 3);
1233        // Coarsest (index 0) should be the shortest array
1234        let coarsest = &levels[0];
1235        let finest_detail = &levels[levels.len() - 1];
1236        assert!(coarsest.len() <= finest_detail.len());
1237    }
1238
1239    #[test]
1240    fn test_haar_inverse_length() {
1241        let a = vec![1.0, 2.0];
1242        let d = vec![0.5, 0.3];
1243        let out = HaarWavelet::inverse(&a, &d);
1244        assert_eq!(out.len(), 4);
1245    }
1246
1247    // ── Daubechies4 ───────────────────────────────────────────────────────────
1248
1249    #[test]
1250    fn test_d4_forward_length() {
1251        let signal: Vec<f64> = (0..16).map(|x| x as f64).collect();
1252        let (a, d) = Daubechies4::forward(&signal);
1253        assert_eq!(a.len(), 8);
1254        assert_eq!(d.len(), 8);
1255    }
1256
1257    #[test]
1258    fn test_d4_filter_norms() {
1259        // Both filters should be unit-norm (orthonormality condition)
1260        let h_norm: f64 = Daubechies4::H.iter().map(|x| x * x).sum::<f64>().sqrt();
1261        let g_norm: f64 = Daubechies4::G.iter().map(|x| x * x).sum::<f64>().sqrt();
1262        assert!((h_norm - 1.0).abs() < 1e-7, "H norm = {h_norm}");
1263        assert!((g_norm - 1.0).abs() < 1e-7, "G norm = {g_norm}");
1264    }
1265
1266    #[test]
1267    fn test_d4_constant_signal() {
1268        // Forward then inverse should approximately reconstruct for constant input
1269        let signal = vec![1.0; 8];
1270        let (a, d) = Daubechies4::forward(&signal);
1271        let rec = Daubechies4::inverse(&a, &d);
1272        // Sum of reconstruction should approximately equal sum of input
1273        let sum_in: f64 = signal.iter().sum();
1274        let sum_rec: f64 = rec.iter().sum();
1275        assert!((sum_in - sum_rec).abs() < 1e-6, "{sum_in} vs {sum_rec}");
1276    }
1277
1278    #[test]
1279    fn test_d4_energy_approximately_conserved() {
1280        let signal: Vec<f64> = (0..16).map(|x| (x as f64).sin()).collect();
1281        let e_in: f64 = signal.iter().map(|x| x * x).sum();
1282        let (a, d) = Daubechies4::forward(&signal);
1283        let e_out: f64 =
1284            a.iter().map(|x| x * x).sum::<f64>() + d.iter().map(|x| x * x).sum::<f64>();
1285        assert!(
1286            (e_in - e_out).abs() / e_in.max(1e-12) < 0.01,
1287            "D4 energy deviation too large: {e_in} vs {e_out}"
1288        );
1289    }
1290
1291    // ── Symlet4 ───────────────────────────────────────────────────────────────
1292
1293    #[test]
1294    fn test_sym4_filter_norms() {
1295        let h_norm: f64 = Symlet4::H.iter().map(|x| x * x).sum::<f64>().sqrt();
1296        let g_norm: f64 = Symlet4::G.iter().map(|x| x * x).sum::<f64>().sqrt();
1297        assert!((h_norm - 1.0).abs() < 1e-7, "Sym4 H norm = {h_norm}");
1298        assert!((g_norm - 1.0).abs() < 1e-7, "Sym4 G norm = {g_norm}");
1299    }
1300
1301    #[test]
1302    fn test_sym4_forward_length() {
1303        let signal: Vec<f64> = (0..32).map(|x| x as f64).collect();
1304        let (a, d) = Symlet4::forward(&signal);
1305        assert_eq!(a.len(), 16);
1306        assert_eq!(d.len(), 16);
1307    }
1308
1309    #[test]
1310    fn test_sym4_energy_approximately_conserved() {
1311        let signal: Vec<f64> = (0..32).map(|x| (x as f64 * 0.5).sin()).collect();
1312        let e_in: f64 = signal.iter().map(|x| x * x).sum();
1313        let (a, d) = Symlet4::forward(&signal);
1314        let e_out: f64 =
1315            a.iter().map(|x| x * x).sum::<f64>() + d.iter().map(|x| x * x).sum::<f64>();
1316        assert!(
1317            (e_in - e_out).abs() / e_in.max(1e-12) < 0.02,
1318            "Sym4 energy deviation: {e_in} vs {e_out}"
1319        );
1320    }
1321
1322    #[test]
1323    fn test_sym4_inverse_length() {
1324        let a = vec![1.0; 8];
1325        let d = vec![0.5; 8];
1326        let out = Symlet4::inverse(&a, &d);
1327        assert_eq!(out.len(), 16);
1328    }
1329
1330    // ── WaveletTransform ──────────────────────────────────────────────────────
1331
1332    #[test]
1333    fn test_wavelet_transform_haar_forward() {
1334        let wt = WaveletTransform::new(WaveletFamily::Haar);
1335        let signal = vec![1.0, 2.0, 3.0, 4.0];
1336        let (a, d) = wt.forward_one_level(&signal);
1337        assert_eq!(a.len(), 2);
1338        assert_eq!(d.len(), 2);
1339    }
1340
1341    #[test]
1342    fn test_wavelet_transform_d4_roundtrip_length() {
1343        let wt = WaveletTransform::new(WaveletFamily::Daubechies4);
1344        let signal: Vec<f64> = (0..16).map(|x| x as f64).collect();
1345        let (a, d) = wt.forward_one_level(&signal);
1346        let rec = wt.inverse_one_level(&a, &d);
1347        assert_eq!(rec.len(), signal.len());
1348    }
1349
1350    #[test]
1351    fn test_wavelet_transform_sym4_forward() {
1352        let wt = WaveletTransform::new(WaveletFamily::Symlet4);
1353        let signal: Vec<f64> = (0..16).map(|x| x as f64).collect();
1354        let (a, d) = wt.forward_one_level(&signal);
1355        assert_eq!(a.len(), 8);
1356        assert_eq!(d.len(), 8);
1357    }
1358
1359    #[test]
1360    fn test_wavelet_transform_energy() {
1361        let coeffs = vec![3.0, 4.0];
1362        assert!((WaveletTransform::energy(&coeffs) - 25.0).abs() < 1e-12);
1363    }
1364
1365    #[test]
1366    fn test_wavelet_transform_entropy_uniform() {
1367        // Uniform coefficients: maximum entropy
1368        let coeffs = vec![1.0; 4];
1369        let h = WaveletTransform::entropy(&coeffs);
1370        assert!(h > 0.0);
1371    }
1372
1373    #[test]
1374    fn test_wavelet_transform_entropy_spike() {
1375        // All energy in one coefficient: minimal entropy
1376        let mut coeffs = vec![0.0; 8];
1377        coeffs[0] = 1.0;
1378        let h = WaveletTransform::entropy(&coeffs);
1379        assert!(h.abs() < 1e-12);
1380    }
1381
1382    // ── MultilevelDwt ─────────────────────────────────────────────────────────
1383
1384    #[test]
1385    fn test_multilevel_haar_reconstruction() {
1386        let signal: Vec<f64> = (0..16).map(|x| x as f64).collect();
1387        let mld = MultilevelDwt::decompose(&signal, 3, WaveletFamily::Haar);
1388        let rec = mld.reconstruct();
1389        assert_eq!(rec.len(), signal.len());
1390        for (a, b) in signal.iter().zip(rec.iter()) {
1391            assert!((a - b).abs() < 1e-8, "MultilevelDwt roundtrip: {a} vs {b}");
1392        }
1393    }
1394
1395    #[test]
1396    fn test_multilevel_d4_reconstruction() {
1397        let signal: Vec<f64> = (0..32).map(|x| (x as f64 * 0.3).sin()).collect();
1398        let mld = MultilevelDwt::decompose(&signal, 2, WaveletFamily::Daubechies4);
1399        let rec = mld.reconstruct();
1400        assert_eq!(rec.len(), signal.len());
1401    }
1402
1403    #[test]
1404    fn test_multilevel_approx_coarsest() {
1405        let signal: Vec<f64> = (0..16).map(|x| x as f64).collect();
1406        let mld = MultilevelDwt::decompose(&signal, 3, WaveletFamily::Haar);
1407        // Coarsest approx should have length 16 / 2^3 = 2
1408        assert_eq!(mld.approx.len(), 2);
1409    }
1410
1411    #[test]
1412    fn test_multilevel_detail_count() {
1413        let signal: Vec<f64> = (0..16).map(|x| x as f64).collect();
1414        let mld = MultilevelDwt::decompose(&signal, 3, WaveletFamily::Haar);
1415        assert_eq!(mld.details.len(), 3);
1416    }
1417
1418    #[test]
1419    fn test_multilevel_total_energy_conserved() {
1420        let signal: Vec<f64> = (0..16).map(|x| (x as f64).cos()).collect();
1421        let e_in: f64 = signal.iter().map(|x| x * x).sum();
1422        let mld = MultilevelDwt::decompose(&signal, 3, WaveletFamily::Haar);
1423        let e_stored = mld.total_energy();
1424        assert!(
1425            (e_in - e_stored).abs() / e_in.max(1e-12) < 1e-8,
1426            "energy conserved: {e_in} vs {e_stored}"
1427        );
1428    }
1429
1430    #[test]
1431    fn test_multilevel_level_entropies_count() {
1432        let signal: Vec<f64> = (0..16).map(|x| x as f64).collect();
1433        let mld = MultilevelDwt::decompose(&signal, 3, WaveletFamily::Haar);
1434        let entropies = mld.level_entropies();
1435        // 1 approx + 3 detail levels
1436        assert_eq!(entropies.len(), 4);
1437    }
1438
1439    // ── WaveletPacket ─────────────────────────────────────────────────────────
1440
1441    #[test]
1442    fn test_packet_leaf_count() {
1443        let signal: Vec<f64> = (0..16).map(|x| x as f64).collect();
1444        let pkt = WaveletPacket::new_haar(&signal, 3);
1445        assert_eq!(pkt.coefficients.len(), 8); // 2^3 leaves
1446    }
1447
1448    #[test]
1449    fn test_packet_energy_distribution_length() {
1450        let signal: Vec<f64> = (0..8).map(|x| x as f64).collect();
1451        let pkt = WaveletPacket::new_haar(&signal, 2);
1452        let energies = pkt.energy_distribution();
1453        assert_eq!(energies.len(), 4);
1454    }
1455
1456    #[test]
1457    fn test_packet_best_basis_nonempty() {
1458        let signal: Vec<f64> = (0..8).map(|x| (x as f64).sin()).collect();
1459        let pkt = WaveletPacket::new_haar(&signal, 2);
1460        let bb = pkt.best_basis();
1461        assert!(!bb.is_empty());
1462    }
1463
1464    #[test]
1465    fn test_packet_total_energy_conserved() {
1466        let signal: Vec<f64> = (0..16).map(|x| x as f64).collect();
1467        let e_in: f64 = signal.iter().map(|x| x * x).sum();
1468        let pkt = WaveletPacket::new_haar(&signal, 1); // 1 level → 2 leaves
1469        let e_out: f64 = pkt.energy_distribution().iter().sum();
1470        assert!((e_in - e_out).abs() < 1e-6, "packet energy not conserved");
1471    }
1472
1473    #[test]
1474    fn test_packet_best_basis_entropy_nonempty() {
1475        let signal: Vec<f64> = (0..16).map(|x| (x as f64 * 0.5).sin()).collect();
1476        let pkt = WaveletPacket::new_haar(&signal, 2);
1477        let bb = pkt.best_basis_entropy();
1478        assert!(!bb.is_empty());
1479    }
1480
1481    #[test]
1482    fn test_packet_entropy_distribution_length() {
1483        let signal: Vec<f64> = (0..8).map(|x| x as f64).collect();
1484        let pkt = WaveletPacket::new_haar(&signal, 2);
1485        let ent = pkt.entropy_distribution();
1486        assert_eq!(ent.len(), 4);
1487    }
1488
1489    #[test]
1490    fn test_packet_new_with_family() {
1491        let signal: Vec<f64> = (0..16).map(|x| x as f64).collect();
1492        let pkt = WaveletPacket::new(&signal, 2, WaveletFamily::Daubechies4);
1493        assert_eq!(pkt.coefficients.len(), 4); // 2^2 leaves
1494    }
1495
1496    #[test]
1497    fn test_packet_total_energy_method() {
1498        let signal: Vec<f64> = (0..8).map(|x| x as f64).collect();
1499        let pkt = WaveletPacket::new_haar(&signal, 1);
1500        assert!(pkt.total_energy() > 0.0);
1501    }
1502
1503    // ── ContinuousWavelet ─────────────────────────────────────────────────────
1504
1505    #[test]
1506    fn test_morlet_at_zero() {
1507        // At t=0 the Gaussian envelope is 1 and cos(0)=1
1508        let v = ContinuousWavelet::morlet(0.0, 1.0);
1509        assert!((v - 1.0).abs() < 1e-12);
1510    }
1511
1512    #[test]
1513    fn test_morlet_decays_at_large_t() {
1514        let v0 = ContinuousWavelet::morlet(0.0, 1.0).abs();
1515        let v10 = ContinuousWavelet::morlet(10.0, 1.0).abs();
1516        assert!(v10 < v0 * 0.01, "Morlet should decay: {v10} vs {v0}");
1517    }
1518
1519    #[test]
1520    fn test_mexican_hat_at_zero() {
1521        let v = ContinuousWavelet::mexican_hat(0.0, 1.0);
1522        assert!((v - 1.0).abs() < 1e-12);
1523    }
1524
1525    #[test]
1526    fn test_mexican_hat_sign_change() {
1527        // At t=sigma, value should be 0 (zero crossing)
1528        let sigma = 1.0;
1529        let v = ContinuousWavelet::mexican_hat(sigma, sigma);
1530        assert!(v.abs() < 1e-12);
1531    }
1532
1533    #[test]
1534    fn test_paul_wavelet_finite() {
1535        let v = ContinuousWavelet::paul(0.5, 4);
1536        assert!(v.is_finite());
1537    }
1538
1539    #[test]
1540    fn test_paul_wavelet_decay() {
1541        // Paul wavelet should decay for large |t|
1542        let v_near = ContinuousWavelet::paul(0.0, 2).abs();
1543        let v_far = ContinuousWavelet::paul(100.0, 2).abs();
1544        assert!(
1545            v_far < v_near,
1546            "Paul wavelet should decay: {v_far} vs {v_near}"
1547        );
1548    }
1549
1550    #[test]
1551    fn test_cwt_morlet_shape() {
1552        let signal: Vec<f64> = (0..16).map(|x| (x as f64 * 0.5).sin()).collect();
1553        let scales = vec![1.0, 2.0, 4.0];
1554        let scalogram = ContinuousWavelet::cwt_morlet(&signal, 1.0, &scales);
1555        assert_eq!(scalogram.len(), 3);
1556        assert_eq!(scalogram[0].len(), 16);
1557    }
1558
1559    #[test]
1560    fn test_cwt_mexican_hat_shape() {
1561        let signal: Vec<f64> = (0..16).map(|x| (x as f64 * 0.5).sin()).collect();
1562        let scales = vec![1.0, 2.0];
1563        let scalogram = ContinuousWavelet::cwt_mexican_hat(&signal, 1.0, &scales);
1564        assert_eq!(scalogram.len(), 2);
1565        assert_eq!(scalogram[0].len(), 16);
1566    }
1567
1568    #[test]
1569    fn test_cwt_paul_shape() {
1570        let signal: Vec<f64> = (0..8).map(|x| (x as f64).sin()).collect();
1571        let scales = vec![1.0, 2.0];
1572        let scalogram = ContinuousWavelet::cwt_paul(&signal, 1.0, &scales, 4);
1573        assert_eq!(scalogram.len(), 2);
1574        assert_eq!(scalogram[0].len(), 8);
1575    }
1576
1577    #[test]
1578    fn test_instantaneous_frequency_length() {
1579        let signal: Vec<f64> = (0..8).map(|x| (x as f64).sin()).collect();
1580        let scales = vec![1.0, 2.0];
1581        let scalogram = ContinuousWavelet::cwt_morlet(&signal, 1.0, &scales);
1582        let freq = ContinuousWavelet::instantaneous_frequency(&scalogram, &scales, 1.0);
1583        assert_eq!(freq.len(), 8);
1584    }
1585
1586    #[test]
1587    fn test_instantaneous_frequency_positive() {
1588        let signal: Vec<f64> = (0..8).map(|x| (x as f64).sin()).collect();
1589        let scales = vec![1.0, 2.0, 4.0];
1590        let scalogram = ContinuousWavelet::cwt_morlet(&signal, 1.0, &scales);
1591        let freq = ContinuousWavelet::instantaneous_frequency(&scalogram, &scales, 1.0);
1592        for f in &freq {
1593            assert!(*f > 0.0, "frequency must be positive");
1594        }
1595    }
1596
1597    #[test]
1598    fn test_scale_averaged_power_length() {
1599        let signal: Vec<f64> = (0..8).map(|x| (x as f64).sin()).collect();
1600        let scales = vec![1.0, 2.0, 4.0];
1601        let scalogram = ContinuousWavelet::cwt_morlet(&signal, 1.0, &scales);
1602        let power = ContinuousWavelet::scale_averaged_power(&scalogram, &scales);
1603        assert_eq!(power.len(), 3);
1604    }
1605
1606    // ── DiscreteWaveletTransform ───────────────────────────────────────────────
1607
1608    #[test]
1609    fn test_dwt_haar_length_preserved() {
1610        let signal: Vec<f64> = (0..8).map(|x| x as f64).collect();
1611        let coeffs = DiscreteWaveletTransform::dwt_haar(&signal);
1612        assert_eq!(coeffs.len(), signal.len());
1613    }
1614
1615    #[test]
1616    fn test_idwt_haar_roundtrip() {
1617        let signal: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
1618        let coeffs = DiscreteWaveletTransform::dwt_haar(&signal);
1619        let reconstructed = DiscreteWaveletTransform::idwt_haar(&coeffs);
1620        for (a, b) in signal.iter().zip(reconstructed.iter()) {
1621            assert!((a - b).abs() < 1e-8, "IDWT roundtrip failed: {a} vs {b}");
1622        }
1623    }
1624
1625    #[test]
1626    fn test_dwt_level_length() {
1627        let signal: Vec<f64> = (0..16).map(|x| x as f64).collect();
1628        let approx = DiscreteWaveletTransform::dwt_level(&signal, 2);
1629        assert_eq!(approx.len(), 4); // 16 / 2^2 = 4
1630    }
1631
1632    #[test]
1633    fn test_wavelet_energy_nonneg() {
1634        let coeffs = vec![1.0, -2.0, 3.0];
1635        let e = DiscreteWaveletTransform::wavelet_energy(&coeffs);
1636        assert!(e >= 0.0);
1637        assert!((e - 14.0).abs() < 1e-12);
1638    }
1639
1640    #[test]
1641    fn test_wavelet_entropy_nonneg() {
1642        let coeffs = vec![1.0, 2.0, 3.0, 4.0];
1643        let h = DiscreteWaveletTransform::wavelet_entropy(&coeffs);
1644        assert!(h >= 0.0);
1645    }
1646
1647    #[test]
1648    fn test_wavelet_entropy_uniform_max() {
1649        // Uniform coefficients should give higher entropy than concentrated ones
1650        let uniform = vec![1.0; 8];
1651        let spike: Vec<f64> = {
1652            let mut v = vec![0.0; 8];
1653            v[0] = 8.0;
1654            v
1655        };
1656        let h_uniform = DiscreteWaveletTransform::wavelet_entropy(&uniform);
1657        let h_spike = DiscreteWaveletTransform::wavelet_entropy(&spike);
1658        assert!(h_uniform >= h_spike, "uniform should have higher entropy");
1659    }
1660
1661    // ── WaveletDenoising ──────────────────────────────────────────────────────
1662
1663    #[test]
1664    fn test_hard_threshold_zeroes_small() {
1665        let mut coeffs = vec![0.1, 0.5, -0.2, 1.0];
1666        WaveletDenoising::hard_threshold(&mut coeffs, 0.3);
1667        assert_eq!(coeffs[0], 0.0);
1668        assert_eq!(coeffs[1], 0.5);
1669        assert_eq!(coeffs[2], 0.0);
1670        assert_eq!(coeffs[3], 1.0);
1671    }
1672
1673    #[test]
1674    fn test_soft_threshold_shrinks() {
1675        let mut coeffs = vec![1.0, -2.0, 0.5];
1676        WaveletDenoising::soft_threshold(&mut coeffs, 0.5);
1677        assert!((coeffs[0] - 0.5).abs() < 1e-12);
1678        assert!((coeffs[1] + 1.5).abs() < 1e-12);
1679        assert!(coeffs[2].abs() < 1e-12);
1680    }
1681
1682    #[test]
1683    fn test_universal_threshold_positive() {
1684        let signal: Vec<f64> = (0..64)
1685            .map(|i| (i as f64 * 0.1).sin() + 0.1 * (i as f64))
1686            .collect();
1687        let t = WaveletDenoising::universal_threshold(&signal);
1688        assert!(t >= 0.0, "threshold must be non-negative");
1689    }
1690
1691    #[test]
1692    fn test_sure_threshold_nonneg() {
1693        let signal = vec![1.0, 2.0, 3.0, 0.1, 0.05];
1694        let t = WaveletDenoising::sure_threshold(&signal);
1695        assert!(t >= 0.0);
1696    }
1697
1698    #[test]
1699    fn test_soft_threshold_zero_input() {
1700        let mut coeffs: Vec<f64> = vec![];
1701        WaveletDenoising::soft_threshold(&mut coeffs, 1.0);
1702        assert!(coeffs.is_empty());
1703    }
1704
1705    #[test]
1706    fn test_denoise_signal_length_preserved() {
1707        let signal: Vec<f64> = (0..16).map(|x| (x as f64 * 0.5).sin()).collect();
1708        let denoised = WaveletDenoising::denoise_signal(&signal);
1709        assert_eq!(denoised.len(), signal.len());
1710    }
1711
1712    #[test]
1713    fn test_denoise_small_signal_unchanged() {
1714        let signal = vec![1.0];
1715        let denoised = WaveletDenoising::denoise_signal(&signal);
1716        assert_eq!(denoised, signal);
1717    }
1718
1719    // ── WaveletCompression ────────────────────────────────────────────────────
1720
1721    #[test]
1722    fn test_compression_ratio_ge_one() {
1723        let signal: Vec<f64> = (0..64).map(|x| (x as f64 * 0.5).sin()).collect();
1724        let comp = WaveletCompression::compress(&signal, 0.90);
1725        assert!(comp.compression_ratio() >= 1.0);
1726    }
1727
1728    #[test]
1729    fn test_compression_lossless_full_retention() {
1730        let signal: Vec<f64> = (0..8).map(|x| x as f64).collect();
1731        let comp = WaveletCompression::compress(&signal, 1.0);
1732        let rec = comp.decompress();
1733        assert_eq!(rec.len(), signal.len());
1734        for (a, b) in signal.iter().zip(rec.iter()) {
1735            assert!((a - b).abs() < 1e-6, "lossless compression: {a} vs {b}");
1736        }
1737    }
1738
1739    #[test]
1740    fn test_compression_original_length_stored() {
1741        let signal: Vec<f64> = (0..32).map(|x| x as f64).collect();
1742        let comp = WaveletCompression::compress(&signal, 0.95);
1743        assert_eq!(comp.original_length, 32);
1744    }
1745
1746    #[test]
1747    fn test_compression_reconstruction_error_finite() {
1748        let signal: Vec<f64> = (0..32).map(|x| (x as f64 * 0.5).sin()).collect();
1749        let comp = WaveletCompression::compress(&signal, 0.99);
1750        let err = comp.reconstruction_error(&signal);
1751        assert!(err.is_finite());
1752        assert!(err >= 0.0);
1753    }
1754
1755    #[test]
1756    fn test_compression_psnr_nonneg() {
1757        let signal: Vec<f64> = (0..32).map(|x| (x as f64 * 0.3).cos()).collect();
1758        let comp = WaveletCompression::compress(&signal, 0.99);
1759        let psnr = comp.psnr(&signal);
1760        assert!(psnr >= 0.0 || psnr.is_infinite());
1761    }
1762
1763    #[test]
1764    fn test_compression_sparse_indices_nonempty() {
1765        let signal: Vec<f64> = (0..16).map(|x| (x as f64).sin()).collect();
1766        let comp = WaveletCompression::compress(&signal, 0.8);
1767        assert!(!comp.sparse_indices.is_empty());
1768    }
1769
1770    // ── MultiResolutionAnalysis ───────────────────────────────────────────────
1771
1772    #[test]
1773    fn test_mra_approx_length() {
1774        let signal: Vec<f64> = (0..16).map(|x| x as f64).collect();
1775        let approx = MultiResolutionAnalysis::approximate_coefficients(&signal, 2);
1776        assert_eq!(approx.len(), 4);
1777    }
1778
1779    #[test]
1780    fn test_mra_detail_length() {
1781        let signal: Vec<f64> = (0..16).map(|x| x as f64).collect();
1782        let detail = MultiResolutionAnalysis::detail_coefficients(&signal, 1);
1783        assert_eq!(detail.len(), 8);
1784    }
1785
1786    #[test]
1787    fn test_mra_detail_level_zero() {
1788        let signal = vec![1.0, 2.0, 3.0, 4.0];
1789        let d = MultiResolutionAnalysis::detail_coefficients(&signal, 0);
1790        // Level 0 returns the signal unchanged
1791        assert_eq!(d, signal);
1792    }
1793
1794    #[test]
1795    fn test_mra_reconstruct_single_level() {
1796        let signal: Vec<f64> = (0..8).map(|x| x as f64).collect();
1797        let approx = MultiResolutionAnalysis::approximate_coefficients(&signal, 1);
1798        let detail = MultiResolutionAnalysis::detail_coefficients(&signal, 1);
1799        let rec = MultiResolutionAnalysis::reconstruct_from_level(&approx, &[detail]);
1800        assert_eq!(rec.len(), signal.len());
1801        for (a, b) in signal.iter().zip(rec.iter()) {
1802            assert!((a - b).abs() < 1e-8, "MRA roundtrip: {a} vs {b}");
1803        }
1804    }
1805
1806    // ── PoddedWavelet ─────────────────────────────────────────────────────────
1807
1808    #[test]
1809    fn test_turbulence_spectrum_nonempty() {
1810        let vel: Vec<f64> = (0..64).map(|i| (i as f64 * 0.3).sin()).collect();
1811        let spectrum = PoddedWavelet::turbulence_spectrum_wavelet(&vel, 0.01);
1812        assert!(!spectrum.is_empty(), "spectrum should not be empty");
1813    }
1814
1815    #[test]
1816    fn test_turbulence_spectrum_positive_freq() {
1817        let vel: Vec<f64> = (0..32).map(|i| i as f64).collect();
1818        let spectrum = PoddedWavelet::turbulence_spectrum_wavelet(&vel, 0.01);
1819        for (f, _e) in &spectrum {
1820            assert!(*f > 0.0, "frequencies must be positive");
1821        }
1822    }
1823
1824    #[test]
1825    fn test_turbulence_spectrum_nonneg_energy() {
1826        let vel: Vec<f64> = (0..32).map(|i| (i as f64).cos()).collect();
1827        for (_f, e) in PoddedWavelet::turbulence_spectrum_wavelet(&vel, 0.01) {
1828            assert!(e >= 0.0);
1829        }
1830    }
1831
1832    #[test]
1833    fn test_shock_detection_step_function() {
1834        // Step function: indices around the discontinuity should be detected
1835        let mut signal = vec![0.0_f64; 16];
1836        for i in 8..16 {
1837            signal[i] = 10.0;
1838        }
1839        let shocks = PoddedWavelet::shock_detection(&signal, 0.5);
1840        assert!(!shocks.is_empty(), "should detect discontinuity");
1841    }
1842
1843    #[test]
1844    fn test_shock_detection_smooth_signal_empty() {
1845        // Very smooth signal: no shocks at high threshold
1846        let signal: Vec<f64> = (0..16).map(|i| (i as f64 * 0.1).sin()).collect();
1847        let shocks = PoddedWavelet::shock_detection(&signal, 1000.0);
1848        assert!(
1849            shocks.is_empty(),
1850            "no shocks expected for smooth signal at high threshold"
1851        );
1852    }
1853
1854    #[test]
1855    fn test_shock_detection_empty_signal() {
1856        let shocks = PoddedWavelet::shock_detection(&[], 0.5);
1857        assert!(shocks.is_empty());
1858    }
1859
1860    #[test]
1861    fn test_intermittency_small_signal() {
1862        // Signal too small → return 0
1863        let v = PoddedWavelet::intermittency_measure(&[1.0, 2.0], 1.0);
1864        assert_eq!(v, 0.0);
1865    }
1866
1867    #[test]
1868    fn test_intermittency_gaussian_near_zero() {
1869        // For a Gaussian-like signal the excess kurtosis is near 0
1870        let signal: Vec<f64> = (0..64).map(|i| (i as f64 * 0.2).sin()).collect();
1871        let k = PoddedWavelet::intermittency_measure(&signal, 2.0);
1872        // Just check it is finite
1873        assert!(k.is_finite(), "intermittency should be finite");
1874    }
1875}