Source code for mne_denoise.dss.denoisers.ica

"""ICA-based nonlinearities for DSS (FastICA equivalence).

This module implements nonlinear denoising functions $f(s)$ that correspond to
maximizing non-Gaussianity (negentropy, kurtosis), effectively making DSS
equivalent to FastICA or RobustICA.

Authors: Sina Esmaeili (sina.esmaeili@umontreal.ca)
         Hamza Abdelhedi (hamza.abdelhedi@umontreal.ca)

References
----------
.. [1] Särelä & Valpola (2005). Denoising Source Separation. J. Mach. Learn. Res., 6, 233-272.
.. [2] Hyvärinen, A. (1999). Fast and robust fixed-point algorithms for independent
       component analysis. IEEE Trans. Neural Netw., 10(3), 626-634.
"""

from __future__ import annotations

import numpy as np

from .base import NonlinearDenoiser


[docs] class TanhMaskDenoiser(NonlinearDenoiser): r"""Tanh mask denoiser (Standard FastICA nonlinearity). Implements the hyperbolic tangent nonlinearity used widely in ICA for super-Gaussian source extraction. It is robust to outliers compared to kurtosis ($s^3$). Formula: $s_{new} = \\tanh(\\alpha \\cdot s)$ Parameters ---------- alpha : float Scaling factor controlling the saturation slope. Default 1.0. normalize : bool If True, normalizes the source to unit variance before applying tanh, then rescales back. This ensures $\\alpha=1$ has a consistent meaning. Default True. Examples -------- >>> # Use for robust ICA >>> from mne_denoise.dss.denoisers import TanhMaskDenoiser, beta_tanh >>> denoiser = TanhMaskDenoiser() >>> dss = IterativeDSS(denoiser=denoiser, beta=beta_tanh) References ---------- Särelä & Valpola (2005). Section 4.2.2 "BETTER ESTIMATE FOR THE SIGNAL VARIANCE" """
[docs] def __init__( self, alpha: float = 1.0, *, normalize: bool = True, ) -> None: self.alpha = alpha self.normalize = normalize
def denoise(self, source: np.ndarray) -> np.ndarray: """Apply tanh nonlinearity.""" if self.normalize: std = np.std(source) if std > 1e-15: source_scaled = source / std denoised = np.tanh(self.alpha * source_scaled) return denoised * std else: return source return np.tanh(self.alpha * source)
[docs] class RobustTanhDenoiser(NonlinearDenoiser): r"""Robust tanh denoiser (FastICA / RobustICA formulation). Implements: $s_{new} = s - \\tanh(\\alpha \\cdot s)$ This form is often used in deflationary FastICA schemas (like `pow3`) where strictly structure relates to optimizing specific cost functions (like negentropy). Parameters ---------- alpha : float Scaling factor. Default 1.0. Examples -------- >>> # Use for robust ICA >>> from mne_denoise.dss.denoisers import RobustTanhDenoiser, beta_tanh >>> denoiser = RobustTanhDenoiser() >>> dss = IterativeDSS(denoiser=denoiser, beta=beta_tanh) References ---------- Särelä & Valpola (2005). Section 4.2.2 "BETTER ESTIMATE FOR THE SIGNAL VARIANCE" """
[docs] def __init__(self, alpha: float = 1.0) -> None: self.alpha = alpha
def denoise(self, source: np.ndarray) -> np.ndarray: """Apply robust tanh denoising.""" return source - np.tanh(self.alpha * source)
[docs] class GaussDenoiser(NonlinearDenoiser): r"""Gaussian nonlinearity (FastICA 'gauss'). Implements: $s_{new} = s \\cdot \\exp(-a s^2 / 2)$ This nonlinearity is robust and works well for super-Gaussian distributions but is also capable of separating sub-Gaussian sources depending on the sign of kurtosis. It corresponds to the derivative of the Gaussian function. Parameters ---------- a : float Parameter 'a_1' in FastICA literature. Default 1.0. Examples -------- >>> # Use for robust ICA >>> from mne_denoise.dss.denoisers import GaussDenoiser, beta_gauss >>> denoiser = GaussDenoiser() >>> dss = IterativeDSS(denoiser=denoiser, beta=beta_gauss) References ---------- Särelä & Valpola (2005). Section 4.2.2 "BETTER ESTIMATE FOR THE SIGNAL VARIANCE" """
[docs] def __init__(self, a: float = 1.0) -> None: self.a = a
def denoise(self, source: np.ndarray) -> np.ndarray: """Apply Gaussian nonlinearity.""" s2 = source**2 return source * np.exp(-self.a * s2 / 2)
[docs] class SkewDenoiser(NonlinearDenoiser): """Skewness nonlinearity (FastICA 'skew'). Implements: $s_{new} = s^2$ Used for extracting sources with asymmetric probability distributions. This maximizes skewness rather than kurtosis. Examples -------- >>> # Use for robust ICA >>> from mne_denoise.dss.denoisers import SkewDenoiser, beta_gauss >>> denoiser = SkewDenoiser() >>> dss = IterativeDSS(denoiser=denoiser, beta=beta_gauss) References ---------- Särelä & Valpola (2005). Section 4.2.2 "BETTER ESTIMATE FOR THE SIGNAL VARIANCE" """ def denoise(self, source: np.ndarray) -> np.ndarray: """Apply skewness ($s^2$).""" return source**2
[docs] class KurtosisDenoiser(NonlinearDenoiser): """Kurtosis maximization denoiser. Can wrap different nonlinearities ('tanh', 'cube', 'gauss') to maximize non-Gaussianity. Included for checking various ICA contrasts. Parameters ---------- nonlinearity : {'tanh', 'cube', 'gauss'} The function $g(s)$ to use. 'cube' ($s^3$) is the classic kurtosis maximization. alpha : float Scaling parameter. Examples -------- >>> from mne_denoise.dss.denoisers import KurtosisDenoiser >>> denoiser = KurtosisDenoiser(nonlinearity="cube") >>> denoised = denoiser.denoise(source) References ---------- Särelä & Valpola (2005). Section 4.2.1 "KURTOSIS-BASED ICA" """
[docs] def __init__( self, nonlinearity: str = "tanh", alpha: float = 1.0, ) -> None: if nonlinearity not in ("tanh", "cube", "gauss"): raise ValueError(f"Unknown nonlinearity: {nonlinearity}") self.nonlinearity = nonlinearity self.alpha = alpha
def denoise(self, source: np.ndarray) -> np.ndarray: """Apply nonlinearity.""" if self.nonlinearity == "tanh": return np.tanh(self.alpha * source) elif self.nonlinearity == "cube": return source**3 else: # self.nonlinearity == 'gauss' (validated in __init__) return source * np.exp(-0.5 * (self.alpha * source) ** 2)
class SmoothTanhDenoiser(NonlinearDenoiser): r"""Smoothed tanh denoiser. Applies temporal smoothing before the tanh nonlinearity. This can help extract sources with both temporal structure and non-Gaussian statistics. Formula: $s_{smoothed} = \\text{lowpass}(s)$ $s_{new} = \\tanh(\\alpha \\cdot s_{smoothed})$ Parameters ---------- alpha : float Scaling factor for tanh. Default 1.0. window : int Smoothing window size in samples. Default 10. Examples -------- >>> from mne_denoise.dss.denoisers import SmoothTanhDenoiser >>> denoiser = SmoothTanhDenoiser(window=20) >>> dss = IterativeDSS(denoiser=denoiser) """ def __init__(self, alpha: float = 1.0, window: int = 10) -> None: self.alpha = alpha self.window = max(3, window) def denoise(self, source: np.ndarray) -> np.ndarray: """Apply smoothed tanh nonlinearity.""" from scipy.ndimage import uniform_filter1d # Smooth the source smoothed = uniform_filter1d(source, size=self.window, mode="reflect") # Apply tanh to smoothed signal return np.tanh(self.alpha * smoothed) # ============================================================================= # Helper functions for beta (Newton step) # ============================================================================= def beta_tanh(source: np.ndarray) -> float: r"""Compute beta for Tanh denoiser (FastICA Newton step). Formula: $\\beta = -E[1 - \\tanh^2(s)]$ Legacy: `beta_tanh.m` Returns ------- beta : float Scalar value. """ return -np.mean(1 - np.tanh(source) ** 2) def beta_pow3(source: np.ndarray) -> float: r"""Compute beta for Cubic ($s^3$) denoiser. Formula: $\\beta = -3$ (constant expectation for $g(s)=s^3$, $g'(s)=3s^2$, assuming unit var) Actually for $g(s)=s^3$, $g'(s)=3s^2$. $E[3s^2] = 3 E[s^2] = 3$ (if whitened). So $\\beta = -3$. Legacy: `beta_pow3.m` """ return -3.0 def beta_gauss(source: np.ndarray, a: float = 1.0) -> float: r"""Compute beta for Gaussian denoiser. Formula: $\\beta = -E[(1 - a s^2) \\exp(-a s^2 / 2)]$ Legacy: `beta_gauss.m` """ s2 = source**2 return -np.mean((1 - a * s2) * np.exp(-a * s2 / 2))