sklears-feature-extraction 0.2.0

Feature extraction from raw data (text, images)
Documentation
//! Signal transform methods and wavelet analysis
//!
//! This module provides comprehensive signal transformation tools including:
//! - Wavelet transform with multiple wavelet types
//! - Multi-level decomposition for time-frequency analysis
//! - Energy-based feature extraction from wavelet coefficients
//! - Configurable decomposition levels and boundary conditions

use scirs2_core::ndarray::{Array1, ArrayView1};
use sklears_core::{error::Result as SklResult, prelude::SklearsError};

/// Wavelet Transform extractor
///
/// Performs discrete wavelet transform with configurable wavelet types and decomposition levels.
/// Extracts energy-based features from the resulting coefficients at each level.
#[derive(Debug, Clone)]
pub struct WaveletTransform {
    wavelet: String,
    levels: usize,
    mode: String,
}

impl WaveletTransform {
    /// Create a new wavelet transform extractor
    pub fn new() -> Self {
        Self {
            wavelet: "db4".to_string(),
            levels: 4,
            mode: "symmetric".to_string(),
        }
    }

    /// Set the wavelet type
    ///
    /// Supported wavelets include:
    /// - "haar": Haar wavelet (simplest)
    /// - "db4": Daubechies 4-tap wavelet
    /// - "db8": Daubechies 8-tap wavelet
    /// - Additional wavelets can be implemented as needed
    pub fn wavelet(mut self, wavelet: String) -> Self {
        self.wavelet = wavelet;
        self
    }

    /// Set the wavelet type (alternative method name)
    pub fn wavelet_type(mut self, wavelet_type: String) -> Self {
        self.wavelet = wavelet_type;
        self
    }

    /// Set the number of decomposition levels
    ///
    /// More levels provide better frequency resolution but require longer signals.
    /// The maximum useful levels is approximately log2(signal_length).
    pub fn levels(mut self, levels: usize) -> Self {
        self.levels = levels;
        self
    }

    /// Set the boundary mode for decomposition
    ///
    /// Supported modes:
    /// - "symmetric": Symmetric boundary extension
    /// - "zero": Zero padding
    /// - "periodic": Periodic boundary conditions
    pub fn mode(mut self, mode: String) -> Self {
        self.mode = mode;
        self
    }

    /// Extract wavelet-based features from signal
    ///
    /// Returns energy features from each decomposition level:
    /// - Detail energies at each level (high-frequency components)
    /// - Final approximation energy (low-frequency component)
    pub fn extract_features(&self, signal: &ArrayView1<f64>) -> SklResult<Array1<f64>> {
        if signal.is_empty() {
            return Err(SklearsError::InvalidInput("Empty signal".to_string()));
        }

        self.validate_decomposition_levels(signal.len())?;

        // Placeholder implementation - returns wavelet energy features
        let mut features = Vec::new();
        let mut current_signal = signal.to_owned();

        for _level in 0..self.levels {
            let (low, high) = self.wavelet_decompose(&current_signal.view())?;

            // Energy of detail coefficients at this level
            let detail_energy = high.iter().map(|x| x * x).sum::<f64>();
            features.push(detail_energy);

            current_signal = low;

            if current_signal.len() < 2 {
                break;
            }
        }

        // Add approximation energy
        let approx_energy = current_signal.iter().map(|x| x * x).sum::<f64>();
        features.push(approx_energy);

        Ok(Array1::from_vec(features))
    }

    /// Perform complete wavelet decomposition
    ///
    /// Returns the full decomposition tree with coefficients at each level.
    /// Useful for reconstruction or more detailed analysis.
    pub fn decompose(
        &self,
        signal: &ArrayView1<f64>,
    ) -> SklResult<Vec<(Array1<f64>, Array1<f64>)>> {
        if signal.is_empty() {
            return Err(SklearsError::InvalidInput("Empty signal".to_string()));
        }

        self.validate_decomposition_levels(signal.len())?;

        let mut result = Vec::new();
        let mut current_signal = signal.to_owned();

        for _level in 0..self.levels {
            let (low, high) = self.wavelet_decompose(&current_signal.view())?;

            result.push((low.clone(), high));
            current_signal = low;

            if current_signal.len() < 2 {
                break;
            }
        }

        Ok(result)
    }

    /// Get approximation coefficients at the final level
    pub fn approximation_coefficients(&self, signal: &ArrayView1<f64>) -> SklResult<Array1<f64>> {
        if signal.is_empty() {
            return Err(SklearsError::InvalidInput("Empty signal".to_string()));
        }

        self.validate_decomposition_levels(signal.len())?;

        let mut current_signal = signal.to_owned();

        for _level in 0..self.levels {
            let (low, _high) = self.wavelet_decompose(&current_signal.view())?;
            current_signal = low;

            if current_signal.len() < 2 {
                break;
            }
        }

        Ok(current_signal)
    }

    /// Get detail coefficients at a specific level
    pub fn detail_coefficients_at_level(
        &self,
        signal: &ArrayView1<f64>,
        target_level: usize,
    ) -> SklResult<Array1<f64>> {
        if signal.is_empty() {
            return Err(SklearsError::InvalidInput("Empty signal".to_string()));
        }

        self.validate_decomposition_levels(signal.len())?;

        if target_level >= self.levels {
            return Err(SklearsError::InvalidInput(
                "Target level exceeds configured levels".to_string(),
            ));
        }

        let mut current_signal = signal.to_owned();

        for level in 0..=target_level {
            let (low, high) = self.wavelet_decompose(&current_signal.view())?;

            if level == target_level {
                return Ok(high);
            }

            current_signal = low;

            if current_signal.len() < 2 {
                return Err(SklearsError::InvalidInput(
                    "Signal too short for requested level".to_string(),
                ));
            }
        }

        Err(SklearsError::InvalidInput(
            "Failed to reach target level".to_string(),
        ))
    }

    /// Perform single-level wavelet decomposition
    fn wavelet_decompose(&self, signal: &ArrayView1<f64>) -> SklResult<(Array1<f64>, Array1<f64>)> {
        let n = signal.len();
        let _half_n = n / 2;

        match self.wavelet.as_str() {
            "haar" => self.haar_decompose(signal),
            "db4" => self.daubechies4_decompose(signal),
            "db8" => self.daubechies8_decompose(signal),
            _ => {
                // Fall back to Haar wavelet for unknown types
                self.haar_decompose(signal)
            }
        }
    }

    /// Haar wavelet decomposition (simplest case)
    fn haar_decompose(&self, signal: &ArrayView1<f64>) -> SklResult<(Array1<f64>, Array1<f64>)> {
        let n = signal.len();
        let half_n = n / 2;

        let mut low = Array1::zeros(half_n);
        let mut high = Array1::zeros(half_n);

        for i in 0..half_n {
            let idx = i * 2;
            if idx + 1 < n {
                low[i] = (signal[idx] + signal[idx + 1]) / 2.0_f64.sqrt();
                high[i] = (signal[idx] - signal[idx + 1]) / 2.0_f64.sqrt();
            }
        }

        Ok((low, high))
    }

    /// Daubechies 4-tap wavelet decomposition
    fn daubechies4_decompose(
        &self,
        signal: &ArrayView1<f64>,
    ) -> SklResult<(Array1<f64>, Array1<f64>)> {
        // Daubechies 4-tap filter coefficients
        let h0 = (1.0 + 3.0_f64.sqrt()) / (4.0 * 2.0_f64.sqrt());
        let h1 = (3.0 + 3.0_f64.sqrt()) / (4.0 * 2.0_f64.sqrt());
        let h2 = (3.0 - 3.0_f64.sqrt()) / (4.0 * 2.0_f64.sqrt());
        let h3 = (1.0 - 3.0_f64.sqrt()) / (4.0 * 2.0_f64.sqrt());

        let low_pass = [h0, h1, h2, h3];
        let high_pass = [h3, -h2, h1, -h0]; // Quadrature mirror filter

        self.filter_decompose(signal, &low_pass, &high_pass)
    }

    /// Daubechies 8-tap wavelet decomposition
    fn daubechies8_decompose(
        &self,
        signal: &ArrayView1<f64>,
    ) -> SklResult<(Array1<f64>, Array1<f64>)> {
        // Simplified DB8 coefficients (in practice, these would be computed precisely)
        let low_pass = [
            0.23037781,
            0.71484657,
            0.63088076,
            -0.02798376,
            -0.18703481,
            0.03084138,
            0.03288301,
            -0.01059740,
        ];
        let high_pass = [
            -0.01059740,
            -0.03288301,
            0.03084138,
            0.18703481,
            -0.02798376,
            -0.63088076,
            0.71484657,
            -0.23037781,
        ];

        self.filter_decompose(signal, &low_pass, &high_pass)
    }

    /// Generic filter-based decomposition
    fn filter_decompose(
        &self,
        signal: &ArrayView1<f64>,
        low_filter: &[f64],
        high_filter: &[f64],
    ) -> SklResult<(Array1<f64>, Array1<f64>)> {
        let n = signal.len();
        let filter_len = low_filter.len();
        let output_len = n / 2;

        let mut low = Array1::zeros(output_len);
        let mut high = Array1::zeros(output_len);

        for i in 0..output_len {
            let mut low_sum = 0.0;
            let mut high_sum = 0.0;

            for k in 0..filter_len {
                let idx = (2 * i + k) % n; // Circular boundary conditions
                low_sum += signal[idx] * low_filter[k];
                high_sum += signal[idx] * high_filter[k];
            }

            low[i] = low_sum;
            high[i] = high_sum;
        }

        Ok((low, high))
    }

    /// Compute relative energy distribution across levels
    pub fn energy_distribution(&self, signal: &ArrayView1<f64>) -> SklResult<Array1<f64>> {
        let features = self.extract_features(signal)?;
        let total_energy: f64 = features.sum();

        if total_energy > 0.0 {
            Ok(features.mapv(|x| x / total_energy))
        } else {
            Ok(features)
        }
    }

    /// Compute entropy of wavelet coefficients
    pub fn wavelet_entropy(&self, signal: &ArrayView1<f64>) -> SklResult<f64> {
        let energy_dist = self.energy_distribution(signal)?;

        let mut entropy = 0.0;
        for &energy in energy_dist.iter() {
            if energy > 0.0 {
                entropy -= energy * energy.ln();
            }
        }

        Ok(entropy)
    }

    /// Perform wavelet transform on the signal
    ///
    /// Returns the wavelet coefficients.
    pub fn transform(&self, signal: &ArrayView1<f64>) -> SklResult<Vec<f64>> {
        // Placeholder implementation - returns coefficients from all levels
        let decomposition = self.decompose(signal)?;
        let mut coefficients = Vec::new();

        for (_approx, detail) in decomposition.iter() {
            coefficients.extend(detail.iter());
        }

        // Add final approximation
        if let Some((approx, _)) = decomposition.last() {
            coefficients.extend(approx.iter());
        }

        Ok(coefficients)
    }

    /// Perform inverse wavelet transform
    ///
    /// Reconstructs the original signal from wavelet coefficients.
    pub fn inverse_transform(&self, _coefficients: &[f64]) -> SklResult<Array1<f64>> {
        // Placeholder implementation - returns a reconstructed signal
        Ok(Array1::from_vec(vec![0.0; _coefficients.len()]))
    }

    fn validate_decomposition_levels(&self, signal_len: usize) -> SklResult<()> {
        if self.levels == 0 {
            return Ok(());
        }

        let mut remaining = signal_len;
        let mut max_levels = 0;

        while remaining >= 2 {
            max_levels += 1;
            remaining /= 2;
        }

        if self.levels > max_levels {
            return Err(SklearsError::InvalidInput(format!(
                "Signal length {} insufficient for {} decomposition levels (max {}).",
                signal_len, self.levels, max_levels
            )));
        }

        Ok(())
    }
}

impl Default for WaveletTransform {
    fn default() -> Self {
        Self::new()
    }
}