Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MRG] Allow marking channels as bad in existing datasets #491

Merged
merged 43 commits into from
Sep 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
b2e3516
Allow marking channels as bad in existing datsets
hoechenberger Aug 6, 2020
8ac6efc
Support channel name as string (not list of str)
hoechenberger Aug 6, 2020
a10d620
Raise if channel not in dataset
hoechenberger Aug 6, 2020
0a720a1
Update tests
hoechenberger Aug 6, 2020
bd4539e
Add `descriptions` kwarg
hoechenberger Aug 6, 2020
a314fe7
Typo
hoechenberger Aug 6, 2020
9f57c44
More tests
hoechenberger Aug 6, 2020
7f3e21f
Use kw-only arguments
hoechenberger Aug 6, 2020
602ad42
Use status_description default of `n/a`, ...
hoechenberger Aug 6, 2020
165c6b1
Remove unnecessary check
hoechenberger Aug 7, 2020
5bdc5c0
Add overwrite kwarg
hoechenberger Aug 7, 2020
02dd70f
Add examples
hoechenberger Aug 7, 2020
883224d
Expose func in mne_bids namespace, add to API docs
hoechenberger Aug 7, 2020
36c7593
Add changelog entry
hoechenberger Aug 7, 2020
8d81c7d
flake8
hoechenberger Aug 7, 2020
657b98b
Fix import order
hoechenberger Aug 7, 2020
0e8e85f
channels -> ch_names
hoechenberger Aug 7, 2020
15f253c
Improve descriptions docstring
hoechenberger Aug 7, 2020
cff8bd5
Remove space
hoechenberger Aug 7, 2020
25254e3
Phrasing
hoechenberger Aug 7, 2020
3fd8988
Better tests
hoechenberger Aug 7, 2020
671b362
Add CLI
hoechenberger Aug 7, 2020
f7ed71d
Allow bad channel reset via CLI
hoechenberger Aug 7, 2020
4f9407e
Docstring
hoechenberger Aug 8, 2020
7bb9fc5
Rework opt usage
hoechenberger Aug 8, 2020
f8021ac
Typo
hoechenberger Aug 8, 2020
952e763
Rework tests
hoechenberger Aug 9, 2020
d95c4cb
Clean up tests
hoechenberger Aug 9, 2020
b2ab5d5
Fix docstring
hoechenberger Aug 9, 2020
8ed972d
Rework logic, write info['bads'] too
hoechenberger Aug 11, 2020
80eadc2
Add example
hoechenberger Aug 9, 2020
b883e85
Add comment
hoechenberger Aug 11, 2020
29e0a3c
Merge branch 'master' of github.com:mne-tools/mne-bids into mark-bads
hoechenberger Aug 31, 2020
77fede1
Merge branch 'master' of github.com:mne-tools/mne-bids into mark-bads
hoechenberger Sep 1, 2020
b6a9a8f
Fully migrate to latest BIDSPath
hoechenberger Sep 1, 2020
3e24e65
Cleanup
hoechenberger Sep 1, 2020
7ee78f8
Merge branch 'master' of github.com:mne-tools/mne-bids into mark-bads
hoechenberger Sep 1, 2020
f9027aa
Fix CLI
hoechenberger Sep 1, 2020
e018ce6
Update example
hoechenberger Sep 1, 2020
8994f9e
Apply suggestions from code review
hoechenberger Sep 1, 2020
7508e28
Fix typo
hoechenberger Sep 1, 2020
c0b5f48
Typo
hoechenberger Sep 1, 2020
159bce8
Remove XXX
hoechenberger Sep 1, 2020
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
1 change: 1 addition & 0 deletions doc/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ MNE BIDS
make_dataset_description
make_report
write_anat
mark_bad_channels
get_head_mri_trans
get_anonymization_daysback
print_dir_tree
Expand Down
1 change: 1 addition & 0 deletions doc/whats_new.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Changelog
- :func:`mne_bids.read_raw_bids` and :func:`mne_bids.write_raw_bids` now map respiratory (``RESP``) channel types, by `Richard Höchenberger`_ (`#482 <https://github.com/mne-tools/mne-bids/pull/482>`_)
- When impedance values are available from a ``raw.impedances`` attribute, MNE-BIDS will now write an ``impedance`` column to ``*_electrodes.tsv`` files, by `Stefan Appelhoff`_ (`#484 <https://github.com/mne-tools/mne-bids/pull/484>`_)
- :func:`mne_bids.write_raw_bids` writes out status_description with 'n/a' values into the channels.tsv sidecar file, by `Adam Li`_ (`#489 <https://github.com/mne-tools/mne-bids/pull/489>`_)
- Added a new function :func:`mne_bids.mark_bad_channels` and command line interface ``mark_bad_channels`` which allows updating of the channel status (bad, good) and description of an existing BIDS dataset, by `Richard Höchenberger`_ (`#491 <https://github.com/mne-tools/mne-bids/pull/491>`_)

Bug
~~~
Expand Down
96 changes: 96 additions & 0 deletions examples/mark_bad_channels.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""
===============================================
10. Changing which channels are marked as "bad"
===============================================

You can use MNE-BIDS to mark MEG or (i)EEG recording channels as "bad", for
example if the connected sensor produced mostly noise – or no signal at
all.

Similarly, you can declare channels as "good", should you discover they were
incorrectly marked as bad.
"""

# Authors: Richard Höchenberger <richard.hoechenberger@gmail.com>
# License: BSD (3-clause)

###############################################################################
# We will demonstrate how to mark individual channels as bad on the MNE
# "sample" dataset. After that, we will mark channels as good again.
#
# Let's start by importing the required modules and functions, reading the
# "sample" data, and writing it in the BIDS format.

import os.path as op
import mne
from mne_bids import BIDSPath, write_raw_bids, read_raw_bids, mark_bad_channels

mne.set_log_level('error') # Suppress distracting log messages.

data_path = mne.datasets.sample.data_path()
raw_fname = op.join(data_path, 'MEG', 'sample', 'sample_audvis_raw.fif')
bids_root = op.join(data_path, '..', 'MNE-sample-data-bids')
bids_path = BIDSPath(subject='01', session='01', task='audiovisual', run='01',
root=bids_root)

raw = mne.io.read_raw_fif(raw_fname, verbose=False)
raw.info['line_freq'] = 60 # Specify power line frequency as required by BIDS.
write_raw_bids(raw, bids_path=bids_path, overwrite=True, verbose=False)

###############################################################################
# Read the (now BIDS-formatted) data and print a list of channels currently
# marked as bad.

raw = read_raw_bids(bids_path=bids_path, verbose=False)
print(f'The following channels are currently marked as bad:\n'
f' {", ".join(raw.info["bads"])}\n')

###############################################################################
# So currently, two channels are maked as bad: ``EEG 053`` and ``MEG 2443``.
# Let's assume that through visual data inspection, we found that two more
# MEG channels are problematic, and we would like to mark them as bad as well.
# To do that, we simply add them to a list, which we then pass to
# :func:`mne_bids.mark_bad_channels`:

bads = ['MEG 0112', 'MEG 0131']
mark_bad_channels(ch_names=bads, bids_path=bids_path, verbose=False)

###############################################################################
# That's it! Let's verify the result.

raw = read_raw_bids(bids_path=bids_path, verbose=False)
print(f'After marking MEG 0112 and MEG 0131 as bad, the following channels '
f'are now marked as bad:\n {", ".join(raw.info["bads"])}\n')

###############################################################################
# As you can see, now a total of **four** channels is marked as bad: the ones
# that were already bad when we started – ``EEG 053`` and ``MEG 2443`` – and
# the two channels we passed to :func:`mne_bids.mark_bad_channels` –
# ``MEG 0112`` and ``MEG 0131``. This shows that marking bad channels via
# :func:`mne_bids.mark_bad_channels`, by default, is an **additive** procedure,
# which allows you to mark additional channels as bad while retaining the
# information about all channels that had *previously* been marked as bad.
#
# If you instead would like to **replace** the collection of bad channels
# entirely, pass the argument ``overwrite=True``:

bads = ['MEG 0112', 'MEG 0131']
mark_bad_channels(ch_names=bads, bids_path=bids_path, overwrite=True,
verbose=False)

raw = read_raw_bids(bids_path=bids_path, verbose=False)
print(f'After marking MEG 0112 and MEG 0131 as bad and passing '
f'`overwrite=True`, the following channels '
f'are now marked as bad:\n {", ".join(raw.info["bads"])}\n')

###############################################################################
# Lastly, if you're looking for a way to mark all channels as good, simply
# pass an empty list as ``ch_names``, combined with ``overwrite=True``:

bads = []
mark_bad_channels(ch_names=bads, bids_path=bids_path, overwrite=True,
verbose=False)

raw = read_raw_bids(bids_path=bids_path, verbose=False)
print(f'After passing `ch_names=[]` and `overwrite=True`, the following '
f'channels are now marked as bad:\n {", ".join(raw.info["bads"])}\n')
2 changes: 1 addition & 1 deletion mne_bids/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@
from mne_bids.read import get_head_mri_trans, read_raw_bids
from mne_bids.utils import (get_anonymization_daysback)
from mne_bids.write import (make_dataset_description, write_anat,
write_raw_bids)
write_raw_bids, mark_bad_channels)
112 changes: 112 additions & 0 deletions mne_bids/commands/mne_bids_mark_bad_channels.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"""Mark channels in an existing BIDS dataset as "bad".

example usage:
$ mne_bids mark_bad_channels --ch_name="MEG 0112" --description="noisy" \
--ch_name="MEG 0131" --description="flat" \
--subject_id=01 --task=experiment --session=test \
hoechenberger marked this conversation as resolved.
Show resolved Hide resolved
--bids_root=bids_root --overwrite

"""
# Authors: Richard Höchenberger <richard.hoechenberger@gmail.com>
#
# License: BSD (3-clause)

from mne.utils import logger

import mne_bids
from mne_bids.config import reader
from mne_bids import BIDSPath, mark_bad_channels


def run():
"""Run the mark_bad_channels command."""
from mne.commands.utils import get_optparser

parser = get_optparser(__file__, usage="usage: %prog options args",
prog_prefix='mne_bids',
version=mne_bids.__version__)

parser.add_option('--ch_name', dest='ch_names', action='append',
default=[],
help='The names of the bad channels. If multiple '
'channels are bad, pass the --ch_name parameter '
'multiple times.')
parser.add_option('--description', dest='descriptions', action='append',
default=[],
help='Descriptions as to why the channels are bad. '
'Must match the number of bad channels provided. '
'Pass multiple times to supply more than one '
'value in that case.')
parser.add_option('--bids_root', dest='bids_root',
help='The path of the folder containing the BIDS '
'dataset')
parser.add_option('--subject_id', dest='subject',
help=('Subject name'))
parser.add_option('--session_id', dest='session',
help='Session name')
parser.add_option('--task', dest='task',
help='Task name')
parser.add_option('--acq', dest='acquisition',
help='Acquisition parameter')
parser.add_option('--run', dest='run',
help='Run number')
parser.add_option('--proc', dest='processing',
help='Processing label.')
parser.add_option('--rec', dest='recording',
help='Recording name')
parser.add_option('--type', dest='datatype',
help='Recording data type, e.g. meg, ieeg or eeg')
parser.add_option('--suffix', dest='suffix',
help='The filename suffix, i.e. the last part before '
'the extension')
parser.add_option('--ext', dest='extension',
help='The filename extension, including the leading '
'period, e.g. .fif')
parser.add_option('--overwrite', dest='overwrite', action='store_true',
help='Replace existing channel status entries')
parser.add_option('--verbose', dest='verbose', action='store_true',
help='Whether do generate additional diagnostic output')

opt, args = parser.parse_args()
if args:
parser.print_help()
parser.error(f'Please do not specify arguments without flags. '
f'Got: {args}.\n')

if opt.bids_root is None:
parser.print_help()
parser.error('You must specify bids_root')
if opt.ch_names is None:
parser.print_help()
parser.error('You must specify some --ch_name parameters.')

ch_names = [] if opt.ch_names == [''] else opt.ch_names
bids_path = BIDSPath(subject=opt.subject, session=opt.session,
task=opt.task, acquisition=opt.acquisition,
run=opt.run, processing=opt.processing,
recording=opt.recording, datatype=opt.datatype,
suffix=opt.suffix, extension=opt.extension,
root=opt.bids_root)

bids_paths = bids_path.match()
# Only keep data we can actually read & write.
allowed_extensions = list(reader.keys())
bids_paths = [p for p in bids_paths
if p.extension in allowed_extensions]

if not bids_paths:
logger.info('No matching files found. Please consider using a less '
'restrictive set of entities to broaden the search.')
return # XXX should be return with an error code?

logger.info(f'Marking channels {", ".join(ch_names)} as bad in '
f'{len(bids_paths)} recording(s) …')
for bids_path in bids_paths:
logger.info(f'Processing: {bids_path.basename}')
mark_bad_channels(ch_names=ch_names, descriptions=opt.descriptions,
bids_path=bids_path, overwrite=opt.overwrite,
verbose=opt.verbose)


if __name__ == '__main__': # pragma: no cover
run()
101 changes: 98 additions & 3 deletions mne_bids/commands/tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@
from mne.datasets import testing
from mne.utils import run_tests_if_main, ArgvSetter

from mne_bids.commands import mne_bids_raw_to_bids, mne_bids_cp
from mne_bids.commands import (mne_bids_raw_to_bids, mne_bids_cp,
mne_bids_mark_bad_channels)
from mne_bids import BIDSPath, read_raw_bids


base_path = op.join(op.dirname(mne.__file__), 'io')
subject_id = '01'
task = 'testing'
datatype = 'meg'


def check_usage(module, force_help=False):
Expand All @@ -49,15 +52,15 @@ def test_raw_to_bids(tmpdir):
# Should work
with ArgvSetter(('--subject_id', subject_id, '--task', task, '--raw',
raw_fname, '--bids_root', output_path,
'--line_freq', '60')):
'--line_freq', 60)):
mne_bids_raw_to_bids.run()

# Test EDF files as well
edf_data_path = op.join(base_path, 'edf', 'tests', 'data')
edf_fname = op.join(edf_data_path, 'test.edf')
with ArgvSetter(('--subject_id', subject_id, '--task', task, '--raw',
edf_fname, '--bids_root', output_path,
'--line_freq', '60')):
'--line_freq', 60)):
mne_bids_raw_to_bids.run()

# Too few input args
Expand Down Expand Up @@ -86,4 +89,96 @@ def test_cp(tmpdir):
mne_bids_cp.run()


def test_mark_bad_chanels_single_file(tmpdir):
"""Test mne_bids mark_bad_channels."""

# Check that help is printed
check_usage(mne_bids_mark_bad_channels)

# Create test dataset.
output_path = str(tmpdir)
data_path = testing.data_path()
raw_fname = op.join(data_path, 'MEG', 'sample',
'sample_audvis_trunc_raw.fif')
old_bads = mne.io.read_raw_fif(raw_fname).info['bads']
bids_path = BIDSPath(subject=subject_id, task=task, root=output_path,
datatype=datatype)

with ArgvSetter(('--subject_id', subject_id, '--task', task,
'--raw', raw_fname, '--bids_root', output_path,
'--line_freq', 60)):
mne_bids_raw_to_bids.run()

# Update the dataset.
ch_names = ['MEG 0112', 'MEG 0131']
descriptions = ['Really bad!', 'Even worse.']

args = ['--subject_id', subject_id, '--task', task,
'--bids_root', output_path, '--type', datatype]
for ch_name, description in zip(ch_names, descriptions):
args.extend(['--ch_name', ch_name])
args.extend(['--description', description])

args = tuple(args)
with ArgvSetter(args):
with pytest.warns(RuntimeWarning, match='The unit for chann*'):
mne_bids_mark_bad_channels.run()

# Check the data was properly written
raw = read_raw_bids(bids_path=bids_path)
assert set(old_bads + ch_names) == set(raw.info['bads'])

# Test resettig bad channels.
args = ('--subject_id', subject_id, '--task', task,
'--bids_root', output_path, '--type', datatype,
'--ch_name', '', '--overwrite')
with ArgvSetter(args):
mne_bids_mark_bad_channels.run()

# Check the data was properly written
raw = read_raw_bids(bids_path=bids_path)
assert raw.info['bads'] == []


def test_mark_bad_chanels_multiple_files(tmpdir):
"""Test mne_bids mark_bad_channels."""

# Check that help is printed
check_usage(mne_bids_mark_bad_channels)

# Create test dataset.
output_path = str(tmpdir)
data_path = testing.data_path()
raw_fname = op.join(data_path, 'MEG', 'sample',
'sample_audvis_trunc_raw.fif')
old_bads = mne.io.read_raw_fif(raw_fname).info['bads']
bids_path = BIDSPath(task=task, root=output_path, datatype=datatype)

subjects = ['01', '02', '03']
for subject in subjects:
with ArgvSetter(('--subject_id', subject, '--task', task,
'--raw', raw_fname, '--bids_root', output_path,
'--line_freq', 60)):
mne_bids_raw_to_bids.run()

# Update the dataset.
ch_names = ['MEG 0112', 'MEG 0131']
descriptions = ['Really bad!', 'Even worse.']

args = ['--task', task, '--bids_root', output_path, '--type', datatype]
for ch_name, description in zip(ch_names, descriptions):
args.extend(['--ch_name', ch_name])
args.extend(['--description', description])

args = tuple(args)
with ArgvSetter(args):
with pytest.warns(RuntimeWarning, match='The unit for chann*'):
mne_bids_mark_bad_channels.run()

# Check the data was properly written
for subject in subjects:
raw = read_raw_bids(bids_path=bids_path.copy().update(subject=subject))
assert set(old_bads + ch_names) == set(raw.info['bads'])


run_tests_if_main()
Loading