def test_edf(_bids_validate): """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'}) write_raw_bids(raw, bids_basename, output_path) # Reading the file back should raise an error, because we renamed channels # in `raw` and used that information to write a channels.tsv. Yet, we # saved the unchanged `raw` in the BIDS folder, so channels in the TSV and # in raw clash with pytest.raises(RuntimeError, match='Channels do not correspond'): read_raw_bids(bids_basename + '_eeg.edf', output_path) bids_fname = bids_basename.replace('run-01', 'run-%s' % run2) write_raw_bids(raw, bids_fname, output_path, overwrite=True) _bids_validate(output_path) # ensure there is an EMG channel in the channels.tsv: 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')) data = _from_tsv(channels_tsv) assert 'ElectroMyoGram' in data['description'] # check that the scans list contains two scans scans_tsv = make_bids_basename(subject=subject_id, session=session_id, suffix='scans.tsv', prefix=op.join(output_path, 'sub-01', 'ses-01')) data = _from_tsv(scans_tsv) assert len(list(data.values())[0]) == 2 # Also cover iEEG # We use the same data and pretend that eeg channels are ecog raw.set_channel_types( {raw.ch_names[i]: 'ecog' for i in mne.pick_types(raw.info, eeg=True)}) output_path = _TempDir() write_raw_bids(raw, bids_basename, output_path) _bids_validate(output_path)
def test_tsv_handler(tmp_path): """Test the TSV handling.""" # create some dummy data d = odict(a=[1, 2, 3, 4], b=['five', 'six', 'seven', 'eight']) assert _contains_row(d, {'a': 1, 'b': 'five'}) d2 = odict(a=[5], b=['nine']) d = _combine_rows(d, d2) assert 5 in d['a'] d2 = odict(a=[5]) d = _combine_rows(d, d2) assert 'n/a' in d['b'] d2 = odict(a=[5], b=['ten']) d = _combine_rows(d, d2, drop_column='a') # make sure that the repeated data was dropped assert 'nine' not in d['b'] print(_tsv_to_str(d)) d_path = tmp_path / 'output.tsv' # write the data to an output tsv file _to_tsv(d, d_path) # now read it back d = _from_tsv(d_path) # test reading the file in with the incorrect number of datatypes raises # an Error with pytest.raises(ValueError): d = _from_tsv(d_path, dtypes=[str]) # we can also pass just a single data type and it will be applied to all # columns d = _from_tsv(d_path, str) # remove any rows with 2 or 5 in them d = _drop(d, [2, 5], 'a') assert 2 not in d['a'] # test combining data with differing numbers of columns d = odict(a=[1, 2], b=['three', 'four']) d2 = odict(a=[4], b=['five'], c=[3.1415]) # raise error if a new column is tried to be added with pytest.raises(KeyError): d = _combine_rows(d, d2) d2 = odict(a=[5]) d = _combine_rows(d, d2) assert d['b'] == ['three', 'four', 'n/a'] assert _contains_row(d, {'a': 5}) # test reading a single column _to_tsv(odict(a=[1, 2, 3, 4]), d_path) d = _from_tsv(d_path) assert d['a'] == ['1', '2', '3', '4']
def test_read_participants_data(tmpdir): """Test reading information from a BIDS sidecar.json file.""" bids_path = _bids_path.copy().update(root=tmpdir, datatype='meg') raw = _read_raw_fif(raw_fname, verbose=False) # if subject info was set, we don't roundtrip birthday # due to possible anonymization in mne-bids subject_info = { 'hand': 1, 'sex': 2, } raw.info['subject_info'] = subject_info write_raw_bids(raw, bids_path, overwrite=True, verbose=False) raw = read_raw_bids(bids_path=bids_path) print(raw.info['subject_info']) assert raw.info['subject_info']['hand'] == 1 assert raw.info['subject_info']['sex'] == 2 assert raw.info['subject_info'].get('birthday', None) is None assert raw.info['subject_info']['his_id'] == f'sub-{bids_path.subject}' assert 'participant_id' not in raw.info['subject_info'] # if modifying participants tsv, then read_raw_bids reflects that participants_tsv_fpath = tmpdir / 'participants.tsv' participants_tsv = _from_tsv(participants_tsv_fpath) participants_tsv['hand'][0] = 'n/a' _to_tsv(participants_tsv, participants_tsv_fpath) raw = read_raw_bids(bids_path=bids_path) assert raw.info['subject_info']['hand'] == 0 assert raw.info['subject_info']['sex'] == 2 assert raw.info['subject_info'].get('birthday', None) is None # make sure things are read even if the entries don't make sense participants_tsv = _from_tsv(participants_tsv_fpath) participants_tsv['hand'][0] = 'righty' participants_tsv['sex'][0] = 'malesy' _to_tsv(participants_tsv, participants_tsv_fpath) with pytest.warns(RuntimeWarning, match='Unable to map'): raw = read_raw_bids(bids_path=bids_path) assert raw.info['subject_info']['hand'] is None assert raw.info['subject_info']['sex'] is None # make sure to read in if no participants file raw = _read_raw_fif(raw_fname, verbose=False) write_raw_bids(raw, bids_path, overwrite=True, verbose=False) os.remove(participants_tsv_fpath) with pytest.warns(RuntimeWarning, match='Participants file not found'): raw = read_raw_bids(bids_path=bids_path) assert raw.info['subject_info'] is None
def _handle_electrodes_reading(electrodes_fname, coord_frame, coord_unit): """Read associated electrodes.tsv and populate raw. Handle xyz coordinates and coordinate frame of each channel. """ logger.info('Reading electrode ' 'coords from {}.'.format(electrodes_fname)) electrodes_dict = _from_tsv(electrodes_fname) ch_names_tsv = electrodes_dict['name'] def _float_or_nan(val): if val == "n/a": return np.nan else: return float(val) # convert coordinates to float and create list of tuples electrodes_dict['x'] = [_float_or_nan(x) for x in electrodes_dict['x']] electrodes_dict['y'] = [_float_or_nan(x) for x in electrodes_dict['y']] electrodes_dict['z'] = [_float_or_nan(x) for x in electrodes_dict['z']] ch_names_raw = [ x for i, x in enumerate(ch_names_tsv) if electrodes_dict['x'][i] != "n/a" ] ch_locs = np.c_[electrodes_dict['x'], electrodes_dict['y'], electrodes_dict['z']] # convert coordinates to meters ch_locs = _scale_coord_to_meters(ch_locs, coord_unit) # create mne.DigMontage ch_pos = dict(zip(ch_names_raw, ch_locs)) montage = mne.channels.make_dig_montage(ch_pos=ch_pos, coord_frame=coord_frame) return montage
def test_bdf(_bids_validate): """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 = mne.io.read_raw_bdf(raw_fname) with pytest.warns(UserWarning, match='No line frequency found'): write_raw_bids(raw, bids_basename, output_path, overwrite=False) _bids_validate(output_path) # Test also the reading of channel types from channels.tsv # the first channel in the raw data is not MISC right now test_ch_idx = 0 assert coil_type(raw.info, test_ch_idx) != 'misc' # we will change the channel type to MISC and overwrite the channels file bids_fname = bids_basename + '_eeg.bdf' channels_fname = _find_matching_sidecar(bids_fname, output_path, 'channels.tsv') channels_dict = _from_tsv(channels_fname) channels_dict['type'][test_ch_idx] = 'MISC' _to_tsv(channels_dict, channels_fname) # Now read the raw data back from BIDS, with the tampered TSV, to show # that the channels.tsv truly influences how read_raw_bids sets ch_types # in the raw data object raw = read_raw_bids(bids_fname, output_path) assert coil_type(raw.info, test_ch_idx) == 'misc' # Test cropped assertion error raw = mne.io.read_raw_bdf(raw_fname) raw.crop(0, raw.times[-2]) with pytest.raises(AssertionError, match='cropped'): write_raw_bids(raw, bids_basename, output_path)
def _handle_participants_reading(participants_fname, raw, subject, verbose=None): participants_tsv = _from_tsv(participants_fname) subjects = participants_tsv['participant_id'] row_ind = subjects.index(subject) # set data from participants tsv into subject_info for infokey, infovalue in participants_tsv.items(): if infokey == 'sex' or infokey == 'hand': value = _map_options(what=infokey, key=infovalue[row_ind], fro='bids', to='mne') # We don't know how to translate to MNE, so skip. if value is None: if infokey == 'sex': info_str = 'subject sex' else: info_str = 'subject handedness' warn(f'Unable to map `{infokey}` value to MNE. ' f'Not setting {info_str}.') else: value = infovalue[row_ind] # add data into raw.Info if raw.info['subject_info'] is None: raw.info['subject_info'] = dict() raw.info['subject_info'][infokey] = value return raw
def test_vhdr(_bids_validate): """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 = mne.io.read_raw_brainvision(raw_fname) # inject a bad channel assert not raw.info['bads'] injected_bad = ['FP1'] raw.info['bads'] = injected_bad # write with injected bad channels write_raw_bids(raw, bids_basename_minimal, output_path, overwrite=False) _bids_validate(output_path) # read and also get the bad channels raw = read_raw_bids(bids_basename_minimal + '_eeg.vhdr', output_path) with pytest.raises(TypeError, match="unexpected keyword argument 'foo'"): read_raw_bids(bids_basename_minimal + '_eeg.vhdr', output_path, extra_params=dict(foo='bar')) # Check that injected bad channel shows up in raw after reading np.testing.assert_array_equal(np.asarray(raw.info['bads']), np.asarray(injected_bad)) # Test that correct channel units are written ... and that bad channel # is in channels.tsv channels_tsv_name = op.join(output_path, 'sub-{}'.format(subject_id), 'eeg', bids_basename_minimal + '_channels.tsv') data = _from_tsv(channels_tsv_name) assert data['units'][data['name'].index('FP1')] == 'µV' assert data['units'][data['name'].index('CP5')] == 'n/a' assert data['status'][data['name'].index(injected_bad[0])] == 'bad' # check events.tsv is written events_tsv_fname = channels_tsv_name.replace('channels', 'events') assert op.exists(events_tsv_fname) # create another bids folder with the overwrite command and check # no files are in the folder data_path = make_bids_folders(subject=subject_id, kind='eeg', output_path=output_path, overwrite=True) assert len([f for f in os.listdir(data_path) if op.isfile(f)]) == 0 # test anonymize and convert raw = mne.io.read_raw_brainvision(raw_fname) _test_anonymize(raw, bids_basename) # Also cover iEEG # We use the same data and pretend that eeg channels are ecog raw = mne.io.read_raw_brainvision(raw_fname) raw.set_channel_types({raw.ch_names[i]: 'ecog' for i in mne.pick_types(raw.info, eeg=True)}) output_path = _TempDir() write_raw_bids(raw, bids_basename, output_path, overwrite=False) _bids_validate(output_path)
def _handle_electrodes_reading(electrodes_fname, coord_frame, raw, verbose): """Read associated electrodes.tsv and populate raw. Handle xyz coordinates and coordinate frame of each channel. Assumes units of coordinates are in 'm'. """ logger.info('Reading electrode ' 'coords from {}.'.format(electrodes_fname)) electrodes_dict = _from_tsv(electrodes_fname) # First, make sure that ordering of names in channels.tsv matches the # ordering of names in the raw data. The "name" column is mandatory in BIDS ch_names_raw = list(raw.ch_names) ch_names_tsv = electrodes_dict['name'] if ch_names_raw != ch_names_tsv: msg = ('Channels do not correspond between raw data and the ' 'channels.tsv file. For MNE-BIDS, the channel names in the ' 'tsv MUST be equal and in the same order as the channels in ' 'the raw data.\n\n' '{} channels in tsv file: "{}"\n\n --> {}\n\n' '{} channels in raw file: "{}"\n\n --> {}\n\n' .format(len(ch_names_tsv), electrodes_fname, ch_names_tsv, len(ch_names_raw), raw.filenames, ch_names_raw) ) # XXX: this could be due to MNE inserting a 'STI 014' channel as the # last channel: In that case, we can work. --> Can be removed soon, # because MNE will stop the synthesis of stim channels in the near # future if not (ch_names_raw[-1] == 'STI 014' and ch_names_raw[:-1] == ch_names_tsv): raise RuntimeError(msg) if verbose: print("The read in electrodes file is: \n", electrodes_dict) # convert coordinates to float and create list of tuples ch_names_raw = [x for i, x in enumerate(ch_names_raw) if electrodes_dict['x'][i] != "n/a"] electrodes_dict['x'] = [float(x) for x in electrodes_dict['x'] if x != "n/a"] electrodes_dict['y'] = [float(x) for x in electrodes_dict['y'] if x != "n/a"] electrodes_dict['z'] = [float(x) for x in electrodes_dict['z'] if x != "n/a"] ch_locs = list(zip(electrodes_dict['x'], electrodes_dict['y'], electrodes_dict['z'])) ch_pos = dict(zip(ch_names_raw, ch_locs)) # create mne.DigMontage montage = mne.channels.make_dig_montage(ch_pos=ch_pos, coord_frame=coord_frame) raw.set_montage(montage) return raw
def test_write_read_fif_split_file(tmpdir): """Test split files are read correctly.""" # load raw test file, extend it to be larger than 2gb, and save it bids_root = tmpdir.mkdir('bids') tmp_dir = tmpdir.mkdir('tmp') bids_path = _bids_path.copy().update(root=bids_root, datatype='meg') raw = _read_raw_fif(raw_fname, verbose=False) bids_path.update(acquisition=None) write_raw_bids(raw, bids_path, verbose=False) bids_path.update(acquisition='01') n_channels = len(raw.ch_names) n_times = int(2.2e9 / (n_channels * 4)) # enough to produce a split data = np.empty((n_channels, n_times), dtype=np.float32) raw = mne.io.RawArray(data, raw.info) big_fif_fname = pathlib.Path(tmp_dir) / 'test_raw.fif' raw.save(big_fif_fname) raw = _read_raw_fif(big_fif_fname, verbose=False) write_raw_bids(raw, bids_path, verbose=False) # test whether split raw files were read correctly raw1 = read_raw_bids(bids_path=bids_path) assert 'split-01' in str(bids_path.fpath) bids_path.update(split='01') raw2 = read_raw_bids(bids_path=bids_path) bids_path.update(split='02') raw3 = read_raw_bids(bids_path=bids_path) assert len(raw) == len(raw1) assert len(raw) == len(raw2) assert len(raw) > len(raw3) # check that split files both appear in scans.tsv scans_tsv = BIDSPath(subject=subject_id, session=session_id, suffix='scans', extension='.tsv', root=bids_root) scan_data = _from_tsv(scans_tsv) scan_fnames = scan_data['filename'] scan_acqtime = scan_data['acq_time'] assert len(scan_fnames) == 3 assert 'split-01' in scan_fnames[0] and 'split-02' in scan_fnames[1] # check that the acq_times in scans.tsv are the same assert scan_acqtime[0] == scan_acqtime[1] # check the recordings are in the correct order assert raw2.first_time < raw3.first_time # check whether non-matching acq_times are caught scan_data['acq_time'][0] = scan_acqtime[0].split('.')[0] _to_tsv(scan_data, scans_tsv) with pytest.raises(ValueError, match='Split files must have the same acq_time.'): read_raw_bids(bids_path) # reset scans.tsv file for downstream tests scan_data['acq_time'][0] = scan_data['acq_time'][1] _to_tsv(scan_data, scans_tsv)
def _update_electrodes_tsv(electrodes_tsv_fpath, elec_labels_anat, atlas_depth): electrodes_tsv = _from_tsv(electrodes_tsv_fpath) if atlas_depth not in electrodes_tsv.keys(): electrodes_tsv[atlas_depth] = ["n/a"] * len(elec_labels_anat) for i in range(len(elec_labels_anat)): ch_name = electrodes_tsv["name"][i] print(ch_name, elec_labels_anat[i]) electrodes_tsv[atlas_depth][i] = elec_labels_anat[i] _to_tsv(electrodes_tsv, electrodes_tsv_fpath) return electrodes_tsv
def _handle_channels_reading(channels_fname, raw): """Read associated channels.tsv and populate raw. Updates status (bad) and types of channels. """ logger.info('Reading channel info from {}.'.format(channels_fname)) channels_dict = _from_tsv(channels_fname) ch_names_tsv = channels_dict['name'] # Now we can do some work. # The "type" column is mandatory in BIDS. We can use it to set channel # types in the raw data using a mapping between channel types channel_type_dict = dict() # Get the best mapping we currently have from BIDS to MNE nomenclature bids_to_mne_ch_types = _get_ch_type_mapping(fro='bids', to='mne') ch_types_json = channels_dict['type'] for ch_name, ch_type in zip(ch_names_tsv, ch_types_json): # Try to map from BIDS nomenclature to MNE, leave channel type # untouched if we are uncertain updated_ch_type = bids_to_mne_ch_types.get(ch_type, None) if updated_ch_type is None: # XXX Try again with uppercase spelling – this should be removed # XXX once https://github.com/bids-standard/bids-validator/issues/1018 # noqa:E501 # XXX has been resolved. # XXX x-ref https://github.com/mne-tools/mne-bids/issues/481 updated_ch_type = bids_to_mne_ch_types.get(ch_type.upper(), None) if updated_ch_type is not None: msg = ('The BIDS dataset contains channel types in lowercase ' 'spelling. This violates the BIDS specification and ' 'will raise an error in the future.') warn(msg) if updated_ch_type is not None: channel_type_dict[ch_name] = updated_ch_type # Rename channels in loaded Raw to match those read from the BIDS sidecar for bids_ch_name, raw_ch_name in zip(ch_names_tsv, raw.ch_names.copy()): if bids_ch_name != raw_ch_name: raw.rename_channels({raw_ch_name: bids_ch_name}) # Set the channel types in the raw data according to channels.tsv raw.set_channel_types(channel_type_dict) # Set bad channels based on _channels.tsv sidecar if 'status' in channels_dict: bads = _get_bads_from_tsv_data(channels_dict) raw.info['bads'] = bads return raw
def load_data(elecfile): """ Load electrodes file. Parameters ---------- elecfile: str Path to space-delimited text file of contact labels and contact coordinates in mm space. Returns ------- ct_img: NiBabel image object NiBabel image object of CT scan input. brainmask_ct: NiBabel image object NiBabel image object of brain mask in CT. elecinitfile: dict() A dictionary of contact coordinates in mm space. Keys are individual contact labels, and values are the corresponding coordinates in mm space. """ elec_coords_mm = {} if elecfile.endswith(".txt"): with open(elecfile) as f: for l in f: row = l.split() elec_coords_mm[row[0]] = np.array(list(map(float, row[1:]))) elif elecfile.endswith(".tsv"): electrodes_tsv = _from_tsv(elecfile) ch_names = electrodes_tsv["name"] elecmatrix = [] for i in range(len(ch_names)): elecmatrix.append([electrodes_tsv[x][i] for x in ["x", "y", "z"]]) elecmatrix = np.array(elecmatrix, dtype=float) for ch_name, coord in zip(ch_names, elecmatrix): elec_coords_mm[ch_name] = coord else: matreader = MatReader() data = matreader.loadmat(elecfile) elec_coords_mm = data["data"] print( f"\n\nFinished loading electrode locations data ({len(elec_coords_mm)}: " ) print(list(elec_coords_mm.keys())) return elec_coords_mm
def print_channel_status(bids_root, subject_id, session_id, acquisition_id): """ Print the channel and status of every channel for a certain acquisition type. Parameters ---------- bids_root : Union[Path or str] The directory where data is being stored. subject_id : str The unique identifier of the subject session_id: str The identifier of the recording session, usually 'seizure' or 'interictal' acquisition_id : str One of 'eeg', 'ecog', or 'seeg' """ channel_fpaths = _get_subject_channels_tsvs( bids_root, subject_id, session_id, acquisition_id ) logger.info(f"Found channel files for subject {subject_id}: {channel_fpaths}") if len(channel_fpaths) == 0: logger.error(f"No channel files for subject: {subject_id}") raise RuntimeError(f"No channel files for subject: {subject_id}.") channel_fpath = channel_fpaths[0] logger.info(f"Using channel file '{channel_fpath}'") # Read in current channel statuses channel_tsv = _from_tsv(channel_fpath) bad_chs = [] good_chs = [] for i, ch in enumerate(channel_tsv["name"]): if channel_tsv["status"][i] == "bad": bad_chs.append(ch) else: good_chs.append(ch) # create string of channels to display to user if len(bad_chs) == 0: bad_channels_str = "None" else: bad_channels_str = ", ".join(bad_chs) if len(bad_chs) == 0: good_channels_str = "None" else: good_channels_str = ", ".join(good_chs) click.echo(f"Bad channels for {subject_id}:") click.echo(bad_channels_str) click.echo(f"Channels included in analysis for {subject_id}:") click.echo(good_channels_str)
def _test_anonymize(raw, bids_basename, events_fname=None, event_id=None): bids_root = _TempDir() write_raw_bids(raw, bids_basename, bids_root, events_data=events_fname, event_id=event_id, anonymize=dict(daysback=33000), overwrite=False) scans_tsv = make_bids_basename( subject=subject_id, session=session_id, suffix='scans.tsv', prefix=op.join(bids_root, 'sub-01', 'ses-01')) data = _from_tsv(scans_tsv) if data['acq_time'] is not None and data['acq_time'][0] != 'n/a': assert datetime.strptime(data['acq_time'][0], '%Y-%m-%dT%H:%M:%S').year < 1925 return bids_root
def _handle_events_reading(events_fname, raw,combine_trialType_value=True): """Read associated events.tsv and populate raw. Handle onset, duration, and description of each event. """ logger.info('Reading events from {}.'.format(events_fname)) events_dict = _from_tsv(events_fname) # Get the descriptions of the events if 'trial_type' in events_dict: if combine_trialType_value and ('value' in events_dict): descriptions = np.asarray([a+':'+b for a,b in zip(events_dict["trial_type"],events_dict["value"])], dtype=str) # Drop events unrelated to a trial type events_dict = _drop(events_dict, 'n/a', 'trial_type') descriptions = np.asarray(events_dict['trial_type'], dtype=str) # If we don't have a proper description of the events, perhaps we have # at least an event value? elif 'value' in events_dict: # Drop events unrelated to value events_dict = _drop(events_dict, 'n/a', 'value') descriptions = np.asarray(events_dict['value'], dtype=str) # Worst case, we go with 'n/a' for all events else: descriptions = 'n/a' # Deal with "n/a" strings before converting to float ons = [np.nan if on == 'n/a' else on for on in events_dict['onset']] dus = [0 if du == 'n/a' else du for du in events_dict['duration']] onsets = np.asarray(ons, dtype=float) durations = np.asarray(dus, dtype=float) # Keep only events where onset is known good_events_idx = ~np.isnan(onsets) onsets = onsets[good_events_idx] durations = durations[good_events_idx] descriptions = descriptions[good_events_idx] del good_events_idx # Add Events to raw as annotations annot_from_events = mne.Annotations(onset=onsets, duration=durations, description=descriptions, orig_time=None) raw.set_annotations(annot_from_events) return raw
def _scans_tsv(raw, raw_fname, fname, overwrite=False, verbose=True): """Create a scans.tsv file and save it. Parameters ---------- raw : instance of Raw The data as MNE-Python Raw object. raw_fname : str Relative path to the raw data file. fname : str Filename to save the scans.tsv to. overwrite : bool Defaults to False. Whether to overwrite the existing data in the file. If there is already data for the given `fname` and overwrite is False, an error will be raised. verbose : bool Set verbose output to true or false. """ # get measurement date from the data info meas_date = raw.info['meas_date'] if isinstance(meas_date, (tuple, list, np.ndarray)): meas_date = meas_date[0] acq_time = datetime.fromtimestamp(meas_date).strftime( '%Y-%m-%dT%H:%M:%S') else: acq_time = 'n/a' data = OrderedDict([('filename', ['%s' % raw_fname.replace(os.sep, '/')]), ('acq_time', [acq_time])]) if os.path.exists(fname): orig_data = _from_tsv(fname) # if the file name is already in the file raise an error if raw_fname in orig_data['filename'] and not overwrite: raise FileExistsError( '"%s" already exists in the scans list. ' # noqa: E501 F821 'Please set overwrite to True.' % raw_fname) # otherwise add the new data data = _combine(orig_data, data, 'filename') # overwrite is forced to True as all issues with overwrite == False have # been handled by this point _write_tsv(fname, data, True, verbose) return fname
def _handle_scans_reading(scans_fname, raw, bids_path, verbose=False): """Read associated scans.tsv and set meas_date.""" scans_tsv = _from_tsv(scans_fname) fname = bids_path.fpath.name if fname.endswith('.pdf'): # for BTI files, the scan is an entire directory fname = fname.split('.')[0] # get the row corresponding to the file # use string concatenation instead of os.path # to work nicely with windows data_fname = bids_path.datatype + '/' + fname fnames = scans_tsv['filename'] acq_times = scans_tsv['acq_time'] row_ind = fnames.index(data_fname) # check whether all split files have the same acq_time # and throw an error if they don't if '_split-' in fname: split_idx = fname.find('split-') pattern = re.compile(bids_path.datatype + '/' + bids_path.basename[:split_idx] + r'split-\d+_' + bids_path.datatype + bids_path.fpath.suffix) split_fnames = list(filter(pattern.match, fnames)) split_acq_times = [] for split_f in split_fnames: split_acq_times.append(acq_times[fnames.index(split_f)]) if len(set(split_acq_times)) != 1: raise ValueError("Split files must have the same acq_time.") # extract the acquisition time from scans file acq_time = acq_times[row_ind] if acq_time != 'n/a': # microseconds in the acquisition time is optional if '.' not in acq_time: # acquisition time ends with '.%fZ' microseconds string acq_time += '.0Z' acq_time = datetime.strptime(acq_time, '%Y-%m-%dT%H:%M:%S.%fZ') acq_time = acq_time.replace(tzinfo=timezone.utc) if verbose: logger.debug(f'Loaded {scans_fname} scans file to set ' f'acq_time as {acq_time}.') raw.set_meas_date(acq_time) return raw
def test_handle_coords_reading(): """Test reading coordinates from BIDS files.""" bids_root = _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) # ensure we are writing 'ecog'/'ieeg' data raw.set_channel_types({ch: 'ecog' for ch in raw.ch_names}) # set a `random` montage ch_names = raw.ch_names elec_locs = np.random.random((len(ch_names), 3)).astype(float) ch_pos = dict(zip(ch_names, elec_locs)) montage = mne.channels.make_dig_montage(ch_pos=ch_pos, coord_frame="mri") raw.set_montage(montage) write_raw_bids(raw, bids_basename, bids_root, overwrite=True) # read in the data and assert montage is the same bids_fname = bids_basename + "_ieeg.edf" raw_test = read_raw_bids(bids_fname, bids_root) # obtain the sensor positions orig_locs = raw.info['dig'][1] test_locs = raw_test.info['dig'][1] assert orig_locs == test_locs assert not object_diff(raw.info['chs'], raw_test.info['chs']) # test error message if electrodes don't match electrodes_fname = _find_matching_sidecar(bids_fname, bids_root, "electrodes.tsv", allow_fail=True) electrodes_dict = _from_tsv(electrodes_fname) # pop off 5 channels for key in electrodes_dict.keys(): for i in range(5): electrodes_dict[key].pop() _to_tsv(electrodes_dict, electrodes_fname) with pytest.raises(RuntimeError, match='Channels do not correspond'): raw_test = read_raw_bids(bids_fname, bids_root)
def test_read_participants_handedness_and_sex_mapping(hand_bids, hand_mne, sex_bids, sex_mne): """Test we're correctly mapping handedness and sex between BIDS and MNE.""" bids_root = _TempDir() bids_path = _bids_path.copy().update(root=bids_root, datatype='meg') participants_tsv_fpath = op.join(bids_root, 'participants.tsv') raw = _read_raw_fif(raw_fname, verbose=False) # Avoid that we end up with subject information stored in the raw data. raw.info['subject_info'] = {} write_raw_bids(raw, bids_path, overwrite=True, verbose=False) participants_tsv = _from_tsv(participants_tsv_fpath) participants_tsv['hand'][0] = hand_bids participants_tsv['sex'][0] = sex_bids _to_tsv(participants_tsv, participants_tsv_fpath) raw = read_raw_bids(bids_path=bids_path) assert raw.info['subject_info']['hand'] is hand_mne assert raw.info['subject_info']['sex'] is sex_mne
def test_handle_channel_type_casing(tmpdir): """Test that non-uppercase entries in the `type` column are accepted.""" bids_path = _bids_path.copy().update(root=tmpdir) raw = _read_raw_fif(raw_fname, verbose=False) write_raw_bids(raw, bids_path, overwrite=True, verbose=False) ch_path = bids_path.copy().update(root=tmpdir, datatype='meg', suffix='channels', extension='.tsv') bids_channels_fname = ch_path.fpath # Convert all channel type entries to lowercase. channels_data = _from_tsv(bids_channels_fname) channels_data['type'] = [t.lower() for t in channels_data['type']] _to_tsv(channels_data, bids_channels_fname) with pytest.warns(RuntimeWarning, match='lowercase spelling'): read_raw_bids(bids_path)
def test_handle_scans_reading(): """Test reading data from a BIDS scans.tsv file.""" bids_root = _TempDir() raw = _read_raw_fif(raw_fname) suffix = "meg" # write copy of raw with line freq of 60 # bids basename and fname bids_path = BIDSPath(subject='01', session='01', task='audiovisual', run='01', datatype=suffix, root=bids_root) bids_path = write_raw_bids(raw, bids_path, overwrite=True) raw_01 = read_raw_bids(bids_path) # find sidecar scans.tsv file and alter the # acquisition time to not have the optional microseconds scans_path = BIDSPath(subject=bids_path.subject, session=bids_path.session, root=bids_root, suffix='scans', extension='.tsv') scans_tsv = _from_tsv(scans_path) acq_time_str = scans_tsv['acq_time'][0] acq_time = datetime.strptime(acq_time_str, '%Y-%m-%dT%H:%M:%S.%fZ') acq_time = acq_time.replace(tzinfo=timezone.utc) new_acq_time = acq_time_str.split('.')[0] assert acq_time == raw_01.info['meas_date'] scans_tsv['acq_time'][0] = new_acq_time _to_tsv(scans_tsv, scans_path) # now re-load the data and it should be different # from the original date and the same as the newly altered date raw_02 = read_raw_bids(bids_path) new_acq_time += '.0Z' new_acq_time = datetime.strptime(new_acq_time, '%Y-%m-%dT%H:%M:%S.%fZ') new_acq_time = new_acq_time.replace(tzinfo=timezone.utc) assert raw_02.info['meas_date'] == new_acq_time assert new_acq_time != raw_01.info['meas_date']
def _handle_events_reading(events_fname, raw): """Read associated events.tsv and populate raw. Handle onset, duration, and description of each event. """ logger.info('Reading events from {}.'.format(events_fname)) events_dict = _from_tsv(events_fname) # Get the descriptions of the events if 'trial_type' in events_dict: # Drop events unrelated to a trial type events_dict = _drop(events_dict, 'n/a', 'trial_type') descriptions = np.asarray(events_dict['trial_type'], dtype=str) # If we don't have a proper description of the events, perhaps we have # at least an event value? elif 'value' in events_dict: # Drop events unrelated to value events_dict = _drop(events_dict, 'n/a', 'value') descriptions = np.asarray(events_dict['value'], dtype=str) # Worst case, we go with 'n/a' for all events else: descriptions = 'n/a' # Deal with "n/a" strings before converting to float ons = [np.nan if on == 'n/a' else on for on in events_dict['onset']] dus = [np.nan if du == 'n/a' else du for du in events_dict['duration']] # Add Events to raw as annotations onsets = np.asarray(ons, dtype=float) durations = np.asarray(dus, dtype=float) annot_from_events = mne.Annotations(onset=onsets, duration=durations, description=descriptions, orig_time=None) raw.set_annotations(annot_from_events) return raw
def _handle_scans_reading(scans_fname, raw, bids_path, verbose=False): """Read associated scans.tsv and set meas_date.""" scans_tsv = _from_tsv(scans_fname) fname = bids_path.fpath.name if '_split-' in fname: # for split files, scans only stores the filename without ``split`` extension = bids_path.fpath.suffix bids_path.update(split=None, extension=extension) fname = bids_path.basename elif fname.endswith('.pdf'): # for BTI files, the scan is an entire directory fname = fname.split('.')[0] # get the row corresponding to the file # use string concatenation instead of os.path # to work nicely with windows data_fname = bids_path.datatype + '/' + fname fnames = scans_tsv['filename'] acq_times = scans_tsv['acq_time'] row_ind = fnames.index(data_fname) # extract the acquisition time from scans file acq_time = acq_times[row_ind] if acq_time != 'n/a': # microseconds in the acquisition time is optional if '.' not in acq_time: # acquisition time ends with '.%fZ' microseconds string acq_time += '.0Z' acq_time = datetime.strptime(acq_time, '%Y-%m-%dT%H:%M:%S.%fZ') acq_time = acq_time.replace(tzinfo=timezone.utc) if verbose: logger.debug(f'Loaded {scans_fname} scans file to set ' f'acq_time as {acq_time}.') raw.set_meas_date(acq_time) return raw
def _handle_electrodes_reading(electrodes_fname, coord_frame, coord_unit, raw, verbose): """Read associated electrodes.tsv and populate raw. Handle xyz coordinates and coordinate frame of each channel. Assumes units of coordinates are in 'm'. """ logger.info('Reading electrode ' 'coords from {}.'.format(electrodes_fname)) electrodes_dict = _from_tsv(electrodes_fname) # First, make sure that ordering of names in channels.tsv matches the # ordering of names in the raw data. The "name" column is mandatory in BIDS ch_names_raw = list(raw.ch_names) ch_names_tsv = electrodes_dict['name'] if ch_names_raw != ch_names_tsv: msg = ('Channels do not correspond between raw data and the ' 'channels.tsv file. For MNE-BIDS, the channel names in the ' 'tsv MUST be equal and in the same order as the channels in ' 'the raw data.\n\n' '{} channels in tsv file: "{}"\n\n --> {}\n\n' '{} channels in raw file: "{}"\n\n --> {}\n\n' .format(len(ch_names_tsv), electrodes_fname, ch_names_tsv, len(ch_names_raw), raw.filenames, ch_names_raw) ) # XXX: this could be due to MNE inserting a 'STI 014' channel as the # last channel: In that case, we can work. --> Can be removed soon, # because MNE will stop the synthesis of stim channels in the near # future if not (ch_names_raw[-1] == 'STI 014' and ch_names_raw[:-1] == ch_names_tsv): raise RuntimeError(msg) if verbose: summary_str = [(ch, coord) for idx, (ch, coord) in enumerate(electrodes_dict.items()) if idx < 5] print("The read in electrodes file is: \n", summary_str) def _float_or_nan(val): if val == "n/a": return np.nan else: return float(val) # convert coordinates to float and create list of tuples electrodes_dict['x'] = [_float_or_nan(x) for x in electrodes_dict['x']] electrodes_dict['y'] = [_float_or_nan(x) for x in electrodes_dict['y']] electrodes_dict['z'] = [_float_or_nan(x) for x in electrodes_dict['z']] ch_names_raw = [x for i, x in enumerate(ch_names_raw) if electrodes_dict['x'][i] != "n/a"] ch_locs = np.c_[electrodes_dict['x'], electrodes_dict['y'], electrodes_dict['z']] # determine if there are problematic channels nan_chs = [] for ch_name, ch_coord in zip(ch_names_raw, ch_locs): if any(np.isnan(ch_coord)) and ch_name not in raw.info['bads']: nan_chs.append(ch_name) if len(nan_chs) > 0: warn("There are channels without locations " "(n/a) that are not marked as bad: {}".format(nan_chs)) # convert coordinates to meters ch_locs = _scale_coord_to_meters(ch_locs, coord_unit) # create mne.DigMontage ch_pos = dict(zip(ch_names_raw, ch_locs)) montage = mne.channels.make_dig_montage(ch_pos=ch_pos, coord_frame=coord_frame) raw.set_montage(montage) return raw
def _handle_events_reading(events_fname, raw): """Read associated events.tsv and populate raw. Handle onset, duration, and description of each event. """ logger.info('Reading events from {}.'.format(events_fname)) events_dict = _from_tsv(events_fname) # Get the descriptions of the events if 'trial_type' in events_dict: trial_type_col_name = 'trial_type' elif 'stim_type' in events_dict: # Backward-compat with old datasets. trial_type_col_name = 'stim_type' warn(f'The events file, {events_fname}, contains a "stim_type" ' f'column. This column should be renamed to "trial_type" for ' f'BIDS compatibility.') else: trial_type_col_name = None if trial_type_col_name is not None: # Drop events unrelated to a trial type events_dict = _drop(events_dict, 'n/a', trial_type_col_name) if 'value' in events_dict: # Check whether the `trial_type` <> `value` mapping is unique. trial_types = events_dict[trial_type_col_name] values = np.asarray(events_dict['value'], dtype=str) for trial_type in np.unique(trial_types): idx = np.where(trial_type == np.atleast_1d(trial_types))[0] matching_values = values[idx] if len(np.unique(matching_values)) > 1: # Event type descriptors are ambiguous; create hierarchical # event descriptors. logger.info( f'The event "{trial_type}" refers to multiple event ' f'values. Creating hierarchical event names.') for ii in idx: new_name = f'{trial_type}/{values[ii]}' logger.info(f' Renaming event: {trial_type} -> ' f'{new_name}') trial_types[ii] = new_name descriptions = np.asarray(trial_types, dtype=str) else: descriptions = np.asarray(events_dict[trial_type_col_name], dtype=str) elif 'value' in events_dict: # If we don't have a proper description of the events, perhaps we have # at least an event value? # Drop events unrelated to value events_dict = _drop(events_dict, 'n/a', 'value') descriptions = np.asarray(events_dict['value'], dtype=str) # Worst case, we go with 'n/a' for all events else: descriptions = np.array(['n/a'] * len(events_dict['onset']), dtype=str) # Deal with "n/a" strings before converting to float ons = [np.nan if on == 'n/a' else on for on in events_dict['onset']] dus = [0 if du == 'n/a' else du for du in events_dict['duration']] onsets = np.asarray(ons, dtype=float) durations = np.asarray(dus, dtype=float) # Keep only events where onset is known good_events_idx = ~np.isnan(onsets) onsets = onsets[good_events_idx] durations = durations[good_events_idx] descriptions = descriptions[good_events_idx] del good_events_idx # Add Events to raw as annotations annot_from_events = mne.Annotations(onset=onsets, duration=durations, description=descriptions, orig_time=None) raw.set_annotations(annot_from_events) return raw
help="The Freesurfer LUT.", required=False, default=fs_lut_fpath, ) parser.add_argument("-mask_img_fpath", required=False, default=mask_img_fpath.fpath) args = parser.parse_args() # Extract arguments from parser mri_xyzcoords_fpath = args.mri_xyzcoords_fpath output_electrodes_tsv_fpath = args.output_bids_electrodes_file fs_patient_dir = args.fs_patient_dir fs_lut_fpath = args.fs_lut_fpath mask_img_fpath = args.mask_img_fpath # load in the T1 MRI image and its affine print(f"Loading in mask file {mask_img_fpath}") t1_img = nb.load(mask_img_fpath) # load in the electrode coordinates print(f"Loading file {output_electrodes_tsv_fpath}") electrodes_tsv = _from_tsv(output_electrodes_tsv_fpath) # extract subject id from bids sidecar electrodes fname subject_id = (os.path.basename(output_electrodes_tsv_fpath).split("_") [0].split("sub-")[1]) # apply atlas apply_mask_on_electrodes(electrodes_tsv, t1_img)
def _write_electrodes_tsv(raw, fname, datatype, overwrite=False, verbose=True): """Create an electrodes.tsv file and save it. Parameters ---------- raw : mne.io.Raw The data as MNE-Python Raw object. fname : str Filename to save the electrodes.tsv to. datatype : str Type of the data recording. Can be ``meg``, ``eeg``, or ``ieeg``. overwrite : bool Defaults to False. Whether to overwrite the existing data in the file. If there is already data for the given `fname` and overwrite is False, an error will be raised. verbose : bool Set verbose output to true or false. """ # create list of channel coordinates and names x, y, z, names = list(), list(), list(), list() for ch in raw.info['chs']: if _check_ch_locs([ch]): x.append(ch['loc'][0]) y.append(ch['loc'][1]) z.append(ch['loc'][2]) else: x.append('n/a') y.append('n/a') z.append('n/a') names.append(ch['ch_name']) # create OrderedDict to write to tsv file if datatype == "ieeg": # XXX: size should be included in the future sizes = ['n/a'] * len(names) data = OrderedDict([ ('name', names), ('x', x), ('y', y), ('z', z), ('size', sizes), ]) elif datatype == 'eeg': data = OrderedDict([ ('name', names), ('x', x), ('y', y), ('z', z), ]) else: # pragma: no cover raise RuntimeError("datatype {} not supported.".format(datatype)) # Add impedance values if available, currently only BrainVision: # https://github.com/mne-tools/mne-python/pull/7974 if hasattr(raw, 'impedances'): data['impedance'] = _get_impedances(raw, names) # note that any coordsystem.json file shared within sessions # will be the same across all runs (currently). So # overwrite is set to True always # XXX: improve later when BIDS is updated # check that there already exists a coordsystem.json if Path(fname).exists() and not overwrite: electrodes_tsv = _from_tsv(fname) # cast values to str to make equality check work if any([ list(map(str, vals1)) != list(vals2) for vals1, vals2 in zip(data.values(), electrodes_tsv.values()) ]): raise RuntimeError( f'Trying to write electrodes.tsv, but it already ' f'exists at {fname} and the contents do not match. ' f'You must differentiate this electrodes.tsv file ' f'from the existing one, or set "overwrite" to True.') _write_tsv(fname, data, overwrite=True, verbose=verbose)
def test_fif(_bids_validate): """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', 'sample_audvis_trunc_raw.fif') event_id = { 'Auditory/Left': 1, 'Auditory/Right': 2, 'Visual/Left': 3, 'Visual/Right': 4, 'Smiley': 5, 'Button': 32 } events_fname = op.join(data_path, 'MEG', 'sample', 'sample_audvis_trunc_raw-eve.fif') 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) # Read the file back in to check that the data has come through cleanly. # Events and bad channel information was read through JSON sidecar files. with pytest.raises(TypeError, match="unexpected keyword argument 'foo'"): read_raw_bids(bids_basename + '_meg.fif', output_path, extra_params=dict(foo='bar')) raw2 = read_raw_bids(bids_basename + '_meg.fif', output_path, extra_params=dict(allow_maxshield=True)) assert set(raw.info['bads']) == set(raw2.info['bads']) events, _ = mne.events_from_annotations(raw2) events2 = mne.read_events(events_fname) events2 = events2[events2[:, 2] != 0] assert_array_equal(events2[:, 0], events[:, 0]) # check if write_raw_bids works when there is no stim channel raw.set_channel_types({ raw.ch_names[i]: 'misc' for i in mne.pick_types(raw.info, stim=True, meg=False) }) output_path = _TempDir() with pytest.warns(UserWarning, match='No events found or provided.'): write_raw_bids(raw, bids_basename, output_path, overwrite=False) _bids_validate(output_path) # try with eeg data only (conversion to bv) output_path = _TempDir() raw = mne.io.read_raw_fif(raw_fname) raw.load_data() raw2 = raw.pick_types(meg=False, eeg=True, stim=True, eog=True, ecg=True) raw2.save(op.join(output_path, 'test-raw.fif'), overwrite=True) raw2 = mne.io.Raw(op.join(output_path, 'test-raw.fif'), preload=False) events = mne.find_events(raw2) event_id = { 'auditory/left': 1, 'auditory/right': 2, 'visual/left': 3, 'visual/right': 4, 'smiley': 5, 'button': 32 } epochs = mne.Epochs(raw2, events, event_id=event_id, tmin=-0.2, tmax=0.5, preload=True) with pytest.warns(UserWarning, match='Converting data files to BrainVision format'): write_raw_bids(raw2, bids_basename, output_path, events_data=events_fname, event_id=event_id, verbose=True, overwrite=False) bids_dir = op.join(output_path, 'sub-%s' % subject_id, 'ses-%s' % session_id, 'eeg') for sidecar in [ 'channels.tsv', 'eeg.eeg', 'eeg.json', 'eeg.vhdr', 'eeg.vmrk', 'events.tsv' ]: assert op.isfile(op.join(bids_dir, bids_basename + '_' + sidecar)) raw2 = read_raw_bids(bids_basename + '_eeg.vhdr', output_path) os.remove(op.join(output_path, 'test-raw.fif')) events2 = mne.find_events(raw2) epochs2 = mne.Epochs(raw2, events2, event_id=event_id, tmin=-0.2, tmax=0.5, preload=True) assert_array_almost_equal(raw.get_data(), raw2.get_data()) assert_array_almost_equal(epochs.get_data(), epochs2.get_data(), decimal=4) _bids_validate(output_path) # write the same data but pretend it is empty room data: raw = mne.io.read_raw_fif(raw_fname) er_date = datetime.fromtimestamp( raw.info['meas_date'][0]).strftime('%Y%m%d') er_bids_basename = 'sub-emptyroom_ses-{0}_task-noise'.format(str(er_date)) write_raw_bids(raw, er_bids_basename, output_path, overwrite=False) assert op.exists( op.join(output_path, 'sub-emptyroom', 'ses-{0}'.format(er_date), 'meg', 'sub-emptyroom_ses-{0}_task-noise_meg.json'.format(er_date))) # test that an incorrect date raises an error. er_bids_basename_bad = 'sub-emptyroom_ses-19000101_task-noise' with pytest.raises(ValueError, match='Date provided'): write_raw_bids(raw, er_bids_basename_bad, output_path, overwrite=False) # give the raw object some fake participant data (potentially overwriting) raw = mne.io.read_raw_fif(raw_fname) raw.info['subject_info'] = { 'his_id': subject_id2, 'birthday': (1993, 1, 26), 'sex': 1 } write_raw_bids(raw, bids_basename, output_path, events_data=events_fname, event_id=event_id, overwrite=True) # assert age of participant is correct participants_tsv = op.join(output_path, 'participants.tsv') data = _from_tsv(participants_tsv) assert data['age'][data['participant_id'].index('sub-01')] == '9' # try and write preloaded data raw = mne.io.read_raw_fif(raw_fname, preload=True) with pytest.raises(ValueError, match='preloaded'): write_raw_bids(raw, bids_basename, output_path, events_data=events_fname, event_id=event_id, overwrite=False) # test anonymize raw = mne.io.read_raw_fif(raw_fname) raw.anonymize() data_path2 = _TempDir() raw_fname2 = op.join(data_path2, 'sample_audvis_raw.fif') raw.save(raw_fname2) bids_basename2 = bids_basename.replace(subject_id, subject_id2) raw = mne.io.read_raw_fif(raw_fname2) bids_output_path = 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(FileExistsError, match="already exists"): # noqa: F821 write_raw_bids(raw, bids_basename2, output_path, events_data=events_fname, event_id=event_id, overwrite=False) # now force the overwrite 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) _bids_validate(output_path) assert op.exists(op.join(output_path, 'participants.tsv')) # asserting that single fif files do not include the part key files = glob( op.join(bids_output_path, 'sub-' + subject_id2, 'ses-' + subject_id2, 'meg', '*.fif')) for ii, FILE in enumerate(files): assert 'part' not in FILE assert ii < 1 # test keyword mne-bids anonymize raw = mne.io.read_raw_fif(raw_fname) with pytest.raises(ValueError, match='`daysback` argument required'): write_raw_bids(raw, bids_basename, output_path, events_data=events_fname, event_id=event_id, anonymize=dict(), overwrite=True) output_path = _TempDir() raw = mne.io.read_raw_fif(raw_fname) with pytest.warns(UserWarning, match='daysback` is too small'): write_raw_bids(raw, bids_basename, output_path, events_data=events_fname, event_id=event_id, anonymize=dict(daysback=400), overwrite=False) output_path = _TempDir() raw = mne.io.read_raw_fif(raw_fname) with pytest.raises(ValueError, match='`daysback` exceeds maximum value'): write_raw_bids(raw, bids_basename, output_path, events_data=events_fname, event_id=event_id, anonymize=dict(daysback=40000), overwrite=False) output_path = _TempDir() raw = mne.io.read_raw_fif(raw_fname) write_raw_bids(raw, bids_basename, output_path, events_data=events_fname, event_id=event_id, anonymize=dict(daysback=30000, keep_his=True), overwrite=False) scans_tsv = make_bids_basename(subject=subject_id, session=session_id, suffix='scans.tsv', prefix=op.join(output_path, 'sub-01', 'ses-01')) data = _from_tsv(scans_tsv) assert datetime.strptime(data['acq_time'][0], '%Y-%m-%dT%H:%M:%S').year < 1925 _bids_validate(output_path) # check that split files have part key raw = mne.io.read_raw_fif(raw_fname) data_path3 = _TempDir() raw_fname3 = op.join(data_path3, 'sample_audvis_raw.fif') raw.save(raw_fname3, buffer_size_sec=1.0, split_size='10MB', split_naming='neuromag', overwrite=True) raw = mne.io.read_raw_fif(raw_fname3) subject_id3 = '03' bids_basename3 = bids_basename.replace(subject_id, subject_id3) bids_output_path = write_raw_bids(raw, bids_basename3, output_path, overwrite=False) files = glob( op.join(bids_output_path, 'sub-' + subject_id3, 'ses-' + subject_id3, 'meg', '*.fif')) for FILE in files: assert 'part' in FILE # test unknown extention raw = mne.io.read_raw_fif(raw_fname) raw._filenames = (raw.filenames[0].replace('.fif', '.foo'), ) with pytest.raises(ValueError, match='Unrecognized file format'): write_raw_bids(raw, bids_basename, output_path)
def test_kit(_bids_validate): """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') events_fname = op.join(data_path, 'test-eve.txt') hpi_fname = op.join(data_path, 'test_mrk.sqd') hpi_pre_fname = op.join(data_path, 'test_mrk_pre.sqd') hpi_post_fname = op.join(data_path, 'test_mrk_post.sqd') electrode_fname = op.join(data_path, 'test_elp.txt') headshape_fname = op.join(data_path, 'test_hsp.txt') event_id = dict(cond=1) kit_bids_basename = bids_basename.replace('_acq-01', '') raw = mne.io.read_raw_kit(raw_fname, mrk=hpi_fname, elp=electrode_fname, hsp=headshape_fname) write_raw_bids(raw, kit_bids_basename, output_path, events_data=events_fname, event_id=event_id, overwrite=False) _bids_validate(output_path) assert op.exists(op.join(output_path, 'participants.tsv')) read_raw_bids(kit_bids_basename + '_meg.sqd', output_path) # test anonymize output_path = _test_anonymize(raw, kit_bids_basename, events_fname, event_id) _bids_validate(output_path) # ensure the channels file has no STI 014 channel: channels_tsv = make_bids_basename(subject=subject_id, session=session_id, task=task, run=run, suffix='channels.tsv', prefix=op.join(output_path, 'sub-01', 'ses-01', 'meg')) data = _from_tsv(channels_tsv) assert 'STI 014' not in data['name'] # ensure the marker file is produced in the right place marker_fname = make_bids_basename(subject=subject_id, session=session_id, task=task, run=run, suffix='markers.sqd', prefix=op.join(output_path, 'sub-01', 'ses-01', 'meg')) assert op.exists(marker_fname) # test attempts at writing invalid event data event_data = np.loadtxt(events_fname) # make the data the wrong number of dimensions event_data_3d = np.atleast_3d(event_data) other_output_path = _TempDir() with pytest.raises(ValueError, match='two dimensions'): write_raw_bids(raw, bids_basename, other_output_path, events_data=event_data_3d, event_id=event_id, overwrite=True) # remove 3rd column event_data = event_data[:, :2] with pytest.raises(ValueError, match='second dimension'): write_raw_bids(raw, bids_basename, other_output_path, events_data=event_data, event_id=event_id, overwrite=True) # test correct naming of marker files raw = mne.io.read_raw_kit(raw_fname, mrk=[hpi_pre_fname, hpi_post_fname], elp=electrode_fname, hsp=headshape_fname) write_raw_bids(raw, kit_bids_basename.replace('sub-01', 'sub-%s' % subject_id2), output_path, events_data=events_fname, event_id=event_id, overwrite=False) _bids_validate(output_path) # ensure the marker files are renamed correctly marker_fname = make_bids_basename(subject=subject_id2, session=session_id, task=task, run=run, suffix='markers.sqd', acquisition='pre', prefix=os.path.join( output_path, 'sub-02', 'ses-01', 'meg')) info = get_kit_info(marker_fname, False)[0] assert info['meas_date'] == get_kit_info(hpi_pre_fname, False)[0]['meas_date'] marker_fname = marker_fname.replace('acq-pre', 'acq-post') info = get_kit_info(marker_fname, False)[0] assert info['meas_date'] == get_kit_info(hpi_post_fname, False)[0]['meas_date'] # check that providing markers in the wrong order raises an error raw = mne.io.read_raw_kit(raw_fname, mrk=[hpi_post_fname, hpi_pre_fname], elp=electrode_fname, hsp=headshape_fname) with pytest.raises(ValueError, match='Markers'): write_raw_bids(raw, kit_bids_basename.replace('sub-01', 'sub-%s' % subject_id2), output_path, events_data=events_fname, event_id=event_id, overwrite=True)
def _handle_channels_reading(channels_fname, bids_fname, raw): """Read associated channels.tsv and populate raw. Updates status (bad) and types of channels. """ logger.info('Reading channel info from {}.'.format(channels_fname)) channels_dict = _from_tsv(channels_fname) # First, make sure that ordering of names in channels.tsv matches the # ordering of names in the raw data. The "name" column is mandatory in BIDS ch_names_raw = list(raw.ch_names) ch_names_tsv = channels_dict['name'] if ch_names_raw != ch_names_tsv: msg = ('Channels do not correspond between raw data and the ' 'channels.tsv file. For MNE-BIDS, the channel names in the ' 'tsv MUST be equal and in the same order as the channels in ' 'the raw data.\n\n' '{} channels in tsv file: "{}"\n\n --> {}\n\n' '{} channels in raw file: "{}"\n\n --> {}\n\n'.format( len(ch_names_tsv), channels_fname, ch_names_tsv, len(ch_names_raw), bids_fname, ch_names_raw)) # XXX: this could be due to MNE inserting a 'STI 014' channel as the # last channel: In that case, we can work. --> Can be removed soon, # because MNE will stop the synthesis of stim channels in the near # future if not (ch_names_raw[-1] == 'STI 014' and ch_names_raw[:-1] == ch_names_tsv): raise RuntimeError(msg) # Now we can do some work. # The "type" column is mandatory in BIDS. We can use it to set channel # types in the raw data using a mapping between channel types channel_type_dict = dict() # Get the best mapping we currently have from BIDS to MNE nomenclature bids_to_mne_ch_types = _get_ch_type_mapping(fro='bids', to='mne') ch_types_json = channels_dict['type'] for ch_name, ch_type in zip(ch_names_tsv, ch_types_json): # Try to map from BIDS nomenclature to MNE, leave channel type # untouched if we are uncertain updated_ch_type = bids_to_mne_ch_types.get(ch_type, None) if updated_ch_type is None: # XXX Try again with uppercase spelling – this should be removed # XXX once https://github.com/bids-standard/bids-validator/issues/1018 # noqa:E501 # XXX has been resolved. # XXX x-ref https://github.com/mne-tools/mne-bids/issues/481 updated_ch_type = bids_to_mne_ch_types.get(ch_type.upper(), None) if updated_ch_type is not None: msg = ('The BIDS dataset contains channel types in lowercase ' 'spelling. This violates the BIDS specification and ' 'will raise an error in the future.') warn(msg) if updated_ch_type is not None: channel_type_dict[ch_name] = updated_ch_type # Set the channel types in the raw data according to channels.tsv raw.set_channel_types(channel_type_dict) # Check whether there is the optional "status" column from which to infer # good and bad channels if 'status' in channels_dict: # find bads from channels.tsv bads_from_tsv = _get_bads_from_tsv_data(channels_dict) if raw.info['bads'] and set(bads_from_tsv) != set(raw.info['bads']): warn(f'Encountered conflicting information on channel status ' f'between {op.basename(channels_fname)} and the associated ' f'raw data file.\n' f'Channels marked as bad in ' f'{op.basename(channels_fname)}: {bads_from_tsv}\n' f'Channels marked as bad in ' f'raw.info["bads"]: {raw.info["bads"]}\n' f'Setting list of bad channels to: {bads_from_tsv}') raw.info['bads'] = bads_from_tsv elif raw.info['bads']: # We do have info['bads'], but no `status` in channels.tsv logger.info(f'No "status" column found in ' f'{op.basename(channels_fname)}; using list of bad ' f'channels found in raw.info["bads"]: {raw.info["bads"]}') return raw