Source code for mne_denoise.dss.selection
"""DSS component-selection helpers."""
from __future__ import annotations
import numpy as np
__all__ = [
"auto_select_components",
"auto_select_components_robust",
"detect_eigenvalue_knee",
"iterative_outlier_removal",
]
[docs]
def iterative_outlier_removal(scores: np.ndarray, sigma: float = 3.0) -> int:
"""Count values removed by iterative mean-plus-sigma thresholding.
At each iteration, values above ``mean + sigma * std`` are removed from the
remaining scores; the process stops when no value qualifies and returns the
number removed.
Parameters
----------
scores : array-like
Component scores.
sigma : float, default=3.0
Threshold multiplier.
Returns
-------
int
Number of removed scores.
"""
scores = np.asarray(scores)
n_outliers = 0
remaining = scores.copy()
while len(remaining) > 2:
mean_val = np.mean(remaining)
std_val = np.std(remaining)
if std_val < 1e-12:
break
threshold = mean_val + sigma * std_val
outliers = remaining > threshold
if not np.any(outliers):
break
n_outliers += np.sum(outliers)
remaining = remaining[~outliers]
return n_outliers
[docs]
def auto_select_components(eigenvalues: np.ndarray, threshold: float = 3.0) -> int:
"""Select a component count with :func:`iterative_outlier_removal`.
Parameters
----------
eigenvalues : array-like
DSS component scores.
threshold : float, default=3.0
Sigma threshold.
Returns
-------
int
Selected component count.
"""
return iterative_outlier_removal(eigenvalues, threshold)
[docs]
def detect_eigenvalue_knee(
scores: np.ndarray,
rel_floor: float = 0.01,
min_ratio: float = 3.0,
) -> int:
"""Select components above the largest qualifying score drop.
Parameters
----------
scores : array-like
Component scores in descending order.
rel_floor : float, default=0.01
Relative floor for valid drop anchors.
min_ratio : float, default=3.0
Minimum adjacent-score ratio.
Returns
-------
int
Number of scores above the knee, or zero if no qualifying knee exists.
Notes
-----
The drop is evaluated in log space with the relative-floor and minimum-ratio
gates. This is a package heuristic.
"""
scores = np.asarray(scores, dtype=float)
n = scores.size
if n < 2:
return int(n)
max_score = scores[0]
if max_score <= 0:
return 0
# log-space drops between consecutive eigenvalues (drops[i] = log10(s[i] / s[i+1]))
floor = max_score * 1e-12
log_scores = np.log10(np.clip(scores, floor, None))
drops = -np.diff(log_scores)
# Only consider drop positions anchored on a "still meaningful" eigenvalue
# (avoids picking the very last gap deep in the numerical noise tail).
anchor_threshold = max_score * rel_floor
valid = scores[:-1] > anchor_threshold
if not np.any(valid):
return 0
valid_drops = np.where(valid, drops, -np.inf)
knee_idx = int(np.argmax(valid_drops))
# Sanity gate: require a meaningful gap to avoid false positives on
# smoothly-decaying spectra.
if valid_drops[knee_idx] < np.log10(min_ratio):
return 0
return knee_idx + 1
[docs]
def auto_select_components_robust(
eigenvalues: np.ndarray,
sigma: float = 3.0,
knee_rel_floor: float = 0.01,
knee_min_ratio: float = 3.0,
) -> int:
"""Combine outlier and knee component counts and return the larger count.
Parameters
----------
eigenvalues : array-like
DSS component scores in descending order.
sigma : float, default=3.0
Outlier threshold multiplier.
knee_rel_floor : float, default=0.01
Relative knee floor.
knee_min_ratio : float, default=3.0
Minimum knee ratio.
Returns
-------
int
Larger of the two proposed counts.
"""
n_outlier = iterative_outlier_removal(eigenvalues, sigma=sigma)
n_knee = detect_eigenvalue_knee(
eigenvalues, rel_floor=knee_rel_floor, min_ratio=knee_min_ratio
)
return int(max(n_outlier, n_knee))