"""Nonlinearities for iterative DSS."""
from __future__ import annotations
import numpy as np
from .base import NonlinearDenoiser
[docs]
class TanhMaskDenoiser(NonlinearDenoiser):
"""Scaled hyperbolic-tangent nonlinearity.
The transform is ``tanh(alpha * source)``. When ``normalize=True``, the
source is divided by its standard deviation before the transform and the
result is rescaled by the same value.
Parameters
----------
alpha : float, default=1.0
Tanh scale.
normalize : bool, default=True
Whether to standardize and rescale the source around the transform.
"""
def __init__(
self,
alpha: float = 1.0,
*,
normalize: bool = True,
) -> None:
self.alpha = alpha
self.normalize = normalize
[docs]
def denoise(self, source: np.ndarray) -> np.ndarray:
"""Apply the scaled hyperbolic-tangent nonlinearity.
Parameters
----------
source : ndarray, shape (n_times,) or (n_times, n_epochs)
Source time series. Normalization, when enabled, is computed over
the supplied values.
Returns
-------
denoised : ndarray, same shape as ``source``
``tanh(alpha * source)`` with optional standard-deviation scaling.
"""
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):
"""Residual hyperbolic-tangent nonlinearity.
The transform is ``source - tanh(alpha * source)``.
Parameters
----------
alpha : float, default=1.0
Tanh scale.
"""
def __init__(self, alpha: float = 1.0) -> None:
self.alpha = alpha
[docs]
def denoise(self, source: np.ndarray) -> np.ndarray:
"""Apply the robust hyperbolic-tangent nonlinearity.
Parameters
----------
source : ndarray, shape (n_times,) or (n_times, n_epochs)
Source time series.
Returns
-------
denoised : ndarray, same shape as ``source``
``source - tanh(alpha * source)``.
"""
return source - np.tanh(self.alpha * source)
[docs]
class GaussDenoiser(NonlinearDenoiser):
"""Gaussian nonlinearity for iterative DSS.
The transform is ``source * exp(-a * source**2 / 2)``.
Parameters
----------
a : float, default=1.0
Gaussian scale.
"""
def __init__(self, a: float = 1.0) -> None:
self.a = a
[docs]
def denoise(self, source: np.ndarray) -> np.ndarray:
"""Apply the Gaussian FastICA nonlinearity.
Parameters
----------
source : ndarray, shape (n_times,) or (n_times, n_epochs)
Source time series.
Returns
-------
denoised : ndarray, same shape as ``source``
``source * exp(-a * source**2 / 2)``.
"""
s2 = source**2
return source * np.exp(-self.a * s2 / 2)
[docs]
class SkewDenoiser(NonlinearDenoiser):
"""Squared-source nonlinearity for iterative DSS.
The transform is ``source**2``.
"""
[docs]
def denoise(self, source: np.ndarray) -> np.ndarray:
"""Apply the squared-source skewness nonlinearity.
Parameters
----------
source : ndarray, shape (n_times,) or (n_times, n_epochs)
Source time series.
Returns
-------
denoised : ndarray, same shape as ``source``
Element-wise squared source.
"""
return source**2
[docs]
class KurtosisDenoiser(NonlinearDenoiser):
"""Configurable ICA contrast nonlinearity.
Parameters
----------
nonlinearity : {"tanh", "cube", "gauss"}, default="tanh"
Transform to apply: ``tanh(alpha * source)``, ``source**3``, or
``source * exp(-0.5 * (alpha * source)**2)``.
alpha : float, default=1.0
Scale used by the ``"tanh"`` and ``"gauss"`` transforms.
"""
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
[docs]
def denoise(self, source: np.ndarray) -> np.ndarray:
"""Apply the configured ICA contrast nonlinearity.
Parameters
----------
source : ndarray, shape (n_times,) or (n_times, n_epochs)
Source time series.
Returns
-------
denoised : ndarray, same shape as ``source``
Transformed source using ``tanh``, ``cube``, or ``gauss``.
"""
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)
[docs]
class SmoothTanhDenoiser(NonlinearDenoiser):
"""Uniformly smoothed hyperbolic-tangent nonlinearity.
Parameters
----------
alpha : float, default=1.0
Tanh scale.
window : int, default=10
Uniform-filter size; values below 3 are set to 3.
Notes
-----
The implementation applies :func:`scipy.ndimage.uniform_filter1d` with its
default last-axis behavior before applying ``tanh``.
"""
def __init__(self, alpha: float = 1.0, window: int = 10) -> None:
self.alpha = alpha
self.window = max(3, window)
[docs]
def denoise(self, source: np.ndarray) -> np.ndarray:
"""Smooth a source and apply the scaled tanh nonlinearity.
Parameters
----------
source : ndarray, shape (n_times,) or (n_times, n_epochs)
Source time series. Smoothing is applied along the time axis.
Returns
-------
denoised : ndarray, same shape as ``source``
Smoothed and nonlinearly transformed source.
"""
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)
# =============================================================================
[docs]
def beta_tanh(source: np.ndarray) -> float:
"""Return the tanh fixed-point coefficient.
The coefficient is ``-mean(1 - tanh(source)**2)``.
Parameters
----------
source : ndarray
Source samples.
"""
return -np.mean(1 - np.tanh(source) ** 2)
[docs]
def beta_pow3(source: np.ndarray) -> float:
"""Return the cubic fixed-point coefficient ``-3.0``.
Parameters
----------
source : ndarray
Source samples; used only to match the fixed-point coefficient API.
"""
return -3.0
[docs]
def beta_gauss(source: np.ndarray, a: float = 1.0) -> float:
"""Return the Gaussian fixed-point coefficient.
The coefficient is ``-mean((1 - a * source**2) * exp(-a * source**2 / 2))``.
Parameters
----------
source : ndarray
Source samples.
a : float, default=1.0
Gaussian scale.
"""
s2 = source**2
return -np.mean((1 - a * s2) * np.exp(-a * s2 / 2))