diff --git a/.circleci/config.yml b/.circleci/config.yml index d8f9eed14..75f8183df 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -20,7 +20,7 @@ jobs: source activate testenv conda install --yes --quiet numpy=1.13.1 scipy=0.19.1 matplotlib pandas=0.20.3 pip install sphinx numpydoc sphinx-gallery sphinx_bootstrap_theme pillow - pip install mne + pip install -U https://api.github.com/repos/mne-tools/mne-python/zipball/master python setup.py develop - run: name: Build the documentation diff --git a/.travis.yml b/.travis.yml index 15b40ec42..7ae9d1e47 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,7 @@ install: - conda install --yes --quiet numpy=1.13.1 scipy=0.19.1 matplotlib pandas=0.20.3 - pip install flake8 pytest pytest-sugar pytest-faulthandler - | - git clone --depth 1 https://github.com/mne-tools/mne-python.git --branch maint/0.16 + git clone --depth 1 https://github.com/mne-tools/mne-python.git cd mne-python pip install --no-deps -e . cd ../ diff --git a/README.md b/README.md index 4245a816a..df1b92791 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,8 @@ Next to `numpy`, `scipy`, and `matplotlib` that are included in the standard anaconda distribution, you will need to install the following dependencies to be able to use `mne_bids`: - $ pip install pandas mne + $ pip install pandas + $ pip install -U https://api.github.com/repos/mne-tools/mne-python/zipball/master Then install `mne_bids`: diff --git a/appveyor.yml b/appveyor.yml index 414896aad..328e80262 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -19,7 +19,7 @@ install: - "pip uninstall -yq mne" - |- cd .. - git clone --depth 1 https://github.com/mne-tools/mne-python.git --branch maint/0.16 + git clone --depth 1 https://github.com/mne-tools/mne-python.git cd mne-python python setup.py develop cd .. diff --git a/doc/api.rst b/doc/api.rst index af0613bd0..0c1bd293b 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -13,7 +13,9 @@ MNE BIDS (:py:mod:`mne_bids.mne_bids`): .. autosummary:: :toctree: generated/ - raw_to_bids + write_raw_bids + make_bids_basename + make_bids_folders Utils (:py:mod:`mne_bids.utils`): @@ -23,10 +25,9 @@ Utils (:py:mod:`mne_bids.utils`): :toctree: generated/ print_dir_tree - make_bids_folders - make_bids_filename make_dataset_description copyfile_brainvision + copyfile_eeglab Datasets (:py:mod:`mne_bids.datasets`): diff --git a/doc/index.rst b/doc/index.rst index 89b89b1bd..9be6095d5 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -13,21 +13,37 @@ Installation We recommend the `Anaconda Python distribution `_. To install ``mne_bids``, you first need to install its dependencies:: - $ pip install pandas mne + $ pip install pandas + $ pip install -U https://api.github.com/repos/mne-tools/mne-python/zipball/master Then install mne_bids:: - $ pip install git+https://github.com/mne-tools/mne-bids.git#egg=mne-bids + $ pip install git+https://github.com/mne-tools/mne-bids.git#egg=mne-bids If you do not have admin privileges on the computer, use the ``--user`` flag with `pip`. To upgrade, use the ``--upgrade`` flag provided by `pip`. To check if everything worked fine, you can do:: - $ python -c 'import mne_bids' + $ python -c 'import mne_bids' and it should not give any error messages. +Quickstart +========== + +Currently, we support writing of BIDS datasets for MEG and EEG. Support for +iEEG is experimental at the moment. + +.. code:: python + + >>> from mne import io + >>> from mne_bids import write_raw_bids + >>> raw = io.read_raw_fif('my_old_file.fif') + >>> write_raw_bids(raw, 'sub-01_ses-01_run-05', output_path='./bids_dataset') + +Reading of BIDS data will also be supported in the next version. + Bug reports =========== diff --git a/examples/convert_eeg_to_bids.py b/examples/convert_eeg_to_bids.py index d5b126334..c61897bb1 100644 --- a/examples/convert_eeg_to_bids.py +++ b/examples/convert_eeg_to_bids.py @@ -23,10 +23,11 @@ import os import shutil as sh +import mne from mne.datasets import eegbci -from mne.io import read_raw_edf +from mne.io.edf.edf import read_annotations_edf -from mne_bids import raw_to_bids +from mne_bids import write_raw_bids, make_bids_basename from mne_bids.utils import print_dir_tree ############################################################################### @@ -84,27 +85,35 @@ # -------------------------- # # Let's start by formatting a single subject. We are reading the data using -# MNE-Python's io module and the `read_raw_edf` function. -data_dir = os.path.join(data_dir, 'physiobank', 'database', 'eegmmidb') -edf_path = os.path.join(data_dir, 'S001', 'S001R02.edf') -raw = read_raw_edf(edf_path, preload=True) +# MNE-Python's io module and the `read_raw_edf` function. Note that we must +# use `preload=False`, the default in MNE-Python. It prevents the data from +# being loaded and modified when converting to BIDS. +edf_path = eegbci.load_data(subject=1, runs=2)[0] +raw = mne.io.read_raw_edf(edf_path, preload=False, stim_channel=None) + +############################################################################### +# The annotations stored in the file must be read in separately and converted +# into a 2D numpy array of events that is compatible with MNE. +annot = read_annotations_edf(edf_path) +raw.set_annotations(annot) +events, event_id = mne.events_from_annotations(raw) + print(raw) ############################################################################### -# With this simple step we have everything to start a new BIDS directory using -# our data. To do that, we can use the high level function `raw_to_bids`, which -# is the core of MNE-BIDS. Generally, `raw_to_bids` tries to extract as much +# With this step, we have everything to start a new BIDS directory using +# our data. To do that, we can use the function `write_raw_bids` +# Generally, `write_raw_bids` tries to extract as much # meta data as possible from the raw data and then formats it in a BIDS -# compatible way. `raw_to_bids` takes a bunch of inputs, most of which are +# compatible way. `write_raw_bids` takes a bunch of inputs, most of which are # however optional. The required inputs are: # -# * subject_id -# * task -# * raw_file +# * raw +# * bids_basename # * output_path # # ... as you can see in the docstring: -print(raw_to_bids.__doc__) +print(write_raw_bids.__doc__) ############################################################################### # We loaded 'S001R02.edf', which corresponds to subject 1 in the second task. @@ -117,19 +126,15 @@ ############################################################################### # Now we just need to specify a few more EEG details to get something sensible: -# First, tell `MNE-BIDS` that it is working with EEG data: -kind = 'eeg' - # Brief description of the event markers present in the data. This will become # the `trial_type` column in our BIDS `events.tsv`. We know about the event # meaning from the documentation on PhysioBank trial_type = {'rest': 0, 'imagine left fist': 1, 'imagine right fist': 2} # Now convert our data to be in a new BIDS dataset. -raw_to_bids(subject_id=subject_id, task=task, raw_file=raw_file, - output_path=output_path, kind=kind, event_id=trial_type, - overwrite=False) - +bids_basename = make_bids_basename(subject=subject_id, task=task) +write_raw_bids(raw_file, bids_basename, output_path, event_id=trial_type, + events_data=events, overwrite=True) ############################################################################### # What does our fresh BIDS directory look like? print_dir_tree(output_path) @@ -155,21 +160,18 @@ for subj_idx in [1, 2]: for task_idx in [2, 4, 12]: # Load the data - edf_path = os.path.join(data_dir, - 'S{:03}'.format(subj_idx), - 'S{:03}R{:02}.edf'.format(subj_idx, task_idx)) - raw = read_raw_edf(edf_path, preload=True) - - # `kind` and `trial_type` were already defined above - raw_to_bids(subject_id='{:03}'.format(subj_idx), - task=task_names[task_idx], - run=run_mapping[task_idx], - raw_file=raw, - output_path=output_path, - kind=kind, - event_id=trial_type, - overwrite=True - ) + edf_path = eegbci.load_data(subject=subj_idx, runs=task_idx)[0] + + raw = mne.io.read_raw_edf(edf_path, preload=False, stim_channel=None) + annot = read_annotations_edf(edf_path) + raw.set_annotations(annot) + events, event_id = mne.events_from_annotations(raw) + + make_bids_basename( + subject='{:03}'.format(subj_idx), task=task_names[task_idx], + run=run_mapping[task_idx]) + write_raw_bids(raw, bids_basename, output_path, event_id=trial_type, + events_data=events, overwrite=True) ############################################################################### # Step 3: Check and compare with standard @@ -183,8 +185,9 @@ # initial `dataset_description` on top! # # Now it's time to manually check the BIDS directory and the meta files to add -# all the information that MNE-BIDS could not infer. These places are marked -# with "n/a". +# all the information that MNE-BIDS could not infer. For instance, you must +# describe EEGReference and EEGGround yourself. It's easy to find these by +# searching for "n/a" in the sidecar files. # # Remember that there is a convenient javascript tool to validate all your BIDS # directories called the "BIDS-validator", available as a web version and a diff --git a/examples/convert_group_studies.py b/examples/convert_group_studies.py index 1ec5840f2..97ccbf377 100644 --- a/examples/convert_group_studies.py +++ b/examples/convert_group_studies.py @@ -24,7 +24,9 @@ # Let us import ``mne_bids`` import os.path as op -from mne_bids import raw_to_bids + +import mne +from mne_bids import write_raw_bids from mne_bids.datasets import fetch_faces_data from mne_bids.utils import print_dir_tree @@ -63,13 +65,14 @@ for subject_id in subject_ids: subject = 'sub%03d' % subject_id for run in runs: - raw_file = op.join(data_path, repo, subject, 'MEG', - 'run_%02d_raw.fif' % run) - - # Make it BIDS compatible - raw_to_bids(subject_id='%02d' % subject_id, session_id='01', run=run, - task='VisualFaces', raw_file=raw_file, event_id=event_id, - output_path=output_path, overwrite=True) + raw_fname = op.join(data_path, repo, subject, 'MEG', + 'run_%02d_raw.fif' % run) + + raw = mne.io.read_raw_fif(raw_fname) + bids_basename = ('sub-%02d_ses-01_task-VisualFaces_run-%d' + % (subject_id, run)) + write_raw_bids(raw, bids_basename, output_path, event_id=event_id, + overwrite=True) ############################################################################### # Now let's see the structure of the BIDS folder we created. diff --git a/examples/convert_mne_sample.py b/examples/convert_mne_sample.py index 1f47ca605..a49e65259 100644 --- a/examples/convert_mne_sample.py +++ b/examples/convert_mne_sample.py @@ -17,8 +17,11 @@ # Let us import mne_bids import os.path as op + +import mne from mne.datasets import sample -from mne_bids import raw_to_bids + +from mne_bids import write_raw_bids from mne_bids.utils import print_dir_tree ############################################################################### @@ -28,17 +31,17 @@ event_id = {'Auditory/Left': 1, 'Auditory/Right': 2, 'Visual/Left': 3, 'Visual/Right': 4, 'Smiley': 5, 'Button': 32} -raw_file = op.join(data_path, 'MEG', 'sample', 'sample_audvis_raw.fif') +raw_fname = op.join(data_path, 'MEG', 'sample', 'sample_audvis_raw.fif') events_data = op.join(data_path, 'MEG', 'sample', 'sample_audvis_raw-eve.fif') output_path = op.join(data_path, '..', 'MNE-sample-data-bids') ############################################################################### # Finally, we specify the raw_file and events_data -raw_to_bids(subject_id='01', run='01', session_id='01', task='audiovisual', - raw_file=raw_file, events_data=events_data, - output_path=output_path, event_id=event_id, - overwrite=False) +raw = mne.io.read_raw_fif(raw_fname) +bids_basename = 'sub-01_ses-01_task-audiovisual_run-01' +write_raw_bids(raw, bids_basename, output_path, events_data=events_data, + event_id=event_id, overwrite=True) ############################################################################### # Now let's see the structure of the BIDS folder we created. diff --git a/examples/create_bids_folder.py b/examples/create_bids_folder.py index 14bf53ae9..463c9a46e 100644 --- a/examples/create_bids_folder.py +++ b/examples/create_bids_folder.py @@ -9,8 +9,8 @@ .. note:: - You may automatically convert Raw files to a BIDS-compatible folder with - `raw_to_bids`. This example is for manually creating files/folders. + You may automatically convert Raw objects to BIDS-compatible files with + `write_raw_bids`. This example is for manually creating files/folders. """ # Authors: Chris Holdgraf @@ -19,27 +19,27 @@ ############################################################################### # We'll import the relevant functions from the utils module -from mne_bids import make_bids_folders, make_bids_filename +from mne_bids import make_bids_folders, make_bids_basename ############################################################################### # Creating file names for BIDS # ---------------------------- # # BIDS requires a specific ordering and structure for metadata fields in -# file paths, the function `filename_bids` allows you to specify many such +# file paths, the function `make_bids_basename` allows you to specify many such # pieces of metadata, ensuring that they are in the correct order in the # final file path. Omitted keys will not be included in the file path. -my_name = make_bids_filename(subject='test', session='two', task='mytask', - suffix='data.csv') -print(my_name) +bids_basename = make_bids_basename(subject='test', session='two', + task='mytask', suffix='data.csv') +print(bids_basename) ############################################################################### # You may also omit the suffix, which will result in *only* a prefix for a # file name. This could then prepended to many more files. -my_name = make_bids_filename(subject='test', task='mytask') -print(my_name) +bids_basename = make_bids_basename(subject='test', task='mytask') +print(bids_basename) ############################################################################### # Creating folders @@ -48,7 +48,7 @@ # You can also use MNE-BIDS to create folder hierarchies. path_folder = make_bids_folders('sub_01', session='mysession', - kind='meg', root='path/to/project', + kind='meg', output_path='path/to/project', make_dir=False) print(path_folder) diff --git a/mne_bids/__init__.py b/mne_bids/__init__.py index c4899432f..34bab9bbc 100644 --- a/mne_bids/__init__.py +++ b/mne_bids/__init__.py @@ -3,6 +3,6 @@ __version__ = '0.1.dev0' -from .mne_bids import raw_to_bids # noqa -from .utils import make_bids_folders, make_bids_filename # noqa +from .mne_bids import write_raw_bids # noqa +from .utils import make_bids_folders, make_bids_basename # noqa from .config import BIDS_VERSION # noqa diff --git a/mne_bids/commands/mne_bids_raw_to_bids.py b/mne_bids/commands/mne_bids_raw_to_bids.py index 6dab927c6..83550923b 100644 --- a/mne_bids/commands/mne_bids_raw_to_bids.py +++ b/mne_bids/commands/mne_bids_raw_to_bids.py @@ -4,11 +4,11 @@ """Command line interface for mne_bids. example usage: $ mne_bids raw_to_bids --subject_id sub01 --task rest ---raw_file data.edf --output_path new_path +--raw data.edf --output_path new_path """ - -from mne_bids import raw_to_bids +from mne_bids import write_raw_bids, make_bids_basename +from mne_bids.io import _read_raw def run(): @@ -17,14 +17,13 @@ def run(): parser = get_optparser(__file__) - parser.set_defaults(kind='meg') parser.add_option('--subject_id', dest='subject_id', help=('The subject name in BIDS compatible format', '(01,02, etc.)'), metavar='s') parser.add_option('--task', dest='task', help='Name of the task the data is based on.', metavar='t') - parser.add_option('--raw_file', dest='raw_file', + parser.add_option('--raw', dest='raw_fname', help='The path to the raw MEG file.', metavar='r') parser.add_option('--output_path', dest='output_path', help='The path of the BIDS compatible folder.', @@ -35,9 +34,9 @@ def run(): parser.add_option('--run', dest='run', help='The run number for this dataset.', metavar='run') - parser.add_option('--kind', dest='kind', - help='The kind of data being converted.', - metavar='k') + parser.add_option('--acq', dest='acq', + help='The acquisition parameter.', + metavar='acq') parser.add_option('--events_data', dest='events_data', help='The events file.', metavar='evt') parser.add_option('--event_id', dest='event_id', @@ -58,13 +57,14 @@ def run(): opt, args = parser.parse_args() - raw_to_bids(subject_id=opt.subject_id, task=opt.task, - raw_file=opt.raw_file, output_path=opt.output_path, - session_id=opt.session_id, run=opt.run, kind=opt.kind, - events_data=opt.events_data, event_id=opt.event_id, - hpi=opt.hpi, electrode=opt.electrode, hsp=opt.hsp, - config=opt.config, overwrite=opt.overwrite, - verbose=True) + bids_basename = make_bids_basename( + subject=opt.subject_id, session=opt.session_id, run=opt.run, + acquisition=opt.acq, task=opt.task) + raw = _read_raw(opt.raw_fname, hpi=opt.hpi, electrode=opt.electrode, + hsp=opt.hsp, config=opt.config) + write_raw_bids(raw, bids_basename, opt.output_path, event_id=opt.event_id, + events_data=opt.events_data, overwrite=opt.overwrite, + verbose=True) is_main = (__name__ == '__main__') diff --git a/mne_bids/io.py b/mne_bids/io.py index 6fec70df7..30494376d 100644 --- a/mne_bids/io.py +++ b/mne_bids/io.py @@ -19,6 +19,12 @@ ALLOWED_EXTENSIONS = allowed_extensions_meg + allowed_extensions_eeg +reader = {'.con': io.read_raw_kit, '.sqd': io.read_raw_kit, + '.fif': io.read_raw_fif, '.pdf': io.read_raw_bti, + '.ds': io.read_raw_ctf, '.vhdr': io.read_raw_brainvision, + '.edf': io.read_raw_edf, '.bdf': io.read_raw_edf, + '.set': io.read_raw_eeglab, '.cnt': io.read_raw_cnt} + def _parse_ext(raw_fname, verbose=False): """Split a filename into its name and extension.""" @@ -44,38 +50,20 @@ def _read_raw(raw_fname, electrode=None, hsp=None, hpi=None, config=None, raw = io.read_raw_kit(raw_fname, elp=electrode, hsp=hsp, mrk=hpi, preload=False) - # Neuromag or converted-to-fif systems - elif ext in ['.fif']: - raw = io.read_raw_fif(raw_fname, preload=False) - # BTi systems - elif ext == '.pdf': - if os.path.isfile(raw_fname): + elif ext == '.pdf' and os.path.isfile(raw_fname): raw = io.read_raw_bti(raw_fname, config_fname=config, head_shape_fname=hsp, preload=False, verbose=verbose) - # CTF systems - elif ext == '.ds': - raw = io.read_raw_ctf(raw_fname) - - # EEG File Types - # -------------- - # BrainVision format by Brain Products, links to a .eeg and a .vmrk file - elif ext == '.vhdr': - raw = io.read_raw_brainvision(raw_fname) + elif ext in ['.fif', '.ds', '.vhdr', '.set']: + raw = reader[ext](raw_fname) # EDF (european data format) or BDF (biosemi) format - elif ext == '.edf' or ext == '.bdf': - raw = io.read_raw_edf(raw_fname, preload=True) - - # EEGLAB .set format, a .fdt file is potentially linked from the .set - elif ext == '.set': - raw = io.read_raw_eeglab(raw_fname) - - # Neuroscan .cnt format - elif ext == '.cnt': - raw = io.read_raw_cnt(raw_fname, montage=montage) + # TODO: integrate with lines above once MNE can read + # annotations with preload=False + elif ext in ['.edf', '.bdf']: + raw = reader[ext](raw_fname, preload=True) # No supported data found ... # --------------------------- diff --git a/mne_bids/mne_bids.py b/mne_bids/mne_bids.py index 761fc5eb5..de707ea3b 100644 --- a/mne_bids/mne_bids.py +++ b/mne_bids/mne_bids.py @@ -10,29 +10,33 @@ import os import errno +import os.path as op + import shutil as sh import pandas as pd from collections import defaultdict, OrderedDict import numpy as np +from numpy.testing import assert_array_equal + from mne import Epochs from mne.io.constants import FIFF from mne.io.pick import channel_type from mne.io import BaseRaw from mne.channels.channels import _unit2human -from mne.externals.six import string_types from mne.utils import check_version from datetime import datetime from warnings import warn from .pick import coil_type -from .utils import (make_bids_filename, make_bids_folders, +from .utils import (make_bids_basename, make_bids_folders, make_dataset_description, _write_json, _write_tsv, - _read_events, _mkdir_p, age_on_date, + _read_events, _mkdir_p, _age_on_date, copyfile_brainvision, copyfile_eeglab, - _infer_eeg_placement_scheme) -from .io import (_parse_ext, _read_raw, ALLOWED_EXTENSIONS) + _infer_eeg_placement_scheme, _parse_bids_filename, + _handle_kind) +from .io import _parse_ext, ALLOWED_EXTENSIONS, reader ALLOWED_KINDS = ['meg', 'eeg', 'ieeg'] @@ -105,14 +109,9 @@ def _channels_tsv(raw, fname, overwrite=False, verbose=True): # get the manufacturer from the file in the Raw object manufacturer = None - if hasattr(raw, 'filenames'): - # XXX: Hack for EEGLAB bug in MNE-Python 0.16; fixed in MNE-Python - # 0.17, ... remove the hack after upgrading dependencies in MNE-BIDS - if raw.filenames[0] is None: # hack - ext = '.set' # hack - else: - _, ext = _parse_ext(raw.filenames[0], verbose=verbose) - manufacturer = MANUFACTURERS[ext] + + _, ext = _parse_ext(raw.filenames[0], verbose=verbose) + manufacturer = MANUFACTURERS[ext] ignored_indexes = [raw.ch_names.index(ch_name) for ch_name in raw.ch_names if ch_name in @@ -253,7 +252,7 @@ def _participants_tsv(raw, subject_id, group, fname, overwrite=False, if meas_date is not None and age is not None: bday = datetime(age[0], age[1], age[2]) meas_datetime = datetime.fromtimestamp(meas_date) - subject_age = age_on_date(bday, meas_datetime) + subject_age = _age_on_date(bday, meas_datetime) else: subject_age = "n/a" @@ -400,8 +399,8 @@ def _coordsystem_json(raw, unit, orient, manufacturer, fname, return fname -def _sidecar_json(raw, task, manufacturer, fname, kind, eeg_ref=None, - eeg_gnd=None, overwrite=False, verbose=True): +def _sidecar_json(raw, task, manufacturer, fname, kind, overwrite=False, + verbose=True): """Create a sidecar json file depending on the kind and save it. The sidecar json file provides meta data about the data of a certain kind. @@ -419,11 +418,6 @@ def _sidecar_json(raw, task, manufacturer, fname, kind, eeg_ref=None, Filename to save the sidecar json to. kind : str Type of the data as in ALLOWED_KINDS. - eeg_ref : str - Description of the type of reference used and (when applicable) of - location of the reference electrode. Defaults to None. - eeg_gnd : str - Description of the location of the ground electrode. Defaults to None. overwrite : bool Whether to overwrite the existing file. Defaults to False. @@ -437,11 +431,6 @@ def _sidecar_json(raw, task, manufacturer, fname, kind, eeg_ref=None, warn('No line frequency found, defaulting to 50 Hz') powerlinefrequency = 50 - if not eeg_ref: - eeg_ref = 'n/a' - if not eeg_gnd: - eeg_gnd = 'n/a' - if isinstance(raw, BaseRaw): rec_type = 'continuous' elif isinstance(raw, Epochs): @@ -492,8 +481,8 @@ def _sidecar_json(raw, task, manufacturer, fname, kind, eeg_ref=None, ('MEGChannelCount', n_megchan), ('MEGREFChannelCount', n_megrefchan)] ch_info_json_eeg = [ - ('EEGReference', eeg_ref), - ('EEGGround', eeg_gnd), + ('EEGReference', 'n/a'), + ('EEGGround', 'n/a'), ('EEGPlacementScheme', _infer_eeg_placement_scheme(raw)), ('Manufacturer', manufacturer)] ch_info_json_ieeg = [ @@ -528,56 +517,46 @@ def _sidecar_json(raw, task, manufacturer, fname, kind, eeg_ref=None, return fname -def raw_to_bids(subject_id, task, raw_file, output_path, session_id=None, - acquisition=None, run=None, kind='meg', events_data=None, - event_id=None, hpi=None, electrode=None, hsp=None, - eeg_ref=None, eeg_gnd=None, config=None, - overwrite=False, verbose=True): +def write_raw_bids(raw, bids_basename, output_path, events_data=None, + event_id=None, overwrite=False, verbose=True): """Walk over a folder of files and create BIDS compatible folder. + .. warning:: The original files are simply copied over. This function + cannot convert modify data files from one format to another. + Modification of the original data files is not allowed. + Parameters ---------- - subject_id : str - The subject name in BIDS compatible format ('01', '02', etc.) - task : str - Name of the task the data is based on. - raw_file : str | instance of mne.Raw - The raw data. If a string, it is assumed to be the path to the raw data - file. Otherwise it must be an instance of mne.Raw + raw : instance of mne.io.Raw + The raw data. It must be an instance of mne.Raw. The data should not be + loaded on disk, i.e., raw.preload must be False. + bids_basename : str + The base filename of the BIDS compatible files. Typically, this can be + generated using make_bids_basename. + Example: sub-01_ses-01_task-testing_acq-01_run-01 + This will write the following files in the correct subfolder + of output_path: + sub-01_ses-01_task-testing_acq-01_run-01_meg.fif + sub-01_ses-01_task-testing_acq-01_run-01_meg.json + sub-01_ses-01_task-testing_acq-01_run-01_channels.tsv + sub-01_ses-01_task-testing_acq-01_run-01_coordsystem.json + and the following one if events_data is not None + sub-01_ses-01_task-testing_acq-01_run-01_events.tsv + and add a line to the following files: + participants.tsv + scans.tsv + Note that the modality 'meg' is automatically inferred from the raw + object and extension '.fif' is copied from raw.filenames. output_path : str - The path of the BIDS compatible folder - session_id : str | None - The session name in BIDS compatible format. - acquisition : str | None - Acquisition parameter for the dataset. - run : int | None - The run number for this dataset. - kind : str, one of ('meg', 'eeg', 'ieeg') - The kind of data being converted. Defaults to "meg". + The path of the root of the BIDS compatible folder. The session and + subject specific folders will be populated automatically by parsing + bids_basename. events_data : str | array | None The events file. If a string, a path to the events file. If an array, the MNE events array (shape n_events, 3). If None, events will be inferred from the stim channel using `mne.find_events`. event_id : dict | None The event id dict used to create a 'trial_type' column in events.tsv - hpi : None | str - Marker points representing the location of the marker coils with - respect to the MEG Sensors, or path to a marker file. - electrode : None | str - Digitizer points representing the location of the fiducials and the - marker coils with respect to the digitized head shape, or path to a - file containing these points. - hsp : None | str | array, shape = (n_points, 3) - Digitizer head shape points, or path to head shape file. If more than - 10`000 points are in the head shape, they are automatically decimated. - eeg_ref : str - Description of the type of reference used and (when applicable) of - location of the reference electrode. Defaults to None. - eeg_gnd : str - Description of the location of the ground electrode. Defaults to None. - config : str | None - A path to the configuration file to use if the data is from a BTi - system. overwrite : bool Whether to overwrite existing files or data in files. Defaults to False. @@ -598,68 +577,70 @@ def raw_to_bids(subject_id, task, raw_file, output_path, session_id=None, of the participant correctly. """ - if isinstance(raw_file, string_types): - # We must read in the raw data - raw = _read_raw(raw_file, electrode=electrode, hsp=hsp, hpi=hpi, - config=config, verbose=verbose) - _, ext = _parse_ext(raw_file, verbose=verbose) - raw_fname = raw_file - elif isinstance(raw_file, BaseRaw): - # We got a raw mne object, get back the filename if possible - # Assume that if no filename attr exists, it's a fif file. - raw = raw_file.copy() - if hasattr(raw, 'filenames'): - _, ext = _parse_ext(raw.filenames[0], verbose=verbose) - raw_fname = raw.filenames[0] - else: - # FIXME: How to get the filename if no filenames attribute? - raw_fname = 'unknown_file_name' - ext = '.fif' - else: - raise ValueError('raw_file must be an instance of str or BaseRaw, ' - 'got %s' % type(raw_file)) - + if not isinstance(raw, BaseRaw): + raise ValueError('raw_file must be an instance of BaseRaw, ' + 'got %s' % type(raw)) + + if not hasattr(raw, 'filenames') or raw.filenames[0] is None: + raise ValueError('raw.filenames is missing. Please set raw.filenames' + 'as a list with the full path of original raw file.') + + if raw.preload is not False: + raise ValueError('The data should not be preloaded.') + + raw = raw.copy() + + raw_fname = raw.filenames[0] + if '.ds' in op.dirname(raw.filenames[0]): + raw_fname = op.dirname(raw.filenames[0]) + # point to file containing header info for multifile systems + raw_fname = raw_fname.replace('.eeg', '.vhdr') + raw_fname = raw_fname.replace('.fdt', '.set') + _, ext = _parse_ext(raw_fname, verbose=verbose) + + raw_orig = reader[ext](**raw._init_kwargs) + assert_array_equal(raw.times, raw_orig.times, + "raw.times should not have changed since reading" + " in from the file. It may have been cropped.") + + params = _parse_bids_filename(bids_basename, verbose) + subject_id, session_id = params['sub'], params['ses'] + acquisition, task, run = params['acq'], params['task'], params['run'] + kind = _handle_kind(raw) + + bids_fname = bids_basename + '_%s%s' % (kind, ext) data_path = make_bids_folders(subject=subject_id, session=session_id, - kind=kind, root=output_path, + kind=kind, output_path=output_path, overwrite=False, verbose=verbose) if session_id is None: ses_path = os.sep.join(data_path.split(os.sep)[:-1]) else: ses_path = make_bids_folders(subject=subject_id, session=session_id, - root=output_path, make_dir=False, + output_path=output_path, make_dir=False, overwrite=False, verbose=verbose) # create filenames - scans_fname = make_bids_filename( + scans_fname = make_bids_basename( subject=subject_id, session=session_id, suffix='scans.tsv', prefix=ses_path) - participants_fname = make_bids_filename(prefix=output_path, + participants_fname = make_bids_basename(prefix=output_path, suffix='participants.tsv') - coordsystem_fname = make_bids_filename( + coordsystem_fname = make_bids_basename( subject=subject_id, session=session_id, acquisition=acquisition, suffix='coordsystem.json', prefix=data_path) - data_meta_fname = make_bids_filename( + sidecar_fname = make_bids_basename( subject=subject_id, session=session_id, task=task, run=run, acquisition=acquisition, suffix='%s.json' % kind, prefix=data_path) - if ext in ['.fif', '.ds', '.vhdr', '.edf', '.bdf', '.set', '.cnt']: - raw_file_bids = make_bids_filename( - subject=subject_id, session=session_id, task=task, run=run, - acquisition=acquisition, suffix='%s%s' % (kind, ext)) - else: - raw_folder = make_bids_filename( - subject=subject_id, session=session_id, task=task, run=run, - acquisition=acquisition, suffix='%s' % kind) - raw_file_bids = make_bids_filename( - subject=subject_id, session=session_id, task=task, run=run, - acquisition=acquisition, suffix='%s%s' % (kind, ext), - prefix=raw_folder) - events_tsv_fname = make_bids_filename( + events_fname = make_bids_basename( subject=subject_id, session=session_id, task=task, acquisition=acquisition, run=run, suffix='events.tsv', prefix=data_path) - channels_fname = make_bids_filename( + channels_fname = make_bids_basename( subject=subject_id, session=session_id, task=task, run=run, acquisition=acquisition, suffix='channels.tsv', prefix=data_path) + if ext not in ['.fif', '.ds', '.vhdr', '.edf', '.bdf', '.set', '.cnt']: + bids_raw_folder = bids_fname.split('.')[0] + bids_fname = op.join(bids_raw_folder, bids_fname) # Read in Raw object and extract metadata from Raw object if needed orient = ORIENTATION.get(ext, 'n/a') @@ -671,7 +652,7 @@ def raw_to_bids(subject_id, task, raw_file, output_path, session_id=None, # save all meta data _participants_tsv(raw, subject_id, "n/a", participants_fname, overwrite, verbose) - _scans_tsv(raw, os.path.join(kind, raw_file_bids), scans_fname, + _scans_tsv(raw, os.path.join(kind, bids_fname), scans_fname, overwrite, verbose) # TODO: Implement coordystem.json and electrodes.tsv for EEG and iEEG @@ -681,24 +662,23 @@ def raw_to_bids(subject_id, task, raw_file, output_path, session_id=None, events = _read_events(events_data, raw) if len(events) > 0: - _events_tsv(events, raw, events_tsv_fname, event_id, overwrite, - verbose) + _events_tsv(events, raw, events_fname, event_id, overwrite, verbose) make_dataset_description(output_path, name=" ", verbose=verbose) - _sidecar_json(raw, task, manufacturer, data_meta_fname, kind, eeg_ref, - eeg_gnd, overwrite, verbose) + _sidecar_json(raw, task, manufacturer, sidecar_fname, kind, overwrite, + verbose) _channels_tsv(raw, channels_fname, overwrite, verbose) # set the raw file name to now be the absolute path to ensure the files # are placed in the right location - raw_file_bids = os.path.join(data_path, raw_file_bids) - if os.path.exists(raw_file_bids) and not overwrite: + bids_fname = os.path.join(data_path, bids_fname) + if os.path.exists(bids_fname) and not overwrite: raise OSError(errno.EEXIST, '"%s" already exists. Please set ' - 'overwrite to True.' % raw_file_bids) - _mkdir_p(os.path.dirname(raw_file_bids)) + 'overwrite to True.' % bids_fname) + _mkdir_p(os.path.dirname(bids_fname)) if verbose: - print('Writing data files to %s' % raw_file_bids) + print('Copying data files to %s' % bids_fname) if ext not in ALLOWED_EXTENSIONS: raise ValueError('ext must be in %s, got %s' @@ -711,7 +691,7 @@ def raw_to_bids(subject_id, task, raw_file, output_path, session_id=None, # TODO Update MNE requirement to version 0.17 when it's released if check_version('mne', '0.17.dev'): split_naming = 'bids' - raw.save(raw_file_bids, split_naming=split_naming, + raw.save(bids_fname, split_naming=split_naming, overwrite=True) else: raise NotImplementedError( @@ -722,29 +702,27 @@ def raw_to_bids(subject_id, task, raw_file, output_path, session_id=None, else: # TODO insert arg `split_naming=split_naming` # when MNE releases 0.17 - raw.save(raw_file_bids, overwrite=True) + raw.save(bids_fname, overwrite=True) + # CTF data is saved in a directory elif ext == '.ds': - sh.copytree(raw_fname, raw_file_bids) + sh.copytree(raw_fname, bids_fname) # BrainVision is multifile, copy over all of them and fix pointers elif ext == '.vhdr': - copyfile_brainvision(raw_fname, raw_file_bids) + copyfile_brainvision(raw_fname, bids_fname) # EEGLAB .set might be accompanied by a .fdt - find out and copy it too elif ext == '.set': - copyfile_eeglab(raw_fname, raw_file_bids) + copyfile_eeglab(raw_fname, bids_fname) else: - sh.copyfile(raw_fname, raw_file_bids) + sh.copyfile(raw_fname, bids_fname) # KIT data requires the marker file to be copied over too - if hpi is not None: - if isinstance(hpi, list): - # No currently accepted way to name multiple marker files. See: - # https://github.com/bids-standard/bids-specification/issues/45 - raise ValueError('Only single marker coils supported currently') + if 'mrk' in raw._init_kwargs: + hpi = raw._init_kwargs['mrk'] _, marker_ext = _parse_ext(hpi) - marker_fname = make_bids_filename( + marker_fname = make_bids_basename( subject=subject_id, session=session_id, task=task, run=run, acquisition=acquisition, suffix='markers%s' % marker_ext, - prefix=os.path.join(data_path, raw_folder)) + prefix=os.path.join(data_path, bids_raw_folder)) sh.copyfile(hpi, marker_fname) return output_path diff --git a/mne_bids/pick.py b/mne_bids/pick.py index 6a861bbc1..9afe71c16 100644 --- a/mne_bids/pick.py +++ b/mne_bids/pick.py @@ -27,8 +27,6 @@ def get_coil_types(): FIFF.FIFFV_COIL_VV_PLANAR_T2, FIFF.FIFFV_COIL_VV_PLANAR_T3), megmag=(FIFF.FIFFV_COIL_POINT_MAGNETOMETER, - FIFF.FIFFV_COIL_POINT_MAGNETOMETER_X, - FIFF.FIFFV_COIL_POINT_MAGNETOMETER_Y, FIFF.FIFFV_COIL_VV_MAG_W, FIFF.FIFFV_COIL_VV_MAG_T1, FIFF.FIFFV_COIL_VV_MAG_T2, diff --git a/mne_bids/tests/test_commands.py b/mne_bids/tests/test_commands.py index a33e43210..1ef631a3c 100644 --- a/mne_bids/tests/test_commands.py +++ b/mne_bids/tests/test_commands.py @@ -28,7 +28,7 @@ def test_raw_to_bids(): raw_fname = op.join(data_path, 'MEG', 'sample', 'sample_audvis_trunc_raw.fif') # check_usage(mne_bids_raw_to_bids) - with ArgvSetter(('--subject_id', subject_id, '--task', task, '--raw_file', + with ArgvSetter(('--subject_id', subject_id, '--task', task, '--raw', raw_fname, '--output_path', output_path)): mne_bids_raw_to_bids.run() diff --git a/mne_bids/tests/test_mne_bids.py b/mne_bids/tests/test_mne_bids.py index a0bee0f87..5a64914ef 100644 --- a/mne_bids/tests/test_mne_bids.py +++ b/mne_bids/tests/test_mne_bids.py @@ -15,12 +15,13 @@ import pytest import pandas as pd + import mne from mne.datasets import testing from mne.utils import _TempDir, run_subprocess from mne.io.constants import FIFF -from mne_bids import raw_to_bids, make_bids_filename, make_bids_folders +from mne_bids import make_bids_basename, make_bids_folders, write_raw_bids base_path = op.join(op.dirname(mne.__file__), 'io') subject_id = '01' @@ -31,6 +32,10 @@ run2 = '02' task = 'testing' +bids_basename = make_bids_basename( + subject=subject_id, session=session_id, run=run, acquisition=acq, + task=task) + # for windows, shell = True is needed # to call npm, bids-validator etc. # see: https://stackoverflow.com/questions/ @@ -43,7 +48,7 @@ # MEG Tests # --------- def test_fif(): - """Test functionality of the raw_to_bids conversion for Neuromag data.""" + """Test functionality of the write_raw_bids conversion for fif.""" output_path = _TempDir() data_path = testing.data_path() raw_fname = op.join(data_path, 'MEG', 'sample', @@ -54,10 +59,9 @@ def test_fif(): events_fname = op.join(data_path, 'MEG', 'sample', 'sample_audvis_trunc_raw-eve.fif') - raw_to_bids(subject_id=subject_id, session_id=session_id, run=run, - acquisition=acq, task=task, raw_file=raw_fname, - events_data=events_fname, output_path=output_path, - event_id=event_id, overwrite=False) + raw = mne.io.read_raw_fif(raw_fname) + write_raw_bids(raw, bids_basename, output_path, events_data=events_fname, + event_id=event_id, overwrite=False) # give the raw object some fake participant data raw = mne.io.read_raw_fif(raw_fname) @@ -67,25 +71,38 @@ def test_fif(): data_path2 = _TempDir() raw_fname2 = op.join(data_path2, 'sample_audvis_raw.fif') raw.save(raw_fname2) - raw_to_bids(subject_id=subject_id2, run=run, task=task, acquisition=acq, - session_id=session_id, raw_file=raw_fname2, - events_data=events_fname, output_path=output_path, - event_id=event_id, overwrite=False) + + bids_basename2 = bids_basename.replace(subject_id, subject_id2) + write_raw_bids(raw, bids_basename2, output_path, events_data=events_fname, + event_id=event_id, overwrite=False) # check that the overwrite parameters work correctly for the participant # data # change the gender but don't force overwrite. raw.info['subject_info'] = {'his_id': subject_id2, 'birthday': (1994, 1, 26), 'sex': 2} with pytest.raises(OSError, match="already exists"): - raw_to_bids(subject_id=subject_id2, run=run, task=task, - acquisition=acq, session_id=session_id, raw_file=raw, - events_data=events_fname, output_path=output_path, - event_id=event_id, overwrite=False) + write_raw_bids(raw, bids_basename2, output_path, + events_data=events_fname, event_id=event_id, + overwrite=False) # now force the overwrite - raw_to_bids(subject_id=subject_id2, run=run, task=task, - acquisition=acq, session_id=session_id, raw_file=raw, - events_data=events_fname, output_path=output_path, - event_id=event_id, overwrite=True) + write_raw_bids(raw, bids_basename2, output_path, events_data=events_fname, + event_id=event_id, overwrite=True) + + with pytest.raises(ValueError, match='raw_file must be'): + write_raw_bids('blah', bids_basename, output_path) + + bids_basename2 = 'sub-01_ses-01_xyz-01_run-01' + with pytest.raises(KeyError, match='Unexpected entity'): + write_raw_bids(raw, bids_basename2, output_path) + + bids_basename2 = 'sub-01_run-01_task-auditory' + with pytest.raises(ValueError, match='ordered correctly'): + write_raw_bids(raw, bids_basename2, output_path, overwrite=True) + + del raw._filenames + with pytest.raises(ValueError, match='raw.filenames is missing'): + write_raw_bids(raw, bids_basename2, output_path) + cmd = ['bids-validator', output_path] run_subprocess(cmd, shell=shell) @@ -93,7 +110,7 @@ def test_fif(): def test_kit(): - """Test functionality of the raw_to_bids conversion for KIT data.""" + """Test functionality of the write_raw_bids conversion for KIT data.""" output_path = _TempDir() data_path = op.join(base_path, 'kit', 'tests', 'data') raw_fname = op.join(data_path, 'test.sqd') @@ -103,17 +120,17 @@ def test_kit(): headshape_fname = op.join(data_path, 'test_hsp.txt') event_id = dict(cond=1) - raw_to_bids(subject_id=subject_id, session_id=session_id, run=run, - task=task, acquisition=acq, raw_file=raw_fname, - events_data=events_fname, event_id=event_id, hpi=hpi_fname, - electrode=electrode_fname, hsp=headshape_fname, - output_path=output_path, overwrite=False) + raw = mne.io.read_raw_kit( + raw_fname, mrk=hpi_fname, elp=electrode_fname, + hsp=headshape_fname) + write_raw_bids(raw, bids_basename, output_path, events_data=events_fname, + event_id=event_id, overwrite=False) cmd = ['bids-validator', output_path] run_subprocess(cmd, shell=shell) assert op.exists(op.join(output_path, 'participants.tsv')) # ensure the channels file has no STI 014 channel: - channels_tsv = make_bids_filename( + channels_tsv = make_bids_basename( subject=subject_id, session=session_id, task=task, run=run, suffix='channels.tsv', acquisition=acq, prefix=op.join(output_path, 'sub-01/ses-01/meg')) @@ -121,58 +138,45 @@ def test_kit(): assert not ('STI 014' in df['name'].values) # ensure the marker file is produced in the right place - raw_folder = make_bids_filename( + raw_folder = make_bids_basename( subject=subject_id, session=session_id, task=task, run=run, acquisition=acq, suffix='%s' % 'meg') - marker_fname = make_bids_filename( + marker_fname = make_bids_basename( subject=subject_id, session=session_id, task=task, run=run, acquisition=acq, suffix='markers.sqd', prefix=os.path.join(output_path, 'sub-01/ses-01/meg', raw_folder)) assert op.exists(marker_fname) - # check for error if there are multiple marker coils specified - with pytest.raises(ValueError): - raw_to_bids(subject_id=subject_id, session_id=session_id, run=run, - task=task, acquisition=acq, raw_file=raw_fname, - events_data=events_fname, event_id=event_id, - hpi=[hpi_fname, hpi_fname], electrode=electrode_fname, - hsp=headshape_fname, output_path=output_path, - overwrite=True) - def test_ctf(): - """Test functionality of the raw_to_bids conversion for CTF data.""" + """Test functionality of the write_raw_bids conversion for CTF data.""" output_path = _TempDir() data_path = op.join(testing.data_path(download=False), 'CTF') raw_fname = op.join(data_path, 'testdata_ctf.ds') - raw_to_bids(subject_id=subject_id, session_id=session_id, run=run, - task=task, acquisition=acq, raw_file=raw_fname, - output_path=output_path, overwrite=False) + raw = mne.io.read_raw_ctf(raw_fname) + write_raw_bids(raw, bids_basename, output_path=output_path) cmd = ['bids-validator', output_path] run_subprocess(cmd, shell=shell) # test to check that running again with overwrite == False raises an error with pytest.raises(OSError, match="already exists"): - raw_to_bids(subject_id=subject_id, session_id=session_id, run=run, - task=task, acquisition=acq, raw_file=raw_fname, - output_path=output_path, overwrite=False) + write_raw_bids(raw, bids_basename, output_path=output_path) assert op.exists(op.join(output_path, 'participants.tsv')) def test_bti(): - """Test functionality of the raw_to_bids conversion for BTi data.""" + """Test functionality of the write_raw_bids conversion for BTi data.""" output_path = _TempDir() data_path = op.join(base_path, 'bti', 'tests', 'data') raw_fname = op.join(data_path, 'test_pdf_linux') config_fname = op.join(data_path, 'test_config_linux') headshape_fname = op.join(data_path, 'test_hs_linux') - raw_to_bids(subject_id=subject_id, session_id=session_id, run=run, - task=task, acquisition=acq, raw_file=raw_fname, - config=config_fname, hsp=headshape_fname, - output_path=output_path, verbose=True, overwrite=False) + raw = mne.io.read_raw_bti(raw_fname, config_fname=config_fname, + head_shape_fname=headshape_fname) + write_raw_bids(raw, bids_basename, output_path, verbose=True) assert op.exists(op.join(output_path, 'participants.tsv')) @@ -183,14 +187,13 @@ def test_bti(): # EEG Tests # --------- def test_vhdr(): - """Test raw_to_bids conversion for BrainVision data.""" + """Test write_raw_bids conversion for BrainVision data.""" output_path = _TempDir() data_path = op.join(base_path, 'brainvision', 'tests', 'data') raw_fname = op.join(data_path, 'test.vhdr') - raw_to_bids(subject_id=subject_id, session_id=session_id, run=run, - task=task, acquisition=acq, raw_file=raw_fname, - output_path=output_path, overwrite=False, kind='eeg') + raw = mne.io.read_raw_brainvision(raw_fname) + write_raw_bids(raw, bids_basename, output_path, overwrite=False) cmd = ['bids-validator', '--bep006', output_path] run_subprocess(cmd, shell=shell) @@ -198,35 +201,36 @@ def test_vhdr(): # create another bids folder with the overwrite command and check # no files are in the folder data_path = make_bids_folders(subject=subject_id, session=session_id, - kind='eeg', root=output_path, + kind='eeg', output_path=output_path, overwrite=True) assert len([f for f in os.listdir(data_path) if op.isfile(f)]) == 0 def test_edf(): - """Test raw_to_bids conversion for European Data Format data.""" + """Test write_raw_bids conversion for European Data Format data.""" output_path = _TempDir() data_path = op.join(testing.data_path(), 'EDF') raw_fname = op.join(data_path, 'test_reduced.edf') raw = mne.io.read_raw_edf(raw_fname, preload=True) + # XXX: hack that should be fixed later. Annotation reading is + # broken for this file with preload=False and read_annotations_edf + raw.preload = False + raw.rename_channels({raw.info['ch_names'][0]: 'EOG'}) raw.info['chs'][0]['coil_type'] = FIFF.FIFFV_COIL_EEG_BIPOLAR raw.rename_channels({raw.info['ch_names'][1]: 'EMG'}) raw.set_channel_types({'EMG': 'emg'}) - raw_to_bids(subject_id=subject_id, session_id=session_id, run=run, - task=task, acquisition=acq, raw_file=raw, - output_path=output_path, overwrite=False, kind='eeg') - raw_to_bids(subject_id=subject_id, session_id=session_id, run=run2, - task=task, acquisition=acq, raw_file=raw, - output_path=output_path, overwrite=True, kind='eeg') + write_raw_bids(raw, bids_basename, output_path) + bids_fname = bids_basename.replace('run-01', 'run-%s' % run2) + write_raw_bids(raw, bids_fname, output_path, overwrite=True) cmd = ['bids-validator', '--bep006', output_path] run_subprocess(cmd, shell=shell) # ensure there is an EMG channel in the channels.tsv: - channels_tsv = make_bids_filename( + channels_tsv = make_bids_basename( subject=subject_id, session=session_id, task=task, run=run, suffix='channels.tsv', acquisition=acq, prefix=op.join(output_path, 'sub-01/ses-01/eeg')) @@ -234,7 +238,7 @@ def test_edf(): assert 'ElectroMyoGram' in df['description'].values # check that the scans list contains two scans - scans_tsv = make_bids_filename( + scans_tsv = make_bids_basename( subject=subject_id, session=session_id, suffix='scans.tsv', prefix=op.join(output_path, 'sub-01/ses-01')) df = pd.read_csv(scans_tsv, sep='\t') @@ -242,60 +246,56 @@ def test_edf(): def test_bdf(): - """Test raw_to_bids conversion for Biosemi data.""" + """Test write_raw_bids conversion for Biosemi data.""" output_path = _TempDir() data_path = op.join(base_path, 'edf', 'tests', 'data') raw_fname = op.join(data_path, 'test.bdf') - raw_to_bids(subject_id=subject_id, session_id=session_id, run=run, - task=task, acquisition=acq, raw_file=raw_fname, - output_path=output_path, overwrite=False, kind='eeg') + raw = mne.io.read_raw_edf(raw_fname) + write_raw_bids(raw, bids_basename, output_path, overwrite=False) cmd = ['bids-validator', '--bep006', output_path] run_subprocess(cmd, shell=shell) + raw.crop(0, raw.times[-2]) + with pytest.raises(AssertionError, match='cropped'): + write_raw_bids(raw, bids_basename, output_path) + def test_set(): - """Test raw_to_bids conversion for EEGLAB data.""" + """Test write_raw_bids conversion for EEGLAB data.""" # standalone .set file output_path = _TempDir() data_path = op.join(testing.data_path(), 'EEGLAB') - raw_fname = op.join(data_path, 'test_raw_onefile.set') - - raw_to_bids(subject_id=subject_id, session_id=session_id, run=run, - task=task, acquisition=acq, raw_file=raw_fname, - output_path=output_path, overwrite=False, kind='eeg') - - cmd = ['bids-validator', '--bep006', output_path] - run_subprocess(cmd, shell=shell) - - with pytest.raises(OSError, match="already exists"): - raw_to_bids(subject_id=subject_id, session_id=session_id, run=run, - task=task, acquisition=acq, raw_file=raw_fname, - output_path=output_path, overwrite=False, kind='eeg') # .set with associated .fdt output_path = _TempDir() data_path = op.join(testing.data_path(), 'EEGLAB') raw_fname = op.join(data_path, 'test_raw.set') - raw_to_bids(subject_id=subject_id, session_id=session_id, run=run, - task=task, acquisition=acq, raw_file=raw_fname, - output_path=output_path, overwrite=False, kind='eeg') + raw = mne.io.read_raw_eeglab(raw_fname) + write_raw_bids(raw, bids_basename, output_path, overwrite=False) + + with pytest.raises(OSError, match="already exists"): + write_raw_bids(raw, bids_basename, output_path=output_path, + overwrite=False) cmd = ['bids-validator', '--bep006', output_path] run_subprocess(cmd, shell=shell) def test_cnt(): - """Test raw_to_bids conversion for Neuroscan data.""" + """Test write_raw_bids conversion for Neuroscan data.""" output_path = _TempDir() data_path = op.join(testing.data_path(), 'CNT') raw_fname = op.join(data_path, 'scan41_short.cnt') - raw_to_bids(subject_id=subject_id, session_id=session_id, run=run, - task=task, acquisition=acq, raw_file=raw_fname, - output_path=output_path, overwrite=False, kind='eeg') + raw = mne.io.read_raw_cnt(raw_fname, montage=None) + write_raw_bids(raw, bids_basename, output_path) cmd = ['bids-validator', '--bep006', output_path] run_subprocess(cmd, shell=shell) + + raw = mne.io.read_raw_cnt(raw_fname, montage=None, preload=True) + with pytest.raises(ValueError, match='preload'): + write_raw_bids(raw, bids_basename, output_path) diff --git a/mne_bids/tests/test_utils.py b/mne_bids/tests/test_utils.py index cc59583da..9619f777c 100644 --- a/mne_bids/tests/test_utils.py +++ b/mne_bids/tests/test_utils.py @@ -13,8 +13,8 @@ from mne.datasets import testing from mne.utils import _TempDir -from mne_bids.utils import (make_bids_folders, make_bids_filename, - _check_types, print_dir_tree, age_on_date, +from mne_bids.utils import (make_bids_folders, make_bids_basename, + _check_types, print_dir_tree, _age_on_date, _get_brainvision_paths, copyfile_brainvision, copyfile_eeglab, _infer_eeg_placement_scheme) @@ -36,28 +36,29 @@ def test_make_filenames(): prefix_data = dict(subject='one', session='two', task='three', acquisition='four', run='five', processing='six', recording='seven', suffix='suffix.csv') - assert make_bids_filename(**prefix_data) == 'sub-one_ses-two_task-three_acq-four_run-five_proc-six_recording-seven_suffix.csv' # noqa + assert make_bids_basename(**prefix_data) == 'sub-one_ses-two_task-three_acq-four_run-five_proc-six_recording-seven_suffix.csv' # noqa # subsets of keys works - assert make_bids_filename(subject='one', task='three') == 'sub-one_task-three' # noqa - assert make_bids_filename(subject='one', task='three', suffix='hi.csv') == 'sub-one_task-three_hi.csv' # noqa + assert make_bids_basename(subject='one', task='three') == 'sub-one_task-three' # noqa + assert make_bids_basename(subject='one', task='three', suffix='hi.csv') == 'sub-one_task-three_hi.csv' # noqa with pytest.raises(ValueError): - make_bids_filename(subject='one-two', suffix='there.csv') + make_bids_basename(subject='one-two', suffix='there.csv') def test_make_folders(): """Test that folders are created and named properly.""" # Make sure folders are created properly output_path = _TempDir() - make_bids_folders(subject='hi', session='foo', kind='ba', root=output_path) + make_bids_folders(subject='hi', session='foo', kind='ba', + output_path=output_path) assert op.isdir(op.join(output_path, 'sub-hi', 'ses-foo', 'ba')) # If we remove a kwarg the folder shouldn't be created output_path = _TempDir() - make_bids_folders(subject='hi', kind='ba', root=output_path) + make_bids_folders(subject='hi', kind='ba', output_path=output_path) assert op.isdir(op.join(output_path, 'sub-hi', 'ba')) # check overwriting of folders - make_bids_folders(subject='hi', kind='ba', root=output_path, + make_bids_folders(subject='hi', kind='ba', output_path=output_path, overwrite=True, verbose=True) @@ -75,11 +76,11 @@ def test_age_on_date(): exp2 = datetime(2018, 1, 26) exp3 = datetime(2018, 1, 27) exp4 = datetime(1990, 1, 1) - assert age_on_date(bday, exp1) == 23 - assert age_on_date(bday, exp2) == 24 - assert age_on_date(bday, exp3) == 24 + assert _age_on_date(bday, exp1) == 23 + assert _age_on_date(bday, exp2) == 24 + assert _age_on_date(bday, exp3) == 24 with pytest.raises(ValueError): - age_on_date(bday, exp4) + _age_on_date(bday, exp4) def test_get_brainvision_paths(): diff --git a/mne_bids/utils.py b/mne_bids/utils.py index d5896f457..34a2ccfe6 100644 --- a/mne_bids/utils.py +++ b/mne_bids/utils.py @@ -62,14 +62,51 @@ def _mkdir_p(path, overwrite=False, verbose=False): raise -def make_bids_filename(subject=None, session=None, task=None, +def _parse_bids_filename(fname, verbose): + """Get dict from BIDS fname.""" + keys = ['sub', 'ses', 'task', 'acq', 'run', 'proc', 'run', 'space', + 'recording'] + params = {key: None for key in keys} + entities = fname.split('_') + idx_key = 0 + for entity in entities: + assert '-' in entity + key, value = entity.split('-') + if key not in keys: + raise KeyError('Unexpected entity ''%s'' found in filename ''%s''' + % (entity, fname)) + if keys.index(key) < idx_key: + raise ValueError('Entities in filename not ordered correctly.' + ' "%s" should have occured earlier in the ' + 'filename "%s"' % (key, fname)) + idx_key = keys.index(key) + params[key] = value + return params + + +def _handle_kind(raw): + """Get kind.""" + if 'meg' in raw: + kind = 'meg' + elif 'eeg' in raw: + kind = 'eeg' + elif 'ecog' in raw: + kind = 'ieeg' + else: + raise ValueError('Neither MEG/EEG/iEEG channels found in data.' + 'Please use raw.set_channel_types to set the ' + 'channel types in the data.') + return kind + + +def make_bids_basename(subject=None, session=None, task=None, acquisition=None, run=None, processing=None, - recording=None, space=None, suffix=None, prefix=None): - """Create a BIDS filename from its component parts. + recording=None, space=None, prefix=None, suffix=None): + """Create a partial/full BIDS filename from its component parts. BIDS filename prefixes have one or more pieces of metadata in them. They must follow a particular order, which is followed by this function. This - will generate the *prefix* for a BIDS file name that can be used with many + will generate the *prefix* for a BIDS filename that can be used with many subsequent files, or you may also give a suffix that will then complete the file name. @@ -94,11 +131,11 @@ def make_bids_filename(subject=None, session=None, task=None, The recording name for this item. Corresponds to "recording". space : str | None The coordinate space for an anatomical file. Corresponds to "space". - suffix : str | None - The suffix of a file that begins with this prefix. E.g., 'audio.wav'. prefix : str | None The prefix for the filename to be created. E.g., a path to the folder in which you wish to create a file with this name. + suffix : str | None + The suffix of a file that begins with this prefix. E.g., 'audio.wav'. Returns ------- @@ -107,7 +144,7 @@ def make_bids_filename(subject=None, session=None, task=None, Examples -------- - >>> print(make_bids_filename(subject='test', session='two', task='mytask', suffix='data.csv')) # noqa + >>> print(make_bids_basename(subject='test', session='two', task='mytask', suffix='data.csv')) # noqa sub-test_ses-two_task-mytask_data.csv """ @@ -143,12 +180,12 @@ def make_bids_filename(subject=None, session=None, task=None, return filename -def make_bids_folders(subject, session=None, kind=None, root=None, +def make_bids_folders(subject, session=None, kind=None, output_path=None, make_dir=True, overwrite=False, verbose=False): """Create a BIDS folder hierarchy. This creates a hierarchy of folders *within* a BIDS dataset. You should - plan to create these folders *inside* the root folder of the dataset. + plan to create these folders *inside* the output_path folder of the dataset. Parameters ---------- @@ -159,8 +196,8 @@ def make_bids_folders(subject, session=None, kind=None, root=None, "anat", "func", etc. session : str | None The session for a item. Corresponds to "ses". - root : str | None - The root for the folders to be created. If None, folders will be + output_path : str | None + The output_path for the folders to be created. If None, folders will be created in the current working directory. make_dir : bool Whether to actually create the folders specified. If False, only a @@ -182,11 +219,12 @@ def make_bids_folders(subject, session=None, kind=None, root=None, Examples -------- >>> print(make_bids_folders('sub_01', session='my_session', - kind='meg', root='path/to/project', make_dir=False)) # noqa + kind='meg', output_path='path/to/project', + make_dir=False)) # noqa path/to/project/sub-sub_01/ses-my_session/meg """ - _check_types((subject, kind, session, root)) + _check_types((subject, kind, session, output_path)) if session is not None: _check_key_val('ses', session) @@ -196,8 +234,8 @@ def make_bids_folders(subject, session=None, kind=None, root=None, if isinstance(kind, string_types): path.append(kind) path = op.join(*path) - if isinstance(root, string_types): - path = op.join(root, path) + if isinstance(output_path, string_types): + path = op.join(output_path, path) if make_dir is True: _mkdir_p(path, overwrite=overwrite, verbose=verbose) @@ -273,7 +311,7 @@ def make_dataset_description(path, name=None, data_license=None, _write_json(description, fname, overwrite=True, verbose=verbose) -def age_on_date(bday, exp_date): +def _age_on_date(bday, exp_date): """Calculate age from birthday and experiment date. Parameters diff --git a/setup.py b/setup.py index f47621237..681e03801 100644 --- a/setup.py +++ b/setup.py @@ -5,9 +5,9 @@ DISTNAME = 'mne-bids' DESCRIPTION = descr -MAINTAINER = 'Alexandre Gramfort' -MAINTAINER_EMAIL = 'alexandre.gramfort@telecom-paristech.fr' -URL = 'http://martinos.org/mne' +MAINTAINER = 'Mainak Jas' +MAINTAINER_EMAIL = 'mainakjas@gmail.com' +URL = 'https://mne-tools.github.io/mne-bids/' LICENSE = 'BSD (3-clause)' DOWNLOAD_URL = 'http://github.com/mne-tools/mne-bids' VERSION = '0.1.dev0'