Skip to content
Open
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
37 changes: 37 additions & 0 deletions mne_bids_pipeline/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1243,6 +1243,43 @@
Regular expression used for searching for calibration events
"""


### Bad channel detection with PyPREP

# for parameter documentation see:
# https://pyprep.readthedocs.io/en/latest/generated/pyprep.NoisyChannels.html#pyprep.NoisyChannels

# run bad channel detection
pyprep_bad_chans: bool = False
## variables which determine which algos to run
# run all available algos. if True
pyprep_all_bads: bool = True
# params for find_all_bads, defaults to empty dict, i.e. pyprep defaults
pyprep_all_bads_params: dict = dict()
# run snr algo
pyprep_by_SNR: bool = False
# run the correlation algo
pyprep_by_correlation: bool = False
# params for correlation, defaults to empty dict, i.e. pyprep defaults
pyprep_by_correlation_params: dict = dict()
# run the deviation algo
pyprep_by_deviation: bool = False
# params for deviation, defaults to empty dict, i.e. pyprep defaults
pyprep_by_deviation_params: dict = dict()
# run the hfnoise algo
pyprep_by_hfnoise: bool = False
# params for hfnoise, defaults to empty dict, i.e. pyprep defaults
pyprep_by_hfnoise_params: dict = dict()
# run the nan flat algo
pyprep_by_nan_flat: bool = False
# params for nan_flat, defaults to empty dict, i.e. pyprep defaults
pyprep_by_nan_flat_params: dict = dict()
# run the ransac algo
pyprep_by_ransac: bool = False
# params for ransac, defaults to empty dict, i.e. pyprep defaults
pyprep_by_ransac_params: dict = dict()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ultimately we should have a useful default configuration. maybe the one defined in the original github-issue on mne-bids-pipeline is reasonable?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

he seems to just use the defaults?
mne-tools#835

Copy link
Member

@behinger behinger Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's too bad then, if it fails misserably for us :(

We have to find our own defaults then.



# ### SSP, ICA, and artifact regression

regress_artifact: dict[str, Any] | None = None
Expand Down
102 changes: 101 additions & 1 deletion mne_bids_pipeline/steps/preprocessing/_01_data_quality.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from mne_bids_pipeline._run import _prep_out_files, failsafe_run, save_logs
from mne_bids_pipeline._viz import plot_auto_scores
from mne_bids_pipeline.typing import FloatArrayT, InFilesT, OutFilesT
from pyprep import NoisyChannels


def get_input_fnames_data_quality(
Expand Down Expand Up @@ -141,7 +142,19 @@ def assess_data_quality(
session=session,
subject=subject,
)
_write_json(out_files["auto_scores"], auto_scores)

_write_json(out_files["auto_scores"], auto_scores)

# find bad channels with pyprep
if cfg.pyprep_bad_chans:
pyprep_bads = _find_bads_pyprep(cfg=cfg,
exec_params=exec_params,
raw=raw,
subject=subject,
session=session,
run=run,
task=task,
)

# Write the bad channels to disk.
out_files["bads_tsv"] = _bads_path(
Expand Down Expand Up @@ -173,6 +186,17 @@ def assess_data_quality(
bads_for_tsv.append(ch)
reasons.append(reason)

if cfg.pyprep_bad_chans:
for k, v in pyprep_bads.items():
for ch in v:
reason = (
f"pre-existing (before MNE-BIDS-pipeline was run) & pyprep {k}"
if ch in preexisting_bads
else f"pyrep {k}"
)
bads_for_tsv.append(ch)
reasons.append(reason)

if preexisting_bads:
for ch in preexisting_bads:
if ch in bads_for_tsv:
Expand Down Expand Up @@ -228,6 +252,22 @@ def assess_data_quality(
plt.close(fig)
else:
report.remove(title=title)
if cfg.pyprep_bad_chans:
raw.set_annotations(None) # annotations only bother us here
msg = "Adding pyprep bad channels to report"
logger.info(**gen_log_kwargs(message=msg))
for k, v in pyprep_bads.items():
raw.info["bads"] = v
if not v or k == "all_bads":
continue
_add_raw(
cfg=cfg,
report=report,
bids_path_in=bids_path_in,
title=f"Raw, algorithm {k}",
tags=("bad_channel",),
raw=raw,
)

assert len(in_files) == 0, in_files.keys()
return _prep_out_files(exec_params=exec_params, out_files=out_files)
Expand Down Expand Up @@ -306,6 +346,52 @@ def _find_bads_maxwell(

return auto_noisy_chs, auto_flat_chs, auto_scores

def _find_bads_pyprep(
*,
cfg: SimpleNamespace,
exec_params: SimpleNamespace,
raw: mne.io.BaseRaw,
subject: str,
session: str | None,
run: str | None,
task: str | None,
) -> tuple[list[str], list[str], dict[str, FloatArrayT]]:
msg = "Finding noisy channels with PyPREP."
logger.info(**gen_log_kwargs(message=msg))
noisy_chans = NoisyChannels(raw)
if cfg.pyprep_all_bads:
msg = "Running pyprep all bads"
logger.info(**gen_log_kwargs(message=msg))
noisy_chans.find_all_bads(**cfg.pyprep_all_bads_params)
else:
if cfg.pyprep_by_SNR:
msg = "Running pyprep SNR"
logger.info(**gen_log_kwargs(message=msg))
noisy_chans.find_bad_by_SNR()
if cfg.pyprep_by_correlation:
msg = "Running pyprep correlation"
logger.info(**gen_log_kwargs(message=msg))
noisy_chans.find_bad_by_correlation(**cfg.pyprep_by_correlation_params)
if cfg.pyprep_by_deviation:
msg = "Running pyprep deviation"
logger.info(**gen_log_kwargs(message=msg))
noisy_chans.find_bad_by_deviation(**cfg.pyprep_by_deviation_params)
if cfg.pyprep_by_hfnoise:
msg = "Running pyprep hfnoise"
logger.info(**gen_log_kwargs(message=msg))
noisy_chans.find_bad_by_hfnoise(**cfg.pyprep_by_hfnoise_params)
if cfg.pyprep_by_nan_flat:
msg = "Running pyprep nanflat"
logger.info(**gen_log_kwargs(message=msg))
noisy_chans.find_bad_by_nanflat(**cfg.pyprep_by_nanflat_params)
if cfg.pyprep_by_ransac:
msg = "Running pyprep ransac"
logger.info(**gen_log_kwargs(message=msg))
noisy_chans.find_bad_by_ransac(**cfg.pyprep_by_ransac_params)

bads = noisy_chans.get_bads(as_dict=True)
return bads


def get_config(
*,
Expand All @@ -332,6 +418,20 @@ def get_config(
# detection
# find_flat_channels_meg=config.find_flat_channels_meg,
# find_noisy_channels_meg=config.find_noisy_channels_meg,
pyprep_bad_chans=config.pyprep_bad_chans,
pyprep_all_bads=config.pyprep_all_bads,
pyprep_all_bads_params=config.pyprep_all_bads_params,
pyprep_by_SNR=config.pyprep_by_SNR,
pyprep_by_correlation=config.pyprep_by_correlation,
pyprep_by_correlation_params=config.pyprep_by_correlation_params,
pyprep_by_deviation=config.pyprep_by_deviation,
pyprep_by_deviation_params=config.pyprep_by_deviation_params,
pyprep_by_hfnoise=config.pyprep_by_hfnoise,
pyprep_by_hfnoise_params=config.pyprep_by_hfnoise_params,
pyprep_by_nan_flat=config.pyprep_by_nan_flat,
pyprep_by_nan_flat_params=config.pyprep_by_nan_flat_params,
pyprep_by_ransac=config.pyprep_by_ransac,
pyprep_by_ransac_params=config.pyprep_by_ransac_params,
**_import_data_kwargs(config=config, subject=subject),
**extra_kwargs,
)
Expand Down