def test_find_matching_sidecar(return_bids_test_dir): """Test finding a sidecar file from a BIDS dir.""" bids_root = return_bids_test_dir bids_fpath = bids_path.copy().update(root=bids_root) # Now find a sidecar sidecar_fname = _find_matching_sidecar(bids_fpath, suffix='coordsystem', extension='.json') expected_file = op.join('sub-01', 'ses-01', 'meg', 'sub-01_ses-01_coordsystem.json') assert sidecar_fname.endswith(expected_file) # Find multiple sidecars, tied in score, triggering an error with pytest.raises(RuntimeError, match='Expected to find a single'): open(sidecar_fname.replace('coordsystem.json', '2coordsystem.json'), 'w').close() print_dir_tree(bids_root) _find_matching_sidecar(bids_fpath, suffix='coordsystem', extension='.json') # Find nothing and raise. with pytest.raises(RuntimeError, match='Did not find any'): fname = _find_matching_sidecar(bids_fpath, suffix='foo', extension='.bogus') # Find nothing and receive None and a warning. on_error = 'warn' with pytest.warns(RuntimeWarning, match='Did not find any'): fname = _find_matching_sidecar(bids_fpath, suffix='foo', extension='.bogus', on_error=on_error) assert fname is None # Find nothing and receive None. on_error = 'ignore' fname = _find_matching_sidecar(bids_fpath, suffix='foo', extension='.bogus', on_error=on_error) assert fname is None # Invalid on_error. on_error = 'hello' with pytest.raises(ValueError, match='Acceptable values for on_error are'): _find_matching_sidecar(bids_fpath, suffix='coordsystem', extension='.json', on_error=on_error)
def test_handle_info_reading(): """Test reading information from a BIDS sidecar.json file.""" bids_root = _TempDir() # read in USA dataset, so it should find 50 Hz raw = _read_raw_fif(raw_fname) # write copy of raw with line freq of 60 # bids basename and fname bids_path = BIDSPath(subject='01', session='01', task='audiovisual', run='01', root=bids_root) suffix = "meg" bids_fname = bids_path.copy().update(suffix=suffix, extension='.fif') write_raw_bids(raw, bids_path, overwrite=True) # find sidecar JSON fname bids_fname.update(datatype=suffix) sidecar_fname = _find_matching_sidecar(bids_fname, suffix=suffix, extension='.json') # assert that we get the same line frequency set raw = read_raw_bids(bids_path=bids_path) assert raw.info['line_freq'] == 60 # 2. if line frequency is not set in raw file, then ValueError raw.info['line_freq'] = None with pytest.raises(ValueError, match="PowerLineFrequency .* required"): write_raw_bids(raw, bids_path, overwrite=True) # make a copy of the sidecar in "derivatives/" # to check that we make sure we always get the right sidecar # in addition, it should not break the sidecar reading # in `read_raw_bids` deriv_dir = op.join(bids_root, "derivatives") sidecar_copy = op.join(deriv_dir, op.basename(sidecar_fname)) os.mkdir(deriv_dir) with open(sidecar_fname, "r", encoding='utf-8') as fin: sidecar_json = json.load(fin) sidecar_json["PowerLineFrequency"] = 45 _write_json(sidecar_copy, sidecar_json) raw = read_raw_bids(bids_path=bids_path) assert raw.info['line_freq'] == 60 # 3. assert that we get an error when sidecar json doesn't match _update_sidecar(sidecar_fname, "PowerLineFrequency", 55) with pytest.raises(ValueError, match="Line frequency in sidecar json"): raw = read_raw_bids(bids_path=bids_path) assert raw.info['line_freq'] == 55
def get_head_mri_trans(bids_path, extra_params=None): """Produce transformation matrix from MEG and MRI landmark points. Will attempt to read the landmarks of Nasion, LPA, and RPA from the sidecar files of (i) the MEG and (ii) the T1 weighted MRI data. The two sets of points will then be used to calculate a transformation matrix from head coordinates to MRI coordinates. Parameters ---------- bids_path : mne_bids.BIDSPath The path of the recording for which to retrieve the transformation. The :class:`mne_bids.BIDSPath` instance passed here **must** have the ``.root`` attribute set. extra_params : None | dict Extra parameters to be passed to MNE read_raw_* functions when reading the lankmarks from the MEG file. If a dict, for example: ``extra_params=dict(allow_maxshield=True)``. Returns ------- trans : mne.transforms.Transform The data transformation matrix from head to MRI coordinates """ if not has_nibabel(): # pragma: no cover raise ImportError('This function requires nibabel.') import nibabel as nib if not isinstance(bids_path, BIDSPath): raise RuntimeError('"bids_path" must be a BIDSPath object. Please ' 'instantiate using mne_bids.BIDSPath().') # check root available bids_path = bids_path.copy() bids_root = bids_path.root if bids_root is None: raise ValueError('The root of the "bids_path" must be set. ' 'Please use `bids_path.update(root="<root>")` ' 'to set the root of the BIDS folder to read.') # only get this for MEG data bids_path.update(datatype='meg') # Get the sidecar file for MRI landmarks bids_fname = bids_path.update(suffix='meg', root=bids_root) t1w_json_path = _find_matching_sidecar(bids_fname, suffix='T1w', extension='.json') # Get MRI landmarks from the JSON sidecar with open(t1w_json_path, 'r', encoding='utf-8-sig') as f: t1w_json = json.load(f) mri_coords_dict = t1w_json.get('AnatomicalLandmarkCoordinates', dict()) mri_landmarks = np.asarray( (mri_coords_dict.get('LPA', np.nan), mri_coords_dict.get('NAS', np.nan), mri_coords_dict.get('RPA', np.nan))) if np.isnan(mri_landmarks).any(): raise RuntimeError( 'Could not parse T1w sidecar file: "{}"\n\n' 'The sidecar file MUST contain a key ' '"AnatomicalLandmarkCoordinates" pointing to a ' 'dict with keys "LPA", "NAS", "RPA". ' 'Yet, the following structure was found:\n\n"{}"'.format( t1w_json_path, t1w_json)) # The MRI landmarks are in "voxels". We need to convert the to the # neuromag RAS coordinate system in order to compare the with MEG landmarks # see also: `mne_bids.write.write_anat` t1w_path = t1w_json_path.replace('.json', '.nii') if not op.exists(t1w_path): t1w_path += '.gz' # perhaps it is .nii.gz? ... else raise an error if not op.exists(t1w_path): raise RuntimeError( 'Could not find the T1 weighted MRI associated ' 'with "{}". Tried: "{}" but it does not exist.'.format( t1w_json_path, t1w_path)) t1_nifti = nib.load(t1w_path) # Convert to MGH format to access vox2ras method t1_mgh = nib.MGHImage(t1_nifti.dataobj, t1_nifti.affine) # now extract transformation matrix and put back to RAS coordinates of MRI vox2ras_tkr = t1_mgh.header.get_vox2ras_tkr() mri_landmarks = apply_trans(vox2ras_tkr, mri_landmarks) mri_landmarks = mri_landmarks * 1e-3 # Get MEG landmarks from the raw file _, ext = _parse_ext(bids_fname) if extra_params is None: extra_params = dict() if ext == '.fif': extra_params = dict(allow_maxshield=True) raw = read_raw_bids(bids_path=bids_path, extra_params=extra_params) meg_coords_dict = _extract_landmarks(raw.info['dig']) meg_landmarks = np.asarray((meg_coords_dict['LPA'], meg_coords_dict['NAS'], meg_coords_dict['RPA'])) # Given the two sets of points, fit the transform trans_fitted = fit_matched_points(src_pts=meg_landmarks, tgt_pts=mri_landmarks) trans = mne.transforms.Transform(fro='head', to='mri', trans=trans_fitted) return trans
def read_raw_bids(bids_path, extra_params=None, verbose=True): """Read BIDS compatible data. Will attempt to read associated events.tsv and channels.tsv files to populate the returned raw object with raw.annotations and raw.info['bads']. Parameters ---------- bids_path : mne_bids.BIDSPath The file to read. The :class:`mne_bids.BIDSPath` instance passed here **must** have the ``.root`` attribute set. The ``.datatype`` attribute **may** be set. If ``.datatype`` is not set and only one data type (e.g., only EEG or MEG data) is present in the dataset, it will be selected automatically. extra_params : None | dict Extra parameters to be passed to MNE read_raw_* functions. If a dict, for example: ``extra_params=dict(allow_maxshield=True)``. Note that the ``exclude`` parameter, which is supported by some MNE-Python readers, is not supported; instead, you need to subset your channels **after** reading. verbose : bool The verbosity level. Returns ------- raw : mne.io.Raw The data as MNE-Python Raw object. Raises ------ RuntimeError If multiple recording data types are present in the dataset, but ``datatype=None``. RuntimeError If more than one data files exist for the specified recording. RuntimeError If no data file in a supported format can be located. ValueError If the specified ``datatype`` cannot be found in the dataset. """ if not isinstance(bids_path, BIDSPath): raise RuntimeError('"bids_path" must be a BIDSPath object. Please ' 'instantiate using mne_bids.BIDSPath().') bids_path = bids_path.copy() sub = bids_path.subject ses = bids_path.session bids_root = bids_path.root datatype = bids_path.datatype suffix = bids_path.suffix # check root available if bids_root is None: raise ValueError('The root of the "bids_path" must be set. ' 'Please use `bids_path.update(root="<root>")` ' 'to set the root of the BIDS folder to read.') # infer the datatype and suffix if they are not present in the BIDSPath if datatype is None: datatype = _infer_datatype(root=bids_root, sub=sub, ses=ses) bids_path.update(datatype=datatype) if suffix is None: bids_path.update(suffix=datatype) data_dir = bids_path.directory bids_fname = bids_path.fpath.name if op.splitext(bids_fname)[1] == '.pdf': bids_raw_folder = op.join(data_dir, f'{bids_path.basename}') bids_fpath = glob.glob(op.join(bids_raw_folder, 'c,rf*'))[0] config = op.join(bids_raw_folder, 'config') else: bids_fpath = op.join(data_dir, bids_fname) config = None if extra_params is None: extra_params = dict() elif 'exclude' in extra_params: del extra_params['exclude'] logger.info('"exclude" parameter is not supported by read_raw_bids') raw = _read_raw(bids_fpath, electrode=None, hsp=None, hpi=None, config=config, verbose=None, **extra_params) # Try to find an associated events.tsv to get information about the # events in the recorded data events_fname = _find_matching_sidecar(bids_path, suffix='events', extension='.tsv', on_error='warn') if events_fname is not None: raw = _handle_events_reading(events_fname, raw) # Try to find an associated channels.tsv to get information about the # status and type of present channels channels_fname = _find_matching_sidecar(bids_path, suffix='channels', extension='.tsv', on_error='warn') if channels_fname is not None: raw = _handle_channels_reading(channels_fname, raw) # Try to find an associated electrodes.tsv and coordsystem.json # to get information about the status and type of present channels on_error = 'warn' if suffix == 'ieeg' else 'ignore' electrodes_fname = _find_matching_sidecar(bids_path, suffix='electrodes', extension='.tsv', on_error=on_error) coordsystem_fname = _find_matching_sidecar(bids_path, suffix='coordsystem', extension='.json', on_error=on_error) if electrodes_fname is not None: if coordsystem_fname is None: raise RuntimeError(f"BIDS mandates that the coordsystem.json " f"should exist if electrodes.tsv does. " f"Please create coordsystem.json for" f"{bids_path.basename}") if datatype in ['meg', 'eeg', 'ieeg']: raw = _read_dig_bids(electrodes_fname, coordsystem_fname, raw, datatype, verbose) # Try to find an associated sidecar .json to get information about the # recording snapshot sidecar_fname = _find_matching_sidecar(bids_path, suffix=datatype, extension='.json', on_error='warn') if sidecar_fname is not None: raw = _handle_info_reading(sidecar_fname, raw, verbose=verbose) # read in associated scans filename scans_fname = BIDSPath(subject=bids_path.subject, session=bids_path.session, suffix='scans', extension='.tsv', root=bids_path.root).fpath if scans_fname.exists(): raw = _handle_scans_reading(scans_fname, raw, bids_path, verbose=verbose) # read in associated subject info from participants.tsv participants_tsv_fpath = op.join(bids_root, 'participants.tsv') subject = f"sub-{bids_path.subject}" if op.exists(participants_tsv_fpath): raw = _handle_participants_reading(participants_tsv_fpath, raw, subject, verbose=verbose) else: warn("Participants file not found for {}... Not reading " "in any particpants.tsv data.".format(bids_fname)) return raw
def get_head_mri_trans(bids_path, extra_params=None, t1_bids_path=None): """Produce transformation matrix from MEG and MRI landmark points. Will attempt to read the landmarks of Nasion, LPA, and RPA from the sidecar files of (i) the MEG and (ii) the T1-weighted MRI data. The two sets of points will then be used to calculate a transformation matrix from head coordinates to MRI coordinates. .. note:: The MEG and MRI data need **not** necessarily be stored in the same session or even in the same BIDS dataset. See the ``t1_bids_path`` parameter for details. Parameters ---------- bids_path : mne_bids.BIDSPath The path of the MEG recording. extra_params : None | dict Extra parameters to be passed to :func:`mne.io.read_raw` when reading the MEG file. t1_bids_path : mne_bids.BIDSPath | None If ``None`` (default), will try to discover the T1-weighted MRI file based on the name and location of the MEG recording specified via the ``bids_path`` parameter. Alternatively, you explicitly specify which T1-weighted MRI scan to use for extraction of MRI landmarks. To do that, pass a :class:`mne_bids.BIDSPath` pointing to the scan. Use this parameter e.g. if the T1 scan was recorded during a different session than the MEG. It is even possible to point to a T1 image stored in an entirely different BIDS dataset than the MEG data. .. versionadded:: 0.8 Returns ------- trans : mne.transforms.Transform The data transformation matrix from head to MRI coordinates. """ if not has_nibabel(): # pragma: no cover raise ImportError('This function requires nibabel.') import nibabel as nib if not isinstance(bids_path, BIDSPath): raise RuntimeError('"bids_path" must be a BIDSPath object. Please ' 'instantiate using mne_bids.BIDSPath().') # check root available meg_bids_path = bids_path.copy() del bids_path if meg_bids_path.root is None: raise ValueError('The root of the "bids_path" must be set. ' 'Please use `bids_path.update(root="<root>")` ' 'to set the root of the BIDS folder to read.') # only get this for MEG data meg_bids_path.update(datatype='meg', suffix='meg') # Get the sidecar file for MRI landmarks if t1_bids_path is None: t1w_json_path = _find_matching_sidecar(meg_bids_path, suffix='T1w', extension='.json') else: t1_bids_path = t1_bids_path.copy().update(suffix='T1w', datatype='anat') t1w_json_path = _find_matching_sidecar(t1_bids_path, suffix='T1w', extension='.json') # Get MRI landmarks from the JSON sidecar with open(t1w_json_path, 'r', encoding='utf-8') as f: t1w_json = json.load(f) mri_coords_dict = t1w_json.get('AnatomicalLandmarkCoordinates', dict()) # landmarks array: rows: [LPA, NAS, RPA]; columns: [x, y, z] mri_landmarks = np.full((3, 3), np.nan) for landmark_name, coords in mri_coords_dict.items(): if landmark_name.upper() == 'LPA': mri_landmarks[0, :] = coords elif landmark_name.upper() == 'RPA': mri_landmarks[2, :] = coords elif (landmark_name.upper() == 'NAS' or landmark_name.lower() == 'nasion'): mri_landmarks[1, :] = coords else: continue if np.isnan(mri_landmarks).any(): raise RuntimeError( f'Could not extract fiducial points from T1w sidecar file: ' f'{t1w_json_path}\n\n' f'The sidecar file SHOULD contain a key ' f'"AnatomicalLandmarkCoordinates" pointing to an ' f'object with the keys "LPA", "NAS", and "RPA". ' f'Yet, the following structure was found:\n\n' f'{mri_coords_dict}') # The MRI landmarks are in "voxels". We need to convert the to the # neuromag RAS coordinate system in order to compare the with MEG landmarks # see also: `mne_bids.write.write_anat` t1w_path = t1w_json_path.replace('.json', '.nii') if not op.exists(t1w_path): t1w_path += '.gz' # perhaps it is .nii.gz? ... else raise an error if not op.exists(t1w_path): raise RuntimeError( 'Could not find the T1 weighted MRI associated ' 'with "{}". Tried: "{}" but it does not exist.'.format( t1w_json_path, t1w_path)) t1_nifti = nib.load(t1w_path) # Convert to MGH format to access vox2ras method t1_mgh = nib.MGHImage(t1_nifti.dataobj, t1_nifti.affine) # now extract transformation matrix and put back to RAS coordinates of MRI vox2ras_tkr = t1_mgh.header.get_vox2ras_tkr() mri_landmarks = apply_trans(vox2ras_tkr, mri_landmarks) mri_landmarks = mri_landmarks * 1e-3 # Get MEG landmarks from the raw file _, ext = _parse_ext(meg_bids_path) if extra_params is None: extra_params = dict() if ext == '.fif': extra_params['allow_maxshield'] = True raw = read_raw_bids(bids_path=meg_bids_path, extra_params=extra_params) meg_coords_dict = _extract_landmarks(raw.info['dig']) meg_landmarks = np.asarray((meg_coords_dict['LPA'], meg_coords_dict['NAS'], meg_coords_dict['RPA'])) # Given the two sets of points, fit the transform trans_fitted = fit_matched_points(src_pts=meg_landmarks, tgt_pts=mri_landmarks) trans = mne.transforms.Transform(fro='head', to='mri', trans=trans_fitted) return trans
def _summarize_channels_tsv(root, scans_fpaths, verbose=True): """Summarize channels.tsv data in BIDS root directory. Currently, summarizes all REQUIRED components of channels data, and some RECOMMENDED and OPTIONAL components. Parameters ---------- root : str | pathlib.Path The path of the root of the BIDS compatible folder. scans_fpaths : list A list of all *_scans.tsv files in ``root``. The summary will occur for all scans listed in the *_scans.tsv files. verbose : bool Returns ------- template_dict : dict A dictionary of values for various template strings. """ root = Path(root) # keep track of channel type, status ch_status_count = {'bad': [], 'good': []} ch_count = [] # loop through each scan for scan_fpath in scans_fpaths: # load in the scans.tsv file # and read metadata for each scan scans_tsv = _from_tsv(scan_fpath) scans = scans_tsv['filename'] for scan in scans: # summarize metadata of recordings bids_path, _ = _parse_ext(scan) datatype = op.dirname(scan) if datatype not in ['meg', 'eeg', 'ieeg']: continue # convert to BIDS Path params = get_entities_from_fname(bids_path) bids_path = BIDSPath(root=root, **params) # XXX: improve to allow emptyroom if bids_path.subject == 'emptyroom': continue channels_fname = _find_matching_sidecar(bids_path=bids_path, suffix='channels', extension='.tsv') # summarize channels.tsv channels_tsv = _from_tsv(channels_fname) for status in ch_status_count.keys(): ch_status = [ ch for ch in channels_tsv['status'] if ch == status ] ch_status_count[status].append(len(ch_status)) ch_count.append(len(channels_tsv['name'])) # create summary template strings for status template_dict = { 'mean_chs': np.mean(ch_count), 'std_chs': np.std(ch_count), 'mean_good_chs': np.mean(ch_status_count['good']), 'std_good_chs': np.std(ch_status_count['good']), 'mean_bad_chs': np.mean(ch_status_count['bad']), 'std_bad_chs': np.std(ch_status_count['bad']), } for key, val in template_dict.items(): template_dict[key] = round(val, 2) return template_dict
def _summarize_sidecar_json(root, scans_fpaths, verbose=True): """Summarize scans in BIDS root directory. Parameters ---------- root : str | pathlib.Path The path of the root of the BIDS compatible folder. scans_fpaths : list A list of all *_scans.tsv files in ``root``. The summary will occur for all scans listed in the *_scans.tsv files. verbose : bool Set verbose output to true or false. Returns ------- template_dict : dict A dictionary of values for various template strings. """ n_scans = 0 powerlinefreqs, sfreqs = set(), set() manufacturers = set() length_recordings = [] # loop through each scan for scan_fpath in scans_fpaths: # load in the scans.tsv file # and read metadata for each scan scans_tsv = _from_tsv(scan_fpath) scans = scans_tsv['filename'] for scan in scans: # summarize metadata of recordings bids_path, ext = _parse_ext(scan) datatype = op.dirname(scan) if datatype not in ALLOWED_DATATYPES: continue n_scans += 1 # convert to BIDS Path params = get_entities_from_fname(bids_path) bids_path = BIDSPath(root=root, **params) # XXX: improve to allow emptyroom if bids_path.subject == 'emptyroom': continue sidecar_fname = _find_matching_sidecar(bids_path=bids_path, suffix=datatype, extension='.json') with open(sidecar_fname, 'r', encoding='utf-8-sig') as fin: sidecar_json = json.load(fin) # aggregate metadata from each scan # REQUIRED kwargs sfreq = sidecar_json['SamplingFrequency'] powerlinefreq = str(sidecar_json['PowerLineFrequency']) software_filters = sidecar_json.get('SoftwareFilters') if not software_filters: software_filters = 'n/a' # RECOMMENDED kwargs manufacturer = sidecar_json.get('Manufacturer', 'n/a') record_duration = sidecar_json.get('RecordingDuration', 'n/a') sfreqs.add(str(np.round(sfreq, 2))) powerlinefreqs.add(str(powerlinefreq)) if manufacturer != 'n/a': manufacturers.add(manufacturer) length_recordings.append(record_duration) # XXX: length summary is only allowed, if no 'n/a' was found if any([dur == 'n/a' for dur in length_recordings]): length_recordings = None template_dict = { 'n_scans': n_scans, 'manufacturer': list(manufacturers), 'sfreq': sfreqs, 'powerlinefreq': powerlinefreqs, 'software_filters': software_filters, 'length_recordings': length_recordings, } return template_dict
def test_handle_ieeg_coords_reading(bids_path): """Test reading iEEG coordinates from BIDS files.""" bids_root = _TempDir() data_path = op.join(testing.data_path(), 'EDF') raw_fname = op.join(data_path, 'test_reduced.edf') bids_fname = bids_path.copy().update(datatype='ieeg', suffix='ieeg', extension='.edf', root=bids_root) raw = _read_raw_edf(raw_fname) # ensure we are writing 'ecog'/'ieeg' data raw.set_channel_types({ch: 'ecog' for ch in raw.ch_names}) # coordinate frames in mne-python should all map correctly # 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)) coordinate_frames = ['mri', 'ras'] for coord_frame in coordinate_frames: # XXX: mne-bids doesn't support multiple electrodes.tsv files sh.rmtree(bids_root) montage = mne.channels.make_dig_montage(ch_pos=ch_pos, coord_frame=coord_frame) raw.set_montage(montage) write_raw_bids(raw, bids_fname, overwrite=True, verbose=False) # read in raw file w/ updated coordinate frame # and make sure all digpoints are correct coordinate frames raw_test = read_raw_bids(bids_path=bids_fname, verbose=False) coord_frame_int = MNE_STR_TO_FRAME[coord_frame] for digpoint in raw_test.info['dig']: assert digpoint['coord_frame'] == coord_frame_int # start w/ new bids root sh.rmtree(bids_root) write_raw_bids(raw, bids_fname, overwrite=True, verbose=False) # obtain the sensor positions and assert ch_coords are same raw_test = read_raw_bids(bids_path=bids_fname, verbose=False) 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']) # read in the data and assert montage is the same # regardless of 'm', 'cm', 'mm', or 'pixel' scalings = {'m': 1, 'cm': 100, 'mm': 1000} bids_fname.update(root=bids_root) coordsystem_fname = _find_matching_sidecar(bids_fname, suffix='coordsystem', extension='.json') electrodes_fname = _find_matching_sidecar(bids_fname, suffix='electrodes', extension='.tsv') orig_electrodes_dict = _from_tsv(electrodes_fname, [str, float, float, float, str]) # not BIDS specified should not be read coord_unit = 'km' scaling = 0.001 _update_sidecar(coordsystem_fname, 'iEEGCoordinateUnits', coord_unit) electrodes_dict = _from_tsv(electrodes_fname, [str, float, float, float, str]) for axis in ['x', 'y', 'z']: electrodes_dict[axis] = \ np.multiply(orig_electrodes_dict[axis], scaling) _to_tsv(electrodes_dict, electrodes_fname) with pytest.warns(RuntimeWarning, match='Coordinate unit is not ' 'an accepted BIDS unit'): raw_test = read_raw_bids(bids_path=bids_fname, verbose=False) # correct BIDS units should scale to meters properly for coord_unit, scaling in scalings.items(): # update coordinate SI units _update_sidecar(coordsystem_fname, 'iEEGCoordinateUnits', coord_unit) electrodes_dict = _from_tsv(electrodes_fname, [str, float, float, float, str]) for axis in ['x', 'y', 'z']: electrodes_dict[axis] = \ np.multiply(orig_electrodes_dict[axis], scaling) _to_tsv(electrodes_dict, electrodes_fname) # read in raw file w/ updated montage raw_test = read_raw_bids(bids_path=bids_fname, verbose=False) # obtain the sensor positions and make sure they're the same assert_dig_allclose(raw.info, raw_test.info) # XXX: Improve by changing names to 'unknown' coordframe (needs mne PR) # check that coordinate systems other coordinate systems should be named # in the file and not the CoordinateSystem, which is reserved for keywords coordinate_frames = ['lia', 'ria', 'lip', 'rip', 'las'] for coord_frame in coordinate_frames: # update coordinate units _update_sidecar(coordsystem_fname, 'iEEGCoordinateSystem', coord_frame) # read in raw file w/ updated coordinate frame # and make sure all digpoints are MRI coordinate frame with pytest.warns(RuntimeWarning, match="iEEG Coordinate frame is " "not accepted BIDS keyword"): raw_test = read_raw_bids(bids_path=bids_fname, verbose=False) assert raw_test.info['dig'] is None # ACPC should be read in as RAS for iEEG _update_sidecar(coordsystem_fname, 'iEEGCoordinateSystem', 'acpc') raw_test = read_raw_bids(bids_path=bids_fname, verbose=False) coord_frame_int = MNE_STR_TO_FRAME['ras'] for digpoint in raw_test.info['dig']: assert digpoint['coord_frame'] == coord_frame_int # if we delete the coordsystem.json file, an error will be raised os.remove(coordsystem_fname) with pytest.raises(RuntimeError, match='BIDS mandates that ' 'the coordsystem.json'): raw = read_raw_bids(bids_path=bids_fname, verbose=False) # test error message if electrodes don't match bids_path.update(root=bids_root) write_raw_bids(raw, bids_path, overwrite=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_path=bids_fname, verbose=False) # make sure montage is set if there are coordinates w/ 'n/a' raw.info['bads'] = [] write_raw_bids(raw, bids_path, overwrite=True, verbose=False) electrodes_dict = _from_tsv(electrodes_fname) for axis in ['x', 'y', 'z']: electrodes_dict[axis][0] = 'n/a' electrodes_dict[axis][3] = 'n/a' _to_tsv(electrodes_dict, electrodes_fname) # test if montage is correctly set via mne-bids # electrode coordinates should be nan # when coordinate is 'n/a' nan_chs = [electrodes_dict['name'][i] for i in [0, 3]] with pytest.warns(RuntimeWarning, match='There are channels ' 'without locations'): raw = read_raw_bids(bids_path=bids_fname, verbose=False) for idx, ch in enumerate(raw.info['chs']): if ch['ch_name'] in nan_chs: assert all(np.isnan(ch['loc'][:3])) else: assert not any(np.isnan(ch['loc'][:3])) assert ch['ch_name'] not in raw.info['bads']
def test_handle_eeg_coords_reading(): """Test reading iEEG coordinates from BIDS files.""" bids_root = _TempDir() bids_path = BIDSPath(subject=subject_id, session=session_id, run=run, acquisition=acq, task=task, root=bids_root) data_path = op.join(testing.data_path(), 'EDF') raw_fname = op.join(data_path, 'test_reduced.edf') raw = _read_raw_edf(raw_fname) # ensure we are writing 'eeg' data raw.set_channel_types({ch: 'eeg' 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)) # # create montage in 'unknown' coordinate frame # # and assert coordsystem/electrodes sidecar tsv don't exist montage = mne.channels.make_dig_montage(ch_pos=ch_pos, coord_frame="unknown") raw.set_montage(montage) with pytest.warns(RuntimeWarning, match="Skipping EEG electrodes.tsv"): write_raw_bids(raw, bids_path, overwrite=True) bids_path.update(root=bids_root) coordsystem_fname = _find_matching_sidecar(bids_path, suffix='coordsystem', extension='.json', on_error='warn') electrodes_fname = _find_matching_sidecar(bids_path, suffix='electrodes', extension='.tsv', on_error='warn') assert coordsystem_fname is None assert electrodes_fname is None # create montage in head frame and set should result in # warning if landmarks not set montage = mne.channels.make_dig_montage(ch_pos=ch_pos, coord_frame="head") raw.set_montage(montage) with pytest.warns(RuntimeWarning, match='Setting montage not possible ' 'if anatomical landmarks'): write_raw_bids(raw, bids_path, overwrite=True) montage = mne.channels.make_dig_montage(ch_pos=ch_pos, coord_frame="head", nasion=[1, 0, 0], lpa=[0, 1, 0], rpa=[0, 0, 1]) raw.set_montage(montage) write_raw_bids(raw, bids_path, overwrite=True) # obtain the sensor positions and assert ch_coords are same raw_test = read_raw_bids(bids_path, verbose=True) assert not object_diff(raw.info['chs'], raw_test.info['chs']) # modify coordinate frame to not-captrak coordsystem_fname = _find_matching_sidecar(bids_path, suffix='coordsystem', extension='.json') _update_sidecar(coordsystem_fname, 'EEGCoordinateSystem', 'besa') with pytest.warns(RuntimeWarning, match='EEG Coordinate frame is not ' 'accepted BIDS keyword'): raw_test = read_raw_bids(bids_path) assert raw_test.info['dig'] is None
def test_handle_info_reading(tmpdir): """Test reading information from a BIDS sidecar JSON file.""" # read in USA dataset, so it should find 50 Hz raw = _read_raw_fif(raw_fname) # write copy of raw with line freq of 60 # bids basename and fname bids_path = BIDSPath(subject='01', session='01', task='audiovisual', run='01', root=tmpdir) suffix = "meg" bids_fname = bids_path.copy().update(suffix=suffix, extension='.fif') write_raw_bids(raw, bids_path, overwrite=True) # find sidecar JSON fname bids_fname.update(datatype=suffix) sidecar_fname = _find_matching_sidecar(bids_fname, suffix=suffix, extension='.json') # assert that we get the same line frequency set raw = read_raw_bids(bids_path=bids_path) assert raw.info['line_freq'] == 60 # setting line_freq to None should produce 'n/a' in the JSON sidecar raw.info['line_freq'] = None write_raw_bids(raw, bids_path, overwrite=True) raw = read_raw_bids(bids_path=bids_path) assert raw.info['line_freq'] is None with open(sidecar_fname, 'r', encoding='utf-8') as fin: sidecar_json = json.load(fin) assert sidecar_json["PowerLineFrequency"] == 'n/a' # 2. if line frequency is not set in raw file, then ValueError del raw.info['line_freq'] with pytest.raises(ValueError, match="PowerLineFrequency .* required"): write_raw_bids(raw, bids_path, overwrite=True) # check whether there are "Extra points" in raw.info['dig'] if # DigitizedHeadPoints is set to True and not otherwise n_dig_points = 0 for dig_point in raw.info['dig']: if dig_point['kind'] == FIFF.FIFFV_POINT_EXTRA: n_dig_points += 1 if sidecar_json['DigitizedHeadPoints']: assert n_dig_points > 0 else: assert n_dig_points == 0 # check whether any of NAS/LPA/RPA are present in raw.info['dig'] # DigitizedLandmark is set to True, and False otherwise landmark_present = False for dig_point in raw.info['dig']: if dig_point['kind'] in [ FIFF.FIFFV_POINT_LPA, FIFF.FIFFV_POINT_RPA, FIFF.FIFFV_POINT_NASION ]: landmark_present = True break if landmark_present: assert sidecar_json['DigitizedLandmarks'] is True else: assert sidecar_json['DigitizedLandmarks'] is False # make a copy of the sidecar in "derivatives/" # to check that we make sure we always get the right sidecar # in addition, it should not break the sidecar reading # in `read_raw_bids` raw.info['line_freq'] = 60 write_raw_bids(raw, bids_path, overwrite=True) deriv_dir = tmpdir.mkdir("derivatives") sidecar_copy = deriv_dir / op.basename(sidecar_fname) with open(sidecar_fname, "r", encoding='utf-8') as fin: sidecar_json = json.load(fin) sidecar_json["PowerLineFrequency"] = 45 _write_json(sidecar_copy, sidecar_json) raw = read_raw_bids(bids_path=bids_path) assert raw.info['line_freq'] == 60 # 3. assert that we get an error when sidecar json doesn't match _update_sidecar(sidecar_fname, "PowerLineFrequency", 55) with pytest.warns(RuntimeWarning, match="Defaulting to .* sidecar JSON"): raw = read_raw_bids(bids_path=bids_path) assert raw.info['line_freq'] == 55
def test_find_matching_sidecar(return_bids_test_dir, tmp_path): """Test finding a sidecar file from a BIDS dir.""" bids_root = return_bids_test_dir bids_path = _bids_path.copy().update(root=bids_root) # Now find a sidecar sidecar_fname = _find_matching_sidecar(bids_path, suffix='coordsystem', extension='.json') expected_file = op.join('sub-01', 'ses-01', 'meg', 'sub-01_ses-01_coordsystem.json') assert sidecar_fname.endswith(expected_file) # Find multiple sidecars, tied in score, triggering an error with pytest.raises(RuntimeError, match='Expected to find a single'): open(sidecar_fname.replace('coordsystem.json', '2coordsystem.json'), 'w').close() print_dir_tree(bids_root) _find_matching_sidecar(bids_path, suffix='coordsystem', extension='.json') # Find nothing and raise. with pytest.raises(RuntimeError, match='Did not find any'): fname = _find_matching_sidecar(bids_path, suffix='foo', extension='.bogus') # Find nothing and receive None and a warning. on_error = 'warn' with pytest.warns(RuntimeWarning, match='Did not find any'): fname = _find_matching_sidecar(bids_path, suffix='foo', extension='.bogus', on_error=on_error) assert fname is None # Find nothing and receive None. on_error = 'ignore' fname = _find_matching_sidecar(bids_path, suffix='foo', extension='.bogus', on_error=on_error) assert fname is None # Invalid on_error. on_error = 'hello' with pytest.raises(ValueError, match='Acceptable values for on_error are'): _find_matching_sidecar(bids_path, suffix='coordsystem', extension='.json', on_error=on_error) # Test behavior of suffix and extension params when suffix and extension # are also (not) present in the passed BIDSPath bids_path = BIDSPath( subject='test', task='task', datatype='eeg', root=tmp_path ) bids_path.mkdir() for suffix, extension in zip( ['eeg', 'eeg', 'events', 'events'], ['.fif', '.json', '.tsv', '.json'] ): bids_path.suffix = suffix bids_path.extension = extension bids_path.fpath.touch() # suffix parameter should always override BIDSPath.suffix bids_path.extension = '.json' for bp_suffix in (None, 'eeg'): bids_path.suffix = bp_suffix s = _find_matching_sidecar(bids_path=bids_path, suffix='events') assert Path(s).name == 'sub-test_task-task_events.json' # extension parameter should always override BIDSPath.extension bids_path.suffix = 'events' for bp_extension in (None, '.json'): bids_path.extension = bp_extension s = _find_matching_sidecar(bids_path=bids_path, extension='.tsv') assert Path(s).name == 'sub-test_task-task_events.tsv' # If suffix and extension parameters are not passed, use BIDSPath # attributes bids_path.suffix = 'events' bids_path.extension = '.tsv' s = _find_matching_sidecar(bids_path=bids_path) assert Path(s).name == 'sub-test_task-task_events.tsv'
def get_head_mri_trans(bids_path, extra_params=None, t1_bids_path=None, fs_subject=None, fs_subjects_dir=None, *, kind=None, verbose=None): """Produce transformation matrix from MEG and MRI landmark points. Will attempt to read the landmarks of Nasion, LPA, and RPA from the sidecar files of (i) the MEG and (ii) the T1-weighted MRI data. The two sets of points will then be used to calculate a transformation matrix from head coordinates to MRI coordinates. .. note:: The MEG and MRI data need **not** necessarily be stored in the same session or even in the same BIDS dataset. See the ``t1_bids_path`` parameter for details. Parameters ---------- bids_path : BIDSPath The path of the electrophysiology recording. If ``datatype`` and ``suffix`` are not present, they will be set to ``'meg'``, and a warning will be raised. .. versionchanged:: 0.10 A warning is raised it ``datatype`` or ``suffix`` are not set. extra_params : None | dict Extra parameters to be passed to :func:`mne.io.read_raw` when reading the MEG file. t1_bids_path : BIDSPath | None If ``None`` (default), will try to discover the T1-weighted MRI file based on the name and location of the MEG recording specified via the ``bids_path`` parameter. Alternatively, you explicitly specify which T1-weighted MRI scan to use for extraction of MRI landmarks. To do that, pass a :class:`mne_bids.BIDSPath` pointing to the scan. Use this parameter e.g. if the T1 scan was recorded during a different session than the MEG. It is even possible to point to a T1 image stored in an entirely different BIDS dataset than the MEG data. fs_subject : str The subject identifier used for FreeSurfer. .. versionchanged:: 0.10 Does not default anymore to ``bids_path.subject`` if ``None``. fs_subjects_dir : path-like | None The FreeSurfer subjects directory. If ``None``, defaults to the ``SUBJECTS_DIR`` environment variable. .. versionadded:: 0.8 kind : str | None The suffix of the anatomical landmark names in the JSON sidecar. A suffix might be present e.g. to distinguish landmarks between sessions. If provided, should not include a leading underscore ``_``. For example, if the landmark names in the JSON sidecar file are ``LPA_ses-1``, ``RPA_ses-1``, ``NAS_ses-1``, you should pass ``'ses-1'`` here. If ``None``, no suffix is appended, the landmarks named ``Nasion`` (or ``NAS``), ``LPA``, and ``RPA`` will be used. .. versionadded:: 0.10 %(verbose)s Returns ------- trans : mne.transforms.Transform The data transformation matrix from head to MRI coordinates. """ if not has_nibabel(): # pragma: no cover raise ImportError('This function requires nibabel.') import nibabel as nib if not isinstance(bids_path, BIDSPath): raise RuntimeError('"bids_path" must be a BIDSPath object. Please ' 'instantiate using mne_bids.BIDSPath().') # check root available meg_bids_path = bids_path.copy() del bids_path if meg_bids_path.root is None: raise ValueError('The root of the "bids_path" must be set. ' 'Please use `bids_path.update(root="<root>")` ' 'to set the root of the BIDS folder to read.') # if the bids_path is underspecified, only get info for MEG data if meg_bids_path.datatype is None: meg_bids_path.datatype = 'meg' warn( 'bids_path did not have a datatype set. Assuming "meg". This ' 'will raise an exception in the future.', module='mne_bids', category=DeprecationWarning) if meg_bids_path.suffix is None: meg_bids_path.suffix = 'meg' warn( 'bids_path did not have a suffix set. Assuming "meg". This ' 'will raise an exception in the future.', module='mne_bids', category=DeprecationWarning) # Get the sidecar file for MRI landmarks t1w_bids_path = ((meg_bids_path if t1_bids_path is None else t1_bids_path).copy().update(datatype='anat', suffix='T1w', task=None)) t1w_json_path = _find_matching_sidecar(bids_path=t1w_bids_path, extension='.json', on_error='ignore') del t1_bids_path if t1w_json_path is not None: t1w_json_path = Path(t1w_json_path) if t1w_json_path is None or not t1w_json_path.exists(): raise FileNotFoundError( f'Did not find T1w JSON sidecar file, tried location: ' f'{t1w_json_path}') for extension in ('.nii', '.nii.gz'): t1w_path_candidate = t1w_json_path.with_suffix(extension) if t1w_path_candidate.exists(): t1w_bids_path = get_bids_path_from_fname(fname=t1w_path_candidate) break if not t1w_bids_path.fpath.exists(): raise FileNotFoundError( f'Did not find T1w recording file, tried location: ' f'{t1w_path_candidate.name.replace(".nii.gz", "")}[.nii, .nii.gz]') # Get MRI landmarks from the JSON sidecar t1w_json = json.loads(t1w_json_path.read_text(encoding='utf-8')) mri_coords_dict = t1w_json.get('AnatomicalLandmarkCoordinates', dict()) # landmarks array: rows: [LPA, NAS, RPA]; columns: [x, y, z] suffix = f"_{kind}" if kind is not None else "" mri_landmarks = np.full((3, 3), np.nan) for landmark_name, coords in mri_coords_dict.items(): if landmark_name.upper() == ('LPA' + suffix).upper(): mri_landmarks[0, :] = coords elif landmark_name.upper() == ('RPA' + suffix).upper(): mri_landmarks[2, :] = coords elif (landmark_name.upper() == ('NAS' + suffix).upper() or landmark_name.lower() == ('nasion' + suffix).lower()): mri_landmarks[1, :] = coords else: continue if np.isnan(mri_landmarks).any(): raise RuntimeError( f'Could not extract fiducial points from T1w sidecar file: ' f'{t1w_json_path}\n\n' f'The sidecar file SHOULD contain a key ' f'"AnatomicalLandmarkCoordinates" pointing to an ' f'object with the keys "LPA", "NAS", and "RPA". ' f'Yet, the following structure was found:\n\n' f'{mri_coords_dict}') # The MRI landmarks are in "voxels". We need to convert them to the # Neuromag RAS coordinate system in order to compare them with MEG # landmarks. See also: `mne_bids.write.write_anat` if fs_subject is None: warn( 'Passing "fs_subject=None" has been deprecated and will raise ' 'an error in future versions. Please explicitly specify the ' 'FreeSurfer subject name.', DeprecationWarning) fs_subject = f'sub-{meg_bids_path.subject}' fs_subjects_dir = get_subjects_dir(fs_subjects_dir, raise_error=False) fs_t1_path = Path(fs_subjects_dir) / fs_subject / 'mri' / 'T1.mgz' if not fs_t1_path.exists(): raise ValueError( f"Could not find {fs_t1_path}. Consider running FreeSurfer's " f"'recon-all` for subject {fs_subject}.") fs_t1_mgh = nib.load(str(fs_t1_path)) t1_nifti = nib.load(str(t1w_bids_path.fpath)) # Convert to MGH format to access vox2ras method t1_mgh = nib.MGHImage(t1_nifti.dataobj, t1_nifti.affine) # convert to scanner RAS mri_landmarks = apply_trans(t1_mgh.header.get_vox2ras(), mri_landmarks) # convert to FreeSurfer T1 voxels (same scanner RAS as T1) mri_landmarks = apply_trans(fs_t1_mgh.header.get_ras2vox(), mri_landmarks) # now extract transformation matrix and put back to RAS coordinates of MRI vox2ras_tkr = fs_t1_mgh.header.get_vox2ras_tkr() mri_landmarks = apply_trans(vox2ras_tkr, mri_landmarks) mri_landmarks = mri_landmarks * 1e-3 # Get MEG landmarks from the raw file _, ext = _parse_ext(meg_bids_path) if extra_params is None: extra_params = dict() if ext == '.fif': extra_params['allow_maxshield'] = True raw = read_raw_bids(bids_path=meg_bids_path, extra_params=extra_params) if (raw.get_montage() is None or raw.get_montage().get_positions() is None or any([ raw.get_montage().get_positions()[fid_key] is None for fid_key in ('nasion', 'lpa', 'rpa') ])): raise RuntimeError( f'Could not extract fiducial points from ``raw`` file: ' f'{meg_bids_path}\n\n' f'The ``raw`` file SHOULD contain digitization points ' 'for the nasion and left and right pre-auricular points ' 'but none were found') pos = raw.get_montage().get_positions() meg_landmarks = np.asarray((pos['lpa'], pos['nasion'], pos['rpa'])) # Given the two sets of points, fit the transform trans_fitted = fit_matched_points(src_pts=meg_landmarks, tgt_pts=mri_landmarks) trans = mne.transforms.Transform(fro='head', to='mri', trans=trans_fitted) return trans
def read_raw_bids(bids_path, extra_params=None, verbose=None): """Read BIDS compatible data. Will attempt to read associated events.tsv and channels.tsv files to populate the returned raw object with raw.annotations and raw.info['bads']. Parameters ---------- bids_path : BIDSPath The file to read. The :class:`mne_bids.BIDSPath` instance passed here **must** have the ``.root`` attribute set. The ``.datatype`` attribute **may** be set. If ``.datatype`` is not set and only one data type (e.g., only EEG or MEG data) is present in the dataset, it will be selected automatically. .. note:: If ``bids_path`` points to a symbolic link of a ``.fif`` file without a ``split`` entity, the link will be resolved before reading. extra_params : None | dict Extra parameters to be passed to MNE read_raw_* functions. Note that the ``exclude`` parameter, which is supported by some MNE-Python readers, is not supported; instead, you need to subset your channels **after** reading. %(verbose)s Returns ------- raw : mne.io.Raw The data as MNE-Python Raw object. Raises ------ RuntimeError If multiple recording data types are present in the dataset, but ``datatype=None``. RuntimeError If more than one data files exist for the specified recording. RuntimeError If no data file in a supported format can be located. ValueError If the specified ``datatype`` cannot be found in the dataset. """ if not isinstance(bids_path, BIDSPath): raise RuntimeError('"bids_path" must be a BIDSPath object. Please ' 'instantiate using mne_bids.BIDSPath().') bids_path = bids_path.copy() sub = bids_path.subject ses = bids_path.session bids_root = bids_path.root datatype = bids_path.datatype suffix = bids_path.suffix # check root available if bids_root is None: raise ValueError('The root of the "bids_path" must be set. ' 'Please use `bids_path.update(root="<root>")` ' 'to set the root of the BIDS folder to read.') # infer the datatype and suffix if they are not present in the BIDSPath if datatype is None: datatype = _infer_datatype(root=bids_root, sub=sub, ses=ses) bids_path.update(datatype=datatype) if suffix is None: bids_path.update(suffix=datatype) if bids_path.fpath.suffix == '.pdf': bids_raw_folder = bids_path.directory / f'{bids_path.basename}' raw_path = list(bids_raw_folder.glob('c,rf*'))[0] config_path = bids_raw_folder / 'config' else: raw_path = bids_path.fpath # Resolve for FIFF files if (raw_path.suffix == '.fif' and bids_path.split is None and raw_path.is_symlink()): target_path = raw_path.resolve() logger.info(f'Resolving symbolic link: ' f'{raw_path} -> {target_path}') raw_path = target_path config_path = None # Special-handle EDF filenames: we accept upper- and lower-case extensions if raw_path.suffix.lower() == '.edf': for extension in ('.edf', '.EDF'): candidate_path = raw_path.with_suffix(extension) if candidate_path.exists(): raw_path = candidate_path break if not raw_path.exists(): raise FileNotFoundError(f'File does not exist: {raw_path}') if config_path is not None and not config_path.exists(): raise FileNotFoundError(f'config directory not found: {config_path}') if extra_params is None: extra_params = dict() elif 'exclude' in extra_params: del extra_params['exclude'] logger.info('"exclude" parameter is not supported by read_raw_bids') if raw_path.suffix == '.fif' and 'allow_maxshield' not in extra_params: extra_params['allow_maxshield'] = True raw = _read_raw(raw_path, electrode=None, hsp=None, hpi=None, config_path=config_path, **extra_params) # Try to find an associated events.tsv to get information about the # events in the recorded data events_fname = _find_matching_sidecar(bids_path, suffix='events', extension='.tsv', on_error='warn') if events_fname is not None: raw = _handle_events_reading(events_fname, raw) # Try to find an associated channels.tsv to get information about the # status and type of present channels channels_fname = _find_matching_sidecar(bids_path, suffix='channels', extension='.tsv', on_error='warn') if channels_fname is not None: raw = _handle_channels_reading(channels_fname, raw) # Try to find an associated electrodes.tsv and coordsystem.json # to get information about the status and type of present channels on_error = 'warn' if suffix == 'ieeg' else 'ignore' electrodes_fname = _find_matching_sidecar(bids_path, suffix='electrodes', extension='.tsv', on_error=on_error) coordsystem_fname = _find_matching_sidecar(bids_path, suffix='coordsystem', extension='.json', on_error=on_error) if electrodes_fname is not None: if coordsystem_fname is None: raise RuntimeError(f"BIDS mandates that the coordsystem.json " f"should exist if electrodes.tsv does. " f"Please create coordsystem.json for" f"{bids_path.basename}") if datatype in ['meg', 'eeg', 'ieeg']: _read_dig_bids(electrodes_fname, coordsystem_fname, raw=raw, datatype=datatype) # Try to find an associated sidecar .json to get information about the # recording snapshot sidecar_fname = _find_matching_sidecar(bids_path, suffix=datatype, extension='.json', on_error='warn') if sidecar_fname is not None: raw = _handle_info_reading(sidecar_fname, raw) # read in associated scans filename scans_fname = BIDSPath(subject=bids_path.subject, session=bids_path.session, suffix='scans', extension='.tsv', root=bids_path.root).fpath if scans_fname.exists(): raw = _handle_scans_reading(scans_fname, raw, bids_path) # read in associated subject info from participants.tsv participants_tsv_path = bids_root / 'participants.tsv' subject = f"sub-{bids_path.subject}" if op.exists(participants_tsv_path): raw = _handle_participants_reading( participants_fname=participants_tsv_path, raw=raw, subject=subject) else: warn(f"participants.tsv file not found for {raw_path}") assert raw.annotations.orig_time == raw.info['meas_date'] return raw