Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions meegkit/asr.py
Original file line number Diff line number Diff line change
Expand Up @@ -487,11 +487,18 @@ def asr_calibrate(X, sfreq, cutoff=5, blocksize=100, win_len=0.5,
# set number of channels and number of samples
[nc, ns] = X.shape

# avoid propagating non-finite samples into the filter and thresholds
X = np.where(np.isfinite(X), X, 0.0)

# filter the data
X, _zf = yulewalk_filter(X, sfreq, ab=None)

# window length for calculating thresholds
N = int(np.round(win_len * sfreq))
if ns < N:
raise ValueError(
f"Calibration data has {ns} samples, shorter than one analysis "
f"window of {N} samples (win_len={win_len} at sfreq={sfreq}).")

U = block_covariance(X, window=blocksize, overlap=win_overlap,
estimator=estimator)
Expand All @@ -510,6 +517,11 @@ def asr_calibrate(X, sfreq, cutoff=5, blocksize=100, win_len=0.5,
# get the threshold matrix T
xsq = np.dot(V.T, X) ** 2
offsets = np.arange(0, ns - N, np.round(N * (1 - win_overlap))).astype(int)
if len(offsets) < 2:
raise ValueError(
f"Only {len(offsets)} window(s) available for threshold "
"estimation; at least 2 are required. Provide more calibration "
"data.")

# root mean squared amplitude per channel (windowed sums via cumulative sum)
csum = np.zeros((nc, ns + 1))
Expand Down
9 changes: 5 additions & 4 deletions meegkit/utils/asr.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,17 @@ def fit_eeg_distribution(X, min_clean_fraction=0.25, max_dropout_fraction=0.1,

Parameters
----------
X : array, shape=(n_channels, n_samples)
EEG data, possibly containing artifacts.
X : array, shape=(n_samples,)
1-D vector of amplitude values (e.g., per-window RMS), possibly
containing artifacts.
min_clean_fraction : float
Minimum fraction that needs to be clean. This is the minimum fraction
of time windows that need to contain essentially uncontaminated EEG
(default=0.1).
(default=0.25).
max_dropout_fraction : float
Maximum fraction that can have dropouts. This is the maximum fraction
of time windows that may have arbitrarily low amplitude (e.g., due to
the sensors being unplugged) (default=0.25).
the sensors being unplugged) (default=0.1).
fit_quantiles : 2-tuple
Quantile range [lower,upper] of the truncated generalized Gaussian
distribution that shall be fit to the EEG contents (default=[0.022
Expand Down
23 changes: 23 additions & 0 deletions tests/test_asr.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,29 @@ def test_asr_class(method, reref, show=False):
ASR(sfreq=125)
ASR(Sfreq=150)


def test_asr_calibrate_nonfinite_input():
"""Non-finite calibration samples must not silently yield NaN M/T."""
raw = np.load(os.path.join(THIS_FOLDER, "data", "eeg_raw.npy"))
sfreq = 250
train_idx = np.arange(5 * sfreq, 45 * sfreq, dtype=int)
X = raw[:, train_idx].copy()
X[0, 100] = np.nan
X[0, 200] = np.inf

M, T = asr_calibrate(X, sfreq)

assert np.all(np.isfinite(M))
assert np.all(np.isfinite(T))


def test_asr_calibrate_too_short():
"""Calibration data shorter than one analysis window should raise."""
X_short = rng.standard_normal((8, 50)) # N = round(0.5 * 250) = 125
with pytest.raises(ValueError, match="shorter than one analysis window"):
asr_calibrate(X_short, 250)


def test_asr_max_bad_chans_param():
"""max_bad_chans is exposed on ASR and defaults to 0.3."""
assert ASR().max_bad_chans == 0.3
Expand Down
Loading