Skip to main content

scirs2_linalg/
fft.rs

1//! Fast Fourier Transform (FFT) and spectral methods for signal processing
2//!
3//! This module implements cutting-edge FFT algorithms and spectral analysis techniques
4//! that provide efficient frequency domain computations for signal processing, image
5//! analysis, quantum physics, and machine learning applications:
6//!
7//! - **Core FFT algorithms**: Cooley-Tukey radix-2, mixed-radix, and prime-factor
8//! - **Real-valued FFT optimizations**: RFFT for real-world signal processing
9//! - **Multidimensional FFT**: 2D/3D FFT for image and volume processing
10//! - **Windowing functions**: Hann, Hamming, Blackman, Kaiser for spectral analysis
11//! - **Discrete transforms**: DCT, DST for compression and scientific computing
12//! - **Convolution algorithms**: FFT-based fast convolution for signal processing
13//! - **Spectral analysis**: Power spectral density and frequency analysis
14//!
15//! ## Key Advantages
16//!
17//! - **Optimal complexity**: O(n log n) instead of O(n²) for DFT
18//! - **Memory efficiency**: In-place algorithms with minimal overhead
19//! - **Numerical accuracy**: Bit-reversal and stable twiddle factor computation
20//! - **Real-world optimized**: RFFT leverages Hermitian symmetry for 2x speedup
21//! - **Comprehensive toolbox**: Complete spectral analysis ecosystem
22//!
23//! ## Mathematical Foundation
24//!
25//! The Discrete Fourier Transform (DFT) of a sequence `x[n]` is defined as:
26//!
27//! ```text
28//! X[k] = Σ(n=0 to N-1) x[n] * exp(-j * 2π * k * n / N)
29//! ```
30//!
31//! The FFT achieves O(n log n) complexity through divide-and-conquer recursion.
32//!
33//! ## References
34//!
35//! - Cooley, J. W., & Tukey, J. W. (1965). "An algorithm for the machine calculation of complex Fourier series"
36//! - Frigo, M., & Johnson, S. G. (2005). "The design and implementation of FFTW3"
37//! - Oppenheim, A. V., & Schafer, R. W. (2009). "Discrete-Time Signal Processing"
38
39use scirs2_core::ndarray::{Array1, Array2, Array3, ArrayView1, ArrayView2, ArrayView3};
40use scirs2_core::numeric::Complex;
41use scirs2_core::numeric::{Float, FloatConst};
42use std::f64::consts::PI;
43
44use crate::error::{LinalgError, LinalgResult};
45
46/// Complex number type for FFT computations
47pub type Complex64 = Complex<f64>;
48pub type Complex32 = Complex<f32>;
49
50/// FFT algorithm type selection
51#[derive(Debug, Clone, Copy)]
52pub enum FFTAlgorithm {
53    /// Cooley-Tukey radix-2 (power-of-2 sizes only)
54    CooleyTukey,
55    /// Mixed-radix algorithm (any size)
56    MixedRadix,
57    /// Prime-factor algorithm (sizes with small prime factors)
58    PrimeFactor,
59    /// Automatic selection based on input size
60    Auto,
61}
62
63/// Window function types for spectral analysis
64#[derive(Debug, Clone, Copy)]
65pub enum WindowFunction {
66    /// Rectangular window (no windowing)
67    Rectangular,
68    /// Hann window (raised cosine)
69    Hann,
70    /// Hamming window
71    Hamming,
72    /// Blackman window
73    Blackman,
74    /// Kaiser window with beta parameter
75    Kaiser(f64),
76    /// Tukey window with taper parameter
77    Tukey(f64),
78    /// Gaussian window with sigma parameter
79    Gaussian(f64),
80}
81
82/// FFT planning and execution context
83#[derive(Debug)]
84pub struct FFTPlan<F> {
85    /// Size of the transform
86    pub size: usize,
87    /// Algorithm to use
88    pub algorithm: FFTAlgorithm,
89    /// Precomputed twiddle factors
90    pub twiddle_factors: Vec<Complex<F>>,
91    /// Bit-reversal permutation indices
92    pub bit_reversal: Vec<usize>,
93    /// Whether this is for real input (RFFT)
94    pub real_input: bool,
95}
96
97impl<F> FFTPlan<F>
98where
99    F: Float + FloatConst,
100{
101    /// Create a new FFT plan for the given size
102    ///
103    /// # Arguments
104    ///
105    /// * `size` - Transform size
106    /// * `algorithm` - FFT algorithm to use
107    /// * `real_input` - Whether input is real-valued
108    ///
109    /// # Returns
110    ///
111    /// * FFT plan ready for execution
112    ///
113    /// # Examples
114    ///
115    /// ```
116    /// use scirs2_linalg::fft::{FFTPlan, FFTAlgorithm};
117    ///
118    /// let plan = FFTPlan::<f64>::new(1024, FFTAlgorithm::CooleyTukey, false).expect("Operation failed");
119    /// ```
120    pub fn new(size: usize, algorithm: FFTAlgorithm, realinput: bool) -> LinalgResult<Self> {
121        if size == 0 {
122            return Err(LinalgError::ShapeError(
123                "FFT size must be positive".to_string(),
124            ));
125        }
126
127        let selected_algorithm = match algorithm {
128            FFTAlgorithm::Auto => Self::select_algorithm(size),
129            _ => algorithm,
130        };
131
132        // Validate algorithm compatibility
133        if let FFTAlgorithm::CooleyTukey = selected_algorithm {
134            if !size.is_power_of_two() {
135                return Err(LinalgError::ShapeError(
136                    "Cooley-Tukey algorithm requires power-of-2 size".to_string(),
137                ));
138            }
139        }
140
141        let twiddle_factors = Self::compute_twiddle_factors(size);
142        let bit_reversal = Self::compute_bit_reversal(size);
143
144        Ok(FFTPlan {
145            size,
146            algorithm: selected_algorithm,
147            twiddle_factors,
148            bit_reversal,
149            real_input: realinput,
150        })
151    }
152
153    /// Automatically select the best algorithm for the given size
154    fn select_algorithm(size: usize) -> FFTAlgorithm {
155        if size.is_power_of_two() {
156            FFTAlgorithm::CooleyTukey
157        } else {
158            FFTAlgorithm::MixedRadix
159        }
160    }
161
162    /// Compute twiddle factors for FFT
163    fn compute_twiddle_factors(size: usize) -> Vec<Complex<F>> {
164        let mut twiddles = Vec::with_capacity(size);
165        let two_pi = F::from(2.0).expect("Failed to convert constant to float") * F::PI();
166
167        for k in 0..size {
168            let angle = -two_pi * F::from(k).expect("Failed to convert to float")
169                / F::from(size).expect("Failed to convert to float");
170            twiddles.push(Complex::new(angle.cos(), angle.sin()));
171        }
172
173        twiddles
174    }
175
176    /// Compute bit-reversal permutation for radix-2 FFT
177    fn compute_bit_reversal(size: usize) -> Vec<usize> {
178        let mut reversal = vec![0; size];
179        let logsize = (size as f64).log2() as usize;
180
181        for (i, item) in reversal.iter_mut().enumerate().take(size) {
182            *item = Self::reverse_bits(i, logsize);
183        }
184
185        reversal
186    }
187
188    /// Reverse bits of a number
189    fn reverse_bits(mut num: usize, bits: usize) -> usize {
190        let mut result = 0;
191        for _ in 0..bits {
192            result = (result << 1) | (num & 1);
193            num >>= 1;
194        }
195        result
196    }
197}
198
199/// Compute 1D FFT using the Cooley-Tukey radix-2 algorithm
200///
201/// # Arguments
202///
203/// * `input` - Input complex sequence
204/// * `inverse` - Whether to compute inverse FFT
205///
206/// # Returns
207///
208/// * FFT coefficients
209///
210/// # Examples
211///
212/// ```
213/// use scirs2_core::ndarray::array;
214/// use scirs2_core::numeric::Complex;
215/// use scirs2_linalg::fft::fft_1d;
216///
217/// let input = array![
218///     Complex::new(1.0, 0.0),
219///     Complex::new(0.0, 0.0),
220///     Complex::new(0.0, 0.0),
221///     Complex::new(0.0, 0.0),
222/// ];
223///
224/// let result = fft_1d(&input.view(), false).expect("Operation failed");
225/// ```
226#[allow(dead_code)]
227pub fn fft_1d(input: &ArrayView1<Complex64>, inverse: bool) -> LinalgResult<Array1<Complex64>> {
228    let size = input.len();
229
230    if !size.is_power_of_two() {
231        return Err(LinalgError::ShapeError(
232            "Input size must be power of 2 for Cooley-Tukey FFT".to_string(),
233        ));
234    }
235
236    let plan = FFTPlan::new(size, FFTAlgorithm::CooleyTukey, false)?;
237    fft_1d_with_plan(input, &plan, inverse)
238}
239
240/// Compute 1D FFT with a precomputed plan
241#[allow(dead_code)]
242pub fn fft_1d_with_plan(
243    input: &ArrayView1<Complex64>,
244    plan: &FFTPlan<f64>,
245    inverse: bool,
246) -> LinalgResult<Array1<Complex64>> {
247    if input.len() != plan.size {
248        return Err(LinalgError::ShapeError(format!(
249            "Input size {} doesn't match plan size {}",
250            input.len(),
251            plan.size
252        )));
253    }
254
255    match plan.algorithm {
256        FFTAlgorithm::CooleyTukey => cooley_tukey_fft(input, plan, inverse),
257        FFTAlgorithm::MixedRadix => mixed_radix_fft(input, plan, inverse),
258        _ => Err(LinalgError::ComputationError(
259            "Unsupported FFT algorithm".to_string(),
260        )),
261    }
262}
263
264/// Cooley-Tukey radix-2 FFT implementation
265#[allow(dead_code)]
266fn cooley_tukey_fft(
267    input: &ArrayView1<Complex64>,
268    plan: &FFTPlan<f64>,
269    inverse: bool,
270) -> LinalgResult<Array1<Complex64>> {
271    let size = input.len();
272    let mut data = input.to_owned();
273
274    // Bit-reversal permutation
275    for i in 0..size {
276        let j = plan.bit_reversal[i];
277        if i < j {
278            data.swap(i, j);
279        }
280    }
281
282    // Iterative FFT computation
283    let mut length = 2;
284    while length <= size {
285        let half_length = length / 2;
286        let step = size / length;
287
288        for start in (0..size).step_by(length) {
289            for i in 0..half_length {
290                let u = data[start + i];
291                let twiddle_index = i * step;
292                let mut twiddle = plan.twiddle_factors[twiddle_index];
293
294                if inverse {
295                    twiddle = twiddle.conj();
296                }
297
298                let v = data[start + i + half_length] * twiddle;
299
300                data[start + i] = u + v;
301                data[start + i + half_length] = u - v;
302            }
303        }
304
305        length *= 2;
306    }
307
308    // Scale for inverse FFT
309    if inverse {
310        let scale = Complex64::new(1.0 / size as f64, 0.0);
311        for elem in data.iter_mut() {
312            *elem *= scale;
313        }
314    }
315
316    Ok(data)
317}
318
319/// Mixed-radix FFT for arbitrary sizes
320#[allow(dead_code)]
321fn mixed_radix_fft(
322    input: &ArrayView1<Complex64>,
323    _plan: &FFTPlan<f64>,
324    inverse: bool,
325) -> LinalgResult<Array1<Complex64>> {
326    let size = input.len();
327
328    // For now, use Bluestein's algorithm for arbitrary sizes
329    bluestein_fft(input, inverse)
330}
331
332/// Bluestein's algorithm for arbitrary-size FFT
333#[allow(dead_code)]
334pub fn bluestein_fft(
335    input: &ArrayView1<Complex64>,
336    inverse: bool,
337) -> LinalgResult<Array1<Complex64>> {
338    let n = input.len();
339    if n <= 1 {
340        return Ok(input.to_owned());
341    }
342
343    // Find the next power of 2 greater than or equal to 2*n-1
344    let m = (2 * n - 1).next_power_of_two();
345
346    // Compute chirp sequence: exp(-j*π*k²/n)
347    let mut chirp = Array1::zeros(m);
348    let pi_over_n = if inverse {
349        PI / n as f64
350    } else {
351        -PI / n as f64
352    };
353
354    for k in 0..n {
355        let arg = pi_over_n * (k * k) as f64;
356        chirp[k] = Complex64::new(arg.cos(), arg.sin());
357        if k > 0 && k < n {
358            chirp[m - k] = chirp[k];
359        }
360    }
361
362    // Multiply input by chirp and zero-pad
363    let mut a = Array1::zeros(m);
364    for k in 0..n {
365        a[k] = input[k] * chirp[k];
366    }
367
368    // Compute FFTs
369    let a_fft = fft_power_of_2(&a.view())?;
370    let chirp_fft = fft_power_of_2(&chirp.view())?;
371
372    // Pointwise multiplication
373    let mut product = Array1::zeros(m);
374    for k in 0..m {
375        product[k] = a_fft[k] * chirp_fft[k];
376    }
377
378    // Inverse FFT
379    let ifft_result = ifft_power_of_2(&product.view())?;
380
381    // Extract result and multiply by chirp
382    let mut result = Array1::zeros(n);
383    for k in 0..n {
384        result[k] = ifft_result[k] * chirp[k];
385    }
386
387    // Scale for inverse
388    if inverse {
389        let scale = Complex64::new(1.0 / n as f64, 0.0);
390        for elem in result.iter_mut() {
391            *elem *= scale;
392        }
393    }
394
395    Ok(result)
396}
397
398/// FFT for power-of-2 sizes (helper for Bluestein)
399#[allow(dead_code)]
400fn fft_power_of_2(input: &ArrayView1<Complex64>) -> LinalgResult<Array1<Complex64>> {
401    if input.len().is_power_of_two() {
402        fft_1d(input, false)
403    } else {
404        Err(LinalgError::ShapeError(
405            "Input size must be power of 2".to_string(),
406        ))
407    }
408}
409
410/// IFFT for power-of-2 sizes (helper for Bluestein)
411#[allow(dead_code)]
412fn ifft_power_of_2(input: &ArrayView1<Complex64>) -> LinalgResult<Array1<Complex64>> {
413    if input.len().is_power_of_two() {
414        fft_1d(input, true)
415    } else {
416        Err(LinalgError::ShapeError(
417            "Input size must be power of 2".to_string(),
418        ))
419    }
420}
421
422/// Real-valued FFT (RFFT) exploiting Hermitian symmetry
423///
424/// For real input of size N, produces N/2+1 complex output coefficients.
425/// This is twice as efficient as complex FFT since we leverage the symmetry
426/// property: `X[N-k] = X*[k]` for real input.
427///
428/// # Arguments
429///
430/// * `input` - Real input sequence
431///
432/// # Returns
433///
434/// * FFT coefficients (N/2+1 complex values)
435///
436/// # Examples
437///
438/// ```
439/// use scirs2_core::ndarray::array;
440/// use scirs2_linalg::fft::rfft_1d;
441///
442/// let input = array![1.0, 0.0, 0.0, 0.0];
443/// let result = rfft_1d(&input.view()).expect("Operation failed");
444/// ```
445#[allow(dead_code)]
446pub fn rfft_1d(input: &ArrayView1<f64>) -> LinalgResult<Array1<Complex64>> {
447    let n = input.len();
448
449    // Convert real _input to complex
450    let mut complex_input = Array1::zeros(n);
451    for i in 0..n {
452        complex_input[i] = Complex64::new(input[i], 0.0);
453    }
454
455    // Compute full FFT
456    let full_fft = if n.is_power_of_two() {
457        fft_1d(&complex_input.view(), false)?
458    } else {
459        bluestein_fft(&complex_input.view(), false)?
460    };
461
462    // Extract positive frequencies (including DC and Nyquist)
463    let outputsize = n / 2 + 1;
464    let mut result = Array1::zeros(outputsize);
465
466    for i in 0..outputsize {
467        result[i] = full_fft[i];
468    }
469
470    Ok(result)
471}
472
473/// Inverse real-valued FFT (IRFFT)
474///
475/// Takes N/2+1 complex coefficients and produces N real output values.
476///
477/// # Arguments
478///
479/// * `input` - Complex FFT coefficients
480/// * `outputsize` - Size of real output (must be even)
481///
482/// # Returns
483///
484/// * Real time-domain signal
485#[allow(dead_code)]
486pub fn irfft_1d(input: &ArrayView1<Complex64>, outputsize: usize) -> LinalgResult<Array1<f64>> {
487    if !outputsize.is_multiple_of(2) {
488        return Err(LinalgError::ShapeError(
489            "Output size must be even for IRFFT".to_string(),
490        ));
491    }
492
493    let expected_inputsize = outputsize / 2 + 1;
494    if input.len() != expected_inputsize {
495        return Err(LinalgError::ShapeError(format!(
496            "Input size {} doesn't match expected size {} for output size {}",
497            input.len(),
498            expected_inputsize,
499            outputsize
500        )));
501    }
502
503    // Reconstruct full spectrum using Hermitian symmetry
504    let mut full_spectrum = Array1::zeros(outputsize);
505
506    // Copy positive frequencies
507    for i in 0..input.len() {
508        full_spectrum[i] = input[i];
509    }
510
511    // Fill negative frequencies using Hermitian symmetry: X[N-k] = X*[k]
512    for i in 1..outputsize / 2 {
513        full_spectrum[outputsize - i] = input[i].conj();
514    }
515
516    // Compute inverse FFT
517    let ifft_result = if outputsize.is_power_of_two() {
518        fft_1d(&full_spectrum.view(), true)?
519    } else {
520        bluestein_fft(&full_spectrum.view(), true)?
521    };
522
523    // Extract real part
524    let mut result = Array1::zeros(outputsize);
525    for i in 0..outputsize {
526        result[i] = ifft_result[i].re;
527    }
528
529    Ok(result)
530}
531
532/// 2D FFT for image processing and 2D signal analysis
533///
534/// # Arguments
535///
536/// * `input` - 2D complex input array
537/// * `inverse` - Whether to compute inverse FFT
538///
539/// # Returns
540///
541/// * 2D FFT coefficients
542///
543/// # Examples
544///
545/// ```
546/// use scirs2_core::ndarray::Array2;
547/// use scirs2_core::numeric::Complex;
548/// use scirs2_linalg::fft::fft_2d;
549///
550/// let input = Array2::from_shape_fn((4, 4), |(i, j)| {
551///     Complex::new((i + j) as f64, 0.0)
552/// });
553///
554/// let result = fft_2d(&input.view(), false).expect("Operation failed");
555/// ```
556#[allow(dead_code)]
557pub fn fft_2d(input: &ArrayView2<Complex64>, inverse: bool) -> LinalgResult<Array2<Complex64>> {
558    let (rows, cols) = input.dim();
559    let mut result = input.to_owned();
560
561    // FFT along rows
562    for i in 0..rows {
563        let row = result.row(i).to_owned();
564        let row_fft = if cols.is_power_of_two() {
565            fft_1d(&row.view(), inverse)?
566        } else {
567            bluestein_fft(&row.view(), inverse)?
568        };
569
570        for j in 0..cols {
571            result[[i, j]] = row_fft[j];
572        }
573    }
574
575    // FFT along columns
576    for j in 0..cols {
577        let col = result.column(j).to_owned();
578        let col_fft = if rows.is_power_of_two() {
579            fft_1d(&col.view(), inverse)?
580        } else {
581            bluestein_fft(&col.view(), inverse)?
582        };
583
584        for i in 0..rows {
585            result[[i, j]] = col_fft[i];
586        }
587    }
588
589    Ok(result)
590}
591
592/// 3D FFT for volume processing and 3D signal analysis
593#[allow(dead_code)]
594pub fn fft_3d(input: &ArrayView3<Complex64>, inverse: bool) -> LinalgResult<Array3<Complex64>> {
595    let (depth, rows, cols) = input.dim();
596    let mut result = input.to_owned();
597
598    // FFT along each dimension
599
600    // First dimension (depth)
601    for i in 0..rows {
602        for j in 0..cols {
603            let mut line = Array1::zeros(depth);
604            for k in 0..depth {
605                line[k] = result[[k, i, j]];
606            }
607
608            let line_fft = if depth.is_power_of_two() {
609                fft_1d(&line.view(), inverse)?
610            } else {
611                bluestein_fft(&line.view(), inverse)?
612            };
613
614            for k in 0..depth {
615                result[[k, i, j]] = line_fft[k];
616            }
617        }
618    }
619
620    // Second dimension (rows)
621    for k in 0..depth {
622        for j in 0..cols {
623            let mut line = Array1::zeros(rows);
624            for i in 0..rows {
625                line[i] = result[[k, i, j]];
626            }
627
628            let line_fft = if rows.is_power_of_two() {
629                fft_1d(&line.view(), inverse)?
630            } else {
631                bluestein_fft(&line.view(), inverse)?
632            };
633
634            for i in 0..rows {
635                result[[k, i, j]] = line_fft[i];
636            }
637        }
638    }
639
640    // Third dimension (cols)
641    for k in 0..depth {
642        for i in 0..rows {
643            let mut line = Array1::zeros(cols);
644            for j in 0..cols {
645                line[j] = result[[k, i, j]];
646            }
647
648            let line_fft = if cols.is_power_of_two() {
649                fft_1d(&line.view(), inverse)?
650            } else {
651                bluestein_fft(&line.view(), inverse)?
652            };
653
654            for j in 0..cols {
655                result[[k, i, j]] = line_fft[j];
656            }
657        }
658    }
659
660    Ok(result)
661}
662
663/// Apply window function to signal for spectral analysis
664///
665/// # Arguments
666///
667/// * `signal` - Input signal
668/// * `window` - Window function type
669///
670/// # Returns
671///
672/// * Windowed signal
673#[allow(dead_code)]
674pub fn apply_window(signal: &ArrayView1<f64>, window: WindowFunction) -> LinalgResult<Array1<f64>> {
675    let n = signal.len();
676    let mut windowed = signal.to_owned();
677
678    match window {
679        WindowFunction::Rectangular => {
680            // No modification needed
681        }
682        WindowFunction::Hann => {
683            for i in 0..n {
684                let factor = 0.5 * (1.0 - (2.0 * PI * i as f64 / (n - 1) as f64).cos());
685                windowed[i] *= factor;
686            }
687        }
688        WindowFunction::Hamming => {
689            for i in 0..n {
690                let factor = 0.54 - 0.46 * (2.0 * PI * i as f64 / (n - 1) as f64).cos();
691                windowed[i] *= factor;
692            }
693        }
694        WindowFunction::Blackman => {
695            for i in 0..n {
696                let x = 2.0 * PI * i as f64 / (n - 1) as f64;
697                let factor = 0.42 - 0.5 * x.cos() + 0.08 * (2.0 * x).cos();
698                windowed[i] *= factor;
699            }
700        }
701        WindowFunction::Kaiser(beta) => {
702            // Kaiser window with given beta parameter
703            let i0_beta = modified_bessel_i0(beta);
704            for i in 0..n {
705                let x = 2.0 * i as f64 / (n - 1) as f64 - 1.0;
706                let factor = modified_bessel_i0(beta * (1.0 - x * x).sqrt()) / i0_beta;
707                windowed[i] *= factor;
708            }
709        }
710        WindowFunction::Tukey(alpha) => {
711            let taper_len = ((alpha * n as f64) / 2.0) as usize;
712            for i in 0..n {
713                let factor = if i < taper_len {
714                    0.5 * (1.0 + (PI * i as f64 / taper_len as f64 - PI).cos())
715                } else if i >= n - taper_len {
716                    0.5 * (1.0 + (PI * (n - 1 - i) as f64 / taper_len as f64 - PI).cos())
717                } else {
718                    1.0
719                };
720                windowed[i] *= factor;
721            }
722        }
723        WindowFunction::Gaussian(sigma) => {
724            let center = (n - 1) as f64 / 2.0;
725            for i in 0..n {
726                let x = (i as f64 - center) / sigma;
727                let factor = (-0.5 * x * x).exp();
728                windowed[i] *= factor;
729            }
730        }
731    }
732
733    Ok(windowed)
734}
735
736/// Modified Bessel function I0 (for Kaiser window)
737#[allow(dead_code)]
738fn modified_bessel_i0(x: f64) -> f64 {
739    let mut result = 1.0;
740    let mut term = 1.0;
741    let mut k = 1.0;
742
743    while term.abs() > 1e-12 * result.abs() {
744        term *= (x / 2.0) * (x / 2.0) / (k * k);
745        result += term;
746        k += 1.0;
747    }
748
749    result
750}
751
752/// Discrete Cosine Transform (DCT) Type-II
753///
754/// The DCT is widely used in image compression (JPEG) and signal processing.
755///
756/// # Arguments
757///
758/// * `input` - Real input sequence
759///
760/// # Returns
761///
762/// * DCT coefficients
763#[allow(dead_code)]
764pub fn dct_1d(input: &ArrayView1<f64>) -> LinalgResult<Array1<f64>> {
765    let n = input.len();
766    let mut result = Array1::zeros(n);
767
768    for k in 0..n {
769        let mut sum = 0.0;
770        for i in 0..n {
771            let angle = PI * k as f64 * (2.0 * i as f64 + 1.0) / (2.0 * n as f64);
772            sum += input[i] * angle.cos();
773        }
774
775        let normalization = if k == 0 {
776            (1.0 / n as f64).sqrt()
777        } else {
778            (2.0 / n as f64).sqrt()
779        };
780
781        result[k] = sum * normalization;
782    }
783
784    Ok(result)
785}
786
787/// Inverse Discrete Cosine Transform (IDCT)
788#[allow(dead_code)]
789pub fn idct_1d(input: &ArrayView1<f64>) -> LinalgResult<Array1<f64>> {
790    let n = input.len();
791    let mut result = Array1::zeros(n);
792
793    for i in 0..n {
794        let mut sum = 0.0;
795
796        // DC component
797        sum += input[0] * (1.0 / n as f64).sqrt();
798
799        // AC components
800        for k in 1..n {
801            let angle = PI * k as f64 * (2.0 * i as f64 + 1.0) / (2.0 * n as f64);
802            sum += input[k] * (2.0 / n as f64).sqrt() * angle.cos();
803        }
804
805        result[i] = sum;
806    }
807
808    Ok(result)
809}
810
811/// Discrete Sine Transform (DST) Type-I
812#[allow(dead_code)]
813pub fn dst_1d(input: &ArrayView1<f64>) -> LinalgResult<Array1<f64>> {
814    let n = input.len();
815    let mut result = Array1::zeros(n);
816
817    for k in 0..n {
818        let mut sum = 0.0;
819        for i in 0..n {
820            let angle = PI * (k + 1) as f64 * (i + 1) as f64 / (n + 1) as f64;
821            sum += input[i] * angle.sin();
822        }
823
824        result[k] = sum * (2.0 / (n + 1) as f64).sqrt();
825    }
826
827    Ok(result)
828}
829
830/// Fast convolution using FFT
831///
832/// Computes the convolution of two signals using FFT, which is more efficient
833/// than direct convolution for large signals.
834///
835/// # Arguments
836///
837/// * `signal1` - First signal
838/// * `signal2` - Second signal (kernel)
839///
840/// # Returns
841///
842/// * Convolved signal
843#[allow(dead_code)]
844pub fn fft_convolve(
845    signal1: &ArrayView1<f64>,
846    signal2: &ArrayView1<f64>,
847) -> LinalgResult<Array1<f64>> {
848    let n1 = signal1.len();
849    let n2 = signal2.len();
850    let outputsize = n1 + n2 - 1;
851
852    // Find next power of 2 for efficient FFT
853    let fftsize = outputsize.next_power_of_two();
854
855    // Zero-pad both signals
856    let mut padded1 = Array1::zeros(fftsize);
857    let mut padded2 = Array1::zeros(fftsize);
858
859    for i in 0..n1 {
860        padded1[i] = signal1[i];
861    }
862    for i in 0..n2 {
863        padded2[i] = signal2[i];
864    }
865
866    // Convert to complex and compute FFTs
867    let complex1 = rfft_1d(&padded1.view())?;
868    let complex2 = rfft_1d(&padded2.view())?;
869
870    // Pointwise multiplication in frequency domain
871    let mut product = Array1::zeros(complex1.len());
872    for i in 0..complex1.len() {
873        product[i] = complex1[i] * complex2[i];
874    }
875
876    // Inverse FFT
877    let result_full = irfft_1d(&product.view(), fftsize)?;
878
879    // Extract valid convolution output
880    let mut result = Array1::zeros(outputsize);
881    for i in 0..outputsize {
882        result[i] = result_full[i];
883    }
884
885    Ok(result)
886}
887
888/// Power Spectral Density (PSD) estimation using periodogram method
889///
890/// # Arguments
891///
892/// * `signal` - Input signal
893/// * `window` - Window function to apply
894/// * `nfft` - FFT size (None for signal length)
895///
896/// # Returns
897///
898/// * Power spectral density
899#[allow(dead_code)]
900pub fn periodogram_psd(
901    signal: &ArrayView1<f64>,
902    window: WindowFunction,
903    nfft: Option<usize>,
904) -> LinalgResult<Array1<f64>> {
905    let n = signal.len();
906    let fftsize = nfft.unwrap_or(n);
907
908    // Apply window
909    let windowed = apply_window(signal, window)?;
910
911    // Zero-pad if necessary
912    let mut padded = Array1::zeros(fftsize);
913    for i in 0..n.min(fftsize) {
914        padded[i] = windowed[i];
915    }
916
917    // Compute FFT
918    let fft_result = rfft_1d(&padded.view())?;
919
920    // Compute power spectral density
921    let mut psd = Array1::zeros(fft_result.len());
922    let normalization = 1.0 / (fftsize as f64);
923
924    for i in 0..fft_result.len() {
925        psd[i] = fft_result[i].norm_sqr() * normalization;
926    }
927
928    // Handle DC and Nyquist components for real signals
929    if fftsize.is_multiple_of(2) && fft_result.len() > 1 {
930        // Double all except DC and Nyquist
931        for i in 1..fft_result.len() - 1 {
932            psd[i] *= 2.0;
933        }
934    } else {
935        // Double all except DC
936        for i in 1..fft_result.len() {
937            psd[i] *= 2.0;
938        }
939    }
940
941    Ok(psd)
942}
943
944/// Welch's method for PSD estimation with overlap
945///
946/// Provides better statistical properties than the periodogram by averaging
947/// multiple overlapped segments.
948///
949/// # Arguments
950///
951/// * `signal` - Input signal
952/// * `nperseg` - Length of each segment
953/// * `overlap` - Overlap between segments (0.0 to 1.0)
954/// * `window` - Window function
955///
956/// # Returns
957///
958/// * Power spectral density estimate
959#[allow(dead_code)]
960pub fn welch_psd(
961    signal: &ArrayView1<f64>,
962    nperseg: usize,
963    overlap: f64,
964    window: WindowFunction,
965) -> LinalgResult<Array1<f64>> {
966    if !(0.0..1.0).contains(&overlap) {
967        return Err(LinalgError::ShapeError(
968            "Overlap must be between 0.0 and 1.0".to_string(),
969        ));
970    }
971
972    let n = signal.len();
973    let step = ((1.0 - overlap) * nperseg as f64) as usize;
974    let num_segments = if n >= nperseg {
975        (n - nperseg) / step + 1
976    } else {
977        0
978    };
979
980    if num_segments == 0 {
981        return Err(LinalgError::ShapeError(
982            "Signal too short for given segment length".to_string(),
983        ));
984    }
985
986    let fftsize = nperseg.next_power_of_two();
987    let outputsize = fftsize / 2 + 1;
988    let mut psd_sum = Array1::zeros(outputsize);
989
990    for seg in 0..num_segments {
991        let start = seg * step;
992        let end = (start + nperseg).min(n);
993
994        // Extract segment
995        let mut segment = Array1::zeros(nperseg);
996        for i in 0..(end - start) {
997            segment[i] = signal[start + i];
998        }
999
1000        // Compute PSD for this segment
1001        let segment_psd = periodogram_psd(&segment.view(), window, Some(fftsize))?;
1002
1003        // Add to sum
1004        for i in 0..outputsize {
1005            psd_sum[i] += segment_psd[i];
1006        }
1007    }
1008
1009    // Average over segments
1010    for i in 0..outputsize {
1011        psd_sum[i] /= num_segments as f64;
1012    }
1013
1014    Ok(psd_sum)
1015}
1016
1017/// Fast Hadamard Transform (FHT) using recursive algorithm
1018///
1019/// The Hadamard transform is a generalization of the discrete Fourier transform
1020/// that uses a different basis. It's particularly useful in signal processing,
1021/// coding theory, and quantum computing.
1022///
1023/// # Arguments
1024///
1025/// * `input` - Input sequence (length must be a power of 2)
1026/// * `inverse` - Whether to compute the inverse transform
1027///
1028/// # Returns
1029///
1030/// * Hadamard transform coefficients
1031///
1032/// # Examples
1033///
1034/// ```
1035/// use scirs2_core::ndarray::array;
1036/// use scirs2_linalg::fft::hadamard_transform;
1037///
1038/// let input = array![1.0, 1.0, 1.0, 1.0];
1039/// let result = hadamard_transform(&input.view(), false).expect("Operation failed");
1040/// ```
1041#[allow(dead_code)]
1042pub fn hadamard_transform(input: &ArrayView1<f64>, inverse: bool) -> LinalgResult<Array1<f64>> {
1043    let n = input.len();
1044
1045    if !n.is_power_of_two() {
1046        return Err(LinalgError::ShapeError(
1047            "Input length must be a power of 2 for Hadamard transform".to_string(),
1048        ));
1049    }
1050
1051    if n == 0 {
1052        return Ok(Array1::zeros(0));
1053    }
1054
1055    let mut data = input.to_owned();
1056    let mut size = 1;
1057
1058    // Iterative Hadamard transform using butterfly operations
1059    while size < n {
1060        for start in (0..n).step_by(size * 2) {
1061            for i in 0..size {
1062                let left = start + i;
1063                let right = start + i + size;
1064
1065                if right < n {
1066                    let a = data[left];
1067                    let b = data[right];
1068                    data[left] = a + b;
1069                    data[right] = a - b;
1070                }
1071            }
1072        }
1073        size *= 2;
1074    }
1075
1076    // For inverse transform, divide by n
1077    if inverse {
1078        let scale = 1.0 / n as f64;
1079        data.mapv_inplace(|x| x * scale);
1080    }
1081
1082    Ok(data)
1083}
1084
1085/// Walsh-Hadamard Transform (WHT) using natural ordering
1086///
1087/// This is a variation of the Hadamard transform with natural ordering
1088/// of basis functions, often used in coding theory and cryptography.
1089///
1090/// # Arguments
1091///
1092/// * `input` - Input sequence (length must be a power of 2)
1093/// * `inverse` - Whether to compute the inverse transform
1094///
1095/// # Returns
1096///
1097/// * Walsh-Hadamard transform coefficients
1098#[allow(dead_code)]
1099pub fn walsh_hadamard_transform(
1100    input: &ArrayView1<f64>,
1101    inverse: bool,
1102) -> LinalgResult<Array1<f64>> {
1103    let n = input.len();
1104
1105    if !n.is_power_of_two() {
1106        return Err(LinalgError::ShapeError(
1107            "Input length must be a power of 2 for Walsh-Hadamard transform".to_string(),
1108        ));
1109    }
1110
1111    let mut data = input.to_owned();
1112    let log_n = (n as f64).log2() as usize;
1113
1114    // Bit-reversal permutation for natural ordering
1115    for i in 0..n {
1116        let j = bit_reverse(i, log_n);
1117        if i < j {
1118            data.swap(i, j);
1119        }
1120    }
1121
1122    // Apply Hadamard transform
1123    hadamard_transform(&data.view(), inverse)
1124}
1125
1126/// Bit-reverse function for Walsh-Hadamard ordering
1127#[allow(dead_code)]
1128fn bit_reverse(mut n: usize, bits: usize) -> usize {
1129    let mut result = 0;
1130    for _ in 0..bits {
1131        result = (result << 1) | (n & 1);
1132        n >>= 1;
1133    }
1134    result
1135}
1136
1137/// Fast Walsh Transform (FWT) for Boolean functions
1138///
1139/// This transform is particularly useful in Boolean function analysis,
1140/// cryptography, and coding theory.
1141///
1142/// # Arguments
1143///
1144/// * `input` - Input sequence representing truth table values
1145/// * `inverse` - Whether to compute the inverse transform
1146///
1147/// # Returns
1148///
1149/// * Walsh coefficients
1150#[allow(dead_code)]
1151pub fn fast_walsh_transform(input: &ArrayView1<f64>, inverse: bool) -> LinalgResult<Array1<f64>> {
1152    let n = input.len();
1153
1154    if !n.is_power_of_two() {
1155        return Err(LinalgError::ShapeError(
1156            "Input length must be a power of 2 for Fast Walsh Transform".to_string(),
1157        ));
1158    }
1159
1160    let mut data = input.to_owned();
1161    let mut h = 1;
1162
1163    while h < n {
1164        for i in (0..n).step_by(h * 2) {
1165            for j in i..i + h {
1166                let u = data[j];
1167                let v = data[j + h];
1168                data[j] = u + v;
1169                data[j + h] = u - v;
1170            }
1171        }
1172        h *= 2;
1173    }
1174
1175    // Normalization for inverse transform
1176    if inverse {
1177        let scale = 1.0 / n as f64;
1178        data.mapv_inplace(|x| x * scale);
1179    }
1180
1181    Ok(data)
1182}
1183
1184/// Generate frequency bins for FFT output
1185///
1186/// # Arguments
1187///
1188/// * `n` - FFT size
1189/// * `sample_rate` - Sampling rate
1190/// * `real_fft` - Whether this is for RFFT (half spectrum)
1191///
1192/// # Returns
1193///
1194/// * Frequency bins in Hz
1195#[allow(dead_code)]
1196pub fn fft_frequencies(n: usize, sample_rate: f64, realfft: bool) -> Array1<f64> {
1197    let outputsize = if realfft { n / 2 + 1 } else { n };
1198    let mut freqs = Array1::zeros(outputsize);
1199
1200    let df = sample_rate / n as f64;
1201
1202    if realfft {
1203        for i in 0..outputsize {
1204            freqs[i] = i as f64 * df;
1205        }
1206    } else {
1207        for i in 0..outputsize {
1208            freqs[i] = if i <= n / 2 {
1209                i as f64 * df
1210            } else {
1211                (i as i64 - n as i64) as f64 * df
1212            };
1213        }
1214    }
1215
1216    freqs
1217}
1218
1219#[cfg(test)]
1220mod tests {
1221    use super::*;
1222    use approx::assert_relative_eq;
1223    use scirs2_core::ndarray::array;
1224
1225    #[test]
1226    fn test_fft_plan_creation() {
1227        let plan =
1228            FFTPlan::<f64>::new(8, FFTAlgorithm::CooleyTukey, false).expect("Operation failed");
1229        assert_eq!(plan.size, 8);
1230        assert_eq!(plan.twiddle_factors.len(), 8);
1231        assert_eq!(plan.bit_reversal.len(), 8);
1232    }
1233
1234    #[test]
1235    fn test_fft_1d_basic() {
1236        let input = array![
1237            Complex64::new(1.0, 0.0),
1238            Complex64::new(0.0, 0.0),
1239            Complex64::new(0.0, 0.0),
1240            Complex64::new(0.0, 0.0),
1241        ];
1242
1243        let result = fft_1d(&input.view(), false).expect("Operation failed");
1244        assert_eq!(result.len(), 4);
1245
1246        // DC component should be 1
1247        assert_relative_eq!(result[0].re, 1.0, epsilon = 1e-10);
1248        assert_relative_eq!(result[0].im, 0.0, epsilon = 1e-10);
1249    }
1250
1251    #[test]
1252    fn test_fft_inverse_property() {
1253        let input = array![
1254            Complex64::new(1.0, 0.5),
1255            Complex64::new(0.2, -0.3),
1256            Complex64::new(-0.1, 0.8),
1257            Complex64::new(0.7, -0.2),
1258        ];
1259
1260        let fft_result = fft_1d(&input.view(), false).expect("Operation failed");
1261        let ifft_result = fft_1d(&fft_result.view(), true).expect("Operation failed");
1262
1263        for i in 0..input.len() {
1264            assert_relative_eq!(input[i].re, ifft_result[i].re, epsilon = 1e-12);
1265            assert_relative_eq!(input[i].im, ifft_result[i].im, epsilon = 1e-12);
1266        }
1267    }
1268
1269    #[test]
1270    fn test_rfft_1d() {
1271        let input = array![1.0, 0.0, 0.0, 0.0];
1272        let result = rfft_1d(&input.view()).expect("Operation failed");
1273
1274        assert_eq!(result.len(), 3); // N/2 + 1
1275        assert_relative_eq!(result[0].re, 1.0, epsilon = 1e-10);
1276        assert_relative_eq!(result[0].im, 0.0, epsilon = 1e-10);
1277    }
1278
1279    #[test]
1280    fn test_irfft_1d() {
1281        let input = array![1.0, 0.0, 0.0, 0.0];
1282        let fft_result = rfft_1d(&input.view()).expect("Operation failed");
1283        let reconstructed = irfft_1d(&fft_result.view(), 4).expect("Operation failed");
1284
1285        for i in 0..input.len() {
1286            assert_relative_eq!(input[i], reconstructed[i], epsilon = 1e-12);
1287        }
1288    }
1289
1290    #[test]
1291    fn test_fft_2d() {
1292        let input = Array2::from_shape_fn((4, 4), |(i, j)| Complex64::new((i + j) as f64, 0.0));
1293
1294        let result = fft_2d(&input.view(), false).expect("Operation failed");
1295        assert_eq!(result.shape(), &[4, 4]);
1296
1297        let reconstructed = fft_2d(&result.view(), true).expect("Operation failed");
1298
1299        for i in 0..4 {
1300            for j in 0..4 {
1301                assert_relative_eq!(input[[i, j]].re, reconstructed[[i, j]].re, epsilon = 1e-12);
1302                assert_relative_eq!(input[[i, j]].im, reconstructed[[i, j]].im, epsilon = 1e-12);
1303            }
1304        }
1305    }
1306
1307    #[test]
1308    fn test_window_functions() {
1309        let signal = array![1.0, 1.0, 1.0, 1.0];
1310
1311        // Rectangular window should not change the signal
1312        let rect =
1313            apply_window(&signal.view(), WindowFunction::Rectangular).expect("Operation failed");
1314        for i in 0..signal.len() {
1315            assert_relative_eq!(signal[i], rect[i], epsilon = 1e-10);
1316        }
1317
1318        // Hann window should taper to zero at edges
1319        let hann = apply_window(&signal.view(), WindowFunction::Hann).expect("Operation failed");
1320        assert_relative_eq!(hann[0], 0.0, epsilon = 1e-10);
1321        assert_relative_eq!(hann[3], 0.0, epsilon = 1e-10);
1322        assert!(hann[1] > 0.0);
1323        assert!(hann[2] > 0.0);
1324    }
1325
1326    #[test]
1327    fn test_dct_1d() {
1328        let input = array![1.0, 0.0, 0.0, 0.0];
1329        let dct_result = dct_1d(&input.view()).expect("Operation failed");
1330        let idct_result = idct_1d(&dct_result.view()).expect("Operation failed");
1331
1332        for i in 0..input.len() {
1333            assert_relative_eq!(input[i], idct_result[i], epsilon = 1e-12);
1334        }
1335    }
1336
1337    #[test]
1338    fn test_dst_1d() {
1339        let input = array![1.0, 2.0, 3.0, 4.0];
1340        let dst_result = dst_1d(&input.view()).expect("Operation failed");
1341        assert_eq!(dst_result.len(), 4);
1342        assert!(!dst_result.iter().all(|&x| x == 0.0));
1343    }
1344
1345    #[test]
1346    fn test_fft_convolve() {
1347        let signal1 = array![1.0, 2.0, 3.0];
1348        let signal2 = array![0.5, 1.5];
1349
1350        let result = fft_convolve(&signal1.view(), &signal2.view()).expect("Operation failed");
1351        assert_eq!(result.len(), 4); // n1 + n2 - 1
1352
1353        // Manual convolution check for first element
1354        assert_relative_eq!(result[0], 1.0 * 0.5, epsilon = 1e-10);
1355    }
1356
1357    #[test]
1358    fn test_periodogram_psd() {
1359        let signal = Array1::from_shape_fn(16, |i| (2.0 * PI * i as f64 / 16.0).sin());
1360        let psd = periodogram_psd(&signal.view(), WindowFunction::Rectangular, None)
1361            .expect("Operation failed");
1362
1363        assert_eq!(psd.len(), 9); // N/2 + 1 for real FFT
1364        assert!(psd.iter().all(|&x| x >= 0.0));
1365    }
1366
1367    #[test]
1368    fn test_welch_psd() {
1369        let signal = Array1::from_shape_fn(64, |i| (2.0 * PI * i as f64 / 8.0).sin());
1370        let psd =
1371            welch_psd(&signal.view(), 16, 0.5, WindowFunction::Hann).expect("Operation failed");
1372
1373        assert!(!psd.is_empty());
1374        assert!(psd.iter().all(|&x| x >= 0.0));
1375    }
1376
1377    #[test]
1378    fn test_fft_frequencies() {
1379        let freqs = fft_frequencies(8, 1000.0, true);
1380        assert_eq!(freqs.len(), 5); // N/2 + 1
1381
1382        assert_relative_eq!(freqs[0], 0.0, epsilon = 1e-10);
1383        assert_relative_eq!(freqs[1], 125.0, epsilon = 1e-10); // 1000/8
1384        assert_relative_eq!(freqs[4], 500.0, epsilon = 1e-10); // Nyquist
1385    }
1386
1387    #[test]
1388    fn test_bluestein_arbitrarysize() {
1389        let input = array![
1390            Complex64::new(1.0, 0.0),
1391            Complex64::new(0.0, 0.0),
1392            Complex64::new(0.0, 0.0),
1393            Complex64::new(0.0, 0.0),
1394            Complex64::new(0.0, 0.0),
1395        ]; // Size 5 (not power of 2)
1396
1397        let result = bluestein_fft(&input.view(), false).expect("Operation failed");
1398        let reconstructed = bluestein_fft(&result.view(), true).expect("Operation failed");
1399
1400        for i in 0..input.len() {
1401            assert_relative_eq!(input[i].re, reconstructed[i].re, epsilon = 1e-10);
1402            assert_relative_eq!(input[i].im, reconstructed[i].im, epsilon = 1e-10);
1403        }
1404    }
1405
1406    #[test]
1407    fn test_fft_3d() {
1408        let input = Array3::from_shape_fn((2, 2, 2), |(i, j, k)| {
1409            Complex64::new((i + j + k) as f64, 0.0)
1410        });
1411
1412        let result = fft_3d(&input.view(), false).expect("Operation failed");
1413        assert_eq!(result.shape(), &[2, 2, 2]);
1414
1415        let reconstructed = fft_3d(&result.view(), true).expect("Operation failed");
1416
1417        for i in 0..2 {
1418            for j in 0..2 {
1419                for k in 0..2 {
1420                    assert_relative_eq!(
1421                        input[[i, j, k]].re,
1422                        reconstructed[[i, j, k]].re,
1423                        epsilon = 1e-12
1424                    );
1425                    assert_relative_eq!(
1426                        input[[i, j, k]].im,
1427                        reconstructed[[i, j, k]].im,
1428                        epsilon = 1e-12
1429                    );
1430                }
1431            }
1432        }
1433    }
1434
1435    #[test]
1436    fn test_kaiser_window() {
1437        let signal = array![1.0, 1.0, 1.0, 1.0, 1.0];
1438        let windowed =
1439            apply_window(&signal.view(), WindowFunction::Kaiser(2.0)).expect("Operation failed");
1440
1441        // Kaiser window should taper towards edges
1442        assert!(windowed[0] < windowed[2]); // Edge less than center
1443        assert!(windowed[4] < windowed[2]); // Edge less than center
1444        assert!(windowed[2] > 0.0); // Center should be positive
1445    }
1446
1447    #[test]
1448    fn test_tukey_window() {
1449        let signal = Array1::ones(10);
1450        let windowed =
1451            apply_window(&signal.view(), WindowFunction::Tukey(0.5)).expect("Operation failed");
1452
1453        // Tukey window should have flat top in middle
1454        assert!(windowed[0] < windowed[5]); // Edge less than center
1455        assert!(windowed[9] < windowed[5]); // Edge less than center
1456    }
1457
1458    #[test]
1459    fn test_hadamard_transform() {
1460        let input = array![1.0, 1.0, 1.0, 1.0];
1461        let result = hadamard_transform(&input.view(), false).expect("Operation failed");
1462
1463        // For input [1,1,1,1], Hadamard transform should be [4,0,0,0]
1464        assert_relative_eq!(result[0], 4.0, epsilon = 1e-12);
1465        assert_relative_eq!(result[1], 0.0, epsilon = 1e-12);
1466        assert_relative_eq!(result[2], 0.0, epsilon = 1e-12);
1467        assert_relative_eq!(result[3], 0.0, epsilon = 1e-12);
1468
1469        // Test inverse
1470        let reconstructed = hadamard_transform(&result.view(), true).expect("Operation failed");
1471        for i in 0..4 {
1472            assert_relative_eq!(input[i], reconstructed[i], epsilon = 1e-12);
1473        }
1474    }
1475
1476    #[test]
1477    fn test_walsh_hadamard_transform() {
1478        let input = array![1.0, 0.0, 1.0, 0.0];
1479        let result = walsh_hadamard_transform(&input.view(), false).expect("Operation failed");
1480
1481        // Test that it produces a valid transform
1482        assert_eq!(result.len(), 4);
1483
1484        // Test inverse
1485        let reconstructed =
1486            walsh_hadamard_transform(&result.view(), true).expect("Operation failed");
1487        for i in 0..4 {
1488            assert_relative_eq!(input[i], reconstructed[i], epsilon = 1e-12);
1489        }
1490    }
1491
1492    #[test]
1493    fn test_fast_walsh_transform() {
1494        let input = array![1.0, -1.0, 1.0, -1.0];
1495        let result = fast_walsh_transform(&input.view(), false).expect("Operation failed");
1496
1497        // Test dimensions
1498        assert_eq!(result.len(), 4);
1499
1500        // Test inverse
1501        let reconstructed = fast_walsh_transform(&result.view(), true).expect("Operation failed");
1502        for i in 0..4 {
1503            assert_relative_eq!(input[i], reconstructed[i], epsilon = 1e-12);
1504        }
1505    }
1506
1507    #[test]
1508    fn test_hadamard_transform_properties() {
1509        // Test that Hadamard transform is involutory (self-inverse up to scaling)
1510        let input = array![1.0, 2.0, 3.0, 4.0];
1511        let transformed = hadamard_transform(&input.view(), false).expect("Operation failed");
1512        let twice_transformed =
1513            hadamard_transform(&transformed.view(), false).expect("Operation failed");
1514
1515        // H²x = n*x for unnormalized Hadamard transform
1516        let n = input.len() as f64;
1517        for i in 0..4 {
1518            assert_relative_eq!(twice_transformed[i], n * input[i], epsilon = 1e-12);
1519        }
1520    }
1521
1522    #[test]
1523    fn test_bit_reverse() {
1524        assert_eq!(bit_reverse(0, 3), 0); // 000 -> 000
1525        assert_eq!(bit_reverse(1, 3), 4); // 001 -> 100
1526        assert_eq!(bit_reverse(2, 3), 2); // 010 -> 010
1527        assert_eq!(bit_reverse(3, 3), 6); // 011 -> 110
1528        assert_eq!(bit_reverse(4, 3), 1); // 100 -> 001
1529    }
1530}