def test_get_entities_from_fname_errors(fname): """Test parsing entities from bids filename. Extends utility for not supported BIDS entities, such as 'description'. """ if 'desc' in fname: with pytest.raises(KeyError, match='Unexpected entity'): params = get_entities_from_fname(fname, on_error='raise') with pytest.warns(RuntimeWarning, match='Unexpected entity'): params = get_entities_from_fname(fname, on_error='warn') params = get_entities_from_fname(fname, on_error='ignore') else: params = get_entities_from_fname(fname, on_error='raise') expected_keys = [ 'subject', 'session', 'task', 'acquisition', 'run', 'processing', 'space', 'recording', 'split', 'suffix' ] assert params['subject'] == '01' assert params['session'] == '02' assert params['run'] == '3' assert params['task'] == 'test' assert params['split'] == '01' if 'meg' in fname: assert params['suffix'] == 'meg' if 'desc' in fname: assert params['desc'] == 'tfr' expected_keys.append('desc') assert list(params.keys()) == expected_keys
def find_result_path(self): """ Identifies the directory path pointing to where results stored Following BIDS requirements, we only have either the subject folder or both subject and session. Parameters ---------- none Returns ------- result_path: str location of results files within BIDS folder """ params = get_entities_from_fname(self.unique_name) if params["ses"] is None: result_path = os.path.join(self.root_path, "derivatives", "automagic", f"sub-{params['sub']}") else: result_path = os.path.join( self.root_path, "derivatives", "automagic", f"sub-{params['sub']}", f"ses-{params['ses']}", ) return result_path
def test_get_entities_from_fname(fname): """Test parsing entities from a bids filename.""" params = get_entities_from_fname(fname) assert params['subject'] == '01' assert params['session'] == '02' assert params['run'] == '3' assert params['task'] == 'test' assert params['split'] == '01' assert list(params.keys()) == ['subject', 'session', 'task', 'acquisition', 'run', 'processing', 'space', 'recording', 'split']
def load_data(bids_basename): # read_raw_bids automatically # - populates bad channels using the BIDS channels.tsv # - sets channels types according to BIDS channels.tsv `type` column # - sets raw.annotations using the BIDS events.tsv params = get_entities_from_fname(bids_basename) subject = params['subject'] session = params['session'] extra_params = dict() if config.allow_maxshield: extra_params['allow_maxshield'] = config.allow_maxshield raw = read_raw_bids(bids_basename=bids_basename, bids_root=config.bids_root, extra_params=extra_params, kind=config.get_kind()) if config.daysback is not None: raw.anonymize(daysback=config.daysback) if subject != 'emptyroom': # Crop the data. if config.crop is not None: raw.crop(*config.crop) # Rename events. if config.rename_events: rename_events(raw=raw, subject=subject, session=session) raw.load_data() if hasattr(raw, 'fix_mag_coil_types'): raw.fix_mag_coil_types() montage_name = config.eeg_template_montage if config.get_kind() == 'eeg' and montage_name: msg = (f'Setting EEG channel locatiions to template montage: ' f'{montage_name}.') logger.info(gen_log_message(message=msg, step=1, subject=subject, session=session)) montage = mne.channels.make_standard_montage(montage_name) raw.set_montage(montage, on_missing='warn') return raw
def convert_img_to_bids(image_input, bids_root, bids_fname, verbose=True): """Run Bids Conversion script to be updated. Performs BIDS conversion for Ct/T1/DTI/fMRI data. TODO: demo for DTI/FMRI """ if verbose: print(f"bids_root is {bids_root}") print(f"Reading in image files from: {image_input}") # create temporary filepath to store the nifti file with tempfile.TemporaryDirectory() as tmpdir: output_fpath = Path(tmpdir, "tmp.nii").as_posix() if len([x for x in Path(image_input).glob("*.dcm")]) > 0: print("Converting dicom -> Nifti...") # try to run mrconvert and reorient to `LAS` direction # try: image_input = _convert_dicom_to_nifti(image_input, output_fpath) # except Exception as e: # "mrconvert {params.CT_FOLDER} {output.CT_bids_fname};" else: print("Passed NIFTI image, so skipping mrconvert from dicom -> nifti...") image_input = str(image_input) print(image_input) # determine the BIDS identifiers params = get_entities_from_fname(bids_fname) subject = params["sub"] session = params["ses"] print("\n\nWriting now to BIDS...") # write to BIDS anat_dir = write_anat( bids_root, subject, t1w=image_input, session=session, overwrite=True, verbose=True, )
def _read_dig_bids(electrodes_fpath, coordsystem_fpath, raw, datatype, verbose): """Read MNE-Python formatted DigMontage from BIDS files. Handles coordinatesystem.json and electrodes.tsv reading to DigMontage. Parameters ---------- electrodes_fpath : str Filepath of the electrodes.tsv to read. coordsystem_fpath : str Filepath of the coordsystem.json to read. raw : instance of Raw The data as MNE-Python Raw object. datatype : str Type of the data recording. Can be ``meg``, ``eeg``, or ``ieeg``. verbose : bool Set verbose output to true or false. Returns ------- raw : instance of Raw The data as MNE-Python Raw object. """ # get the space entity params = get_entities_from_fname(electrodes_fpath) space = params['space'] if space is None: space = '' space = space.lower() # read in coordinate information coord_frame, coord_unit = _handle_coordsystem_reading(coordsystem_fpath, datatype, verbose) if datatype == 'meg': if coord_frame not in BIDS_MEG_COORDINATE_FRAMES: warn("MEG Coordinate frame is not accepted " "BIDS keyword. The allowed keywords are: " "{}".format(BIDS_MEG_COORDINATE_FRAMES)) coord_frame = None elif coord_frame == 'other': warn("Coordinate frame of MEG data can't be determined " "when 'other'. The currently accepted keywords are: " "{}".format(BIDS_MEG_COORDINATE_FRAMES)) coord_frame = None else: coord_frame = BIDS_TO_MNE_FRAMES.get(coord_frame, None) elif datatype == 'ieeg': if coord_frame not in BIDS_IEEG_COORDINATE_FRAMES: warn("iEEG Coordinate frame is not accepted " "BIDS keyword. The allowed keywords are: " "{}".format(BIDS_IEEG_COORDINATE_FRAMES)) coord_frame = None elif coord_frame == 'pixels': warn("Coordinate frame of iEEG data in pixels does not " "get read in by mne-python. Skipping reading of " "electrodes.tsv ...") coord_frame = None elif coord_frame == 'acpc': coord_frame = BIDS_TO_MNE_FRAMES.get(coord_frame, None) elif coord_frame == 'other': # XXX: We allow 'other' coordinate frames, but must be mne-python if space not in BIDS_TO_MNE_FRAMES: # default coordinate frames to available ones in mne-python # noqa: see https://bids-specification.readthedocs.io/en/stable/99-appendices/08-coordinate-systems.html warn("Defaulting coordinate frame to unknown " "from coordinate system input {}".format(coord_frame)) coord_frame = BIDS_TO_MNE_FRAMES.get(space, None) elif datatype == 'eeg': # only accept captrak if coord_frame not in BIDS_EEG_COORDINATE_FRAMES: warn("EEG Coordinate frame is not accepted " "BIDS keyword. The allowed keywords are: " "{}".format(BIDS_IEEG_COORDINATE_FRAMES)) coord_frame = None else: coord_frame = BIDS_TO_MNE_FRAMES.get(coord_frame, None) # check coordinate units if coord_unit not in BIDS_COORDINATE_UNITS: warn("Coordinate unit is not an accepted BIDS unit for {}. " "Please specify to be one of {}. Skipping electrodes.tsv " "reading..." .format(electrodes_fpath, BIDS_COORDINATE_UNITS)) coord_frame = None # only set montage if coordinate frame was properly parsed if coord_frame is not None: # read in electrode coordinates and attach to raw raw = _handle_electrodes_reading(electrodes_fpath, coord_frame, coord_unit, raw, verbose) return raw
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 _write_dig_bids(electrodes_path, coordsystem_path, root, raw, datatype, overwrite=False, verbose=True): """Write BIDS formatted DigMontage from Raw instance. Handles coordinatesystem.json and electrodes.tsv writing from DigMontage. Parameters ---------- electrodes_path : str Filename to save the electrodes.tsv to. coordsystem_path : str Filename to save the coordsystem.json to. root : str | pathlib.Path Path to the data directory raw : instance of Raw The data as MNE-Python Raw object. datatype : str Type of the data recording. Can be ``meg``, ``eeg``, or ``ieeg``. overwrite : bool Whether to overwrite the existing file. Defaults to False. verbose : bool Set verbose output to true or false. """ # write electrodes data for iEEG and EEG unit = "m" # defaults to meters params = get_entities_from_fname(electrodes_path) subject_id = params['subject'] session_id = params['session'] acquisition = params['acquisition'] # get coordinate frame from digMontage digpoint = raw.info['dig'][0] if any(digpoint['coord_frame'] != _digpoint['coord_frame'] for _digpoint in raw.info['dig']): warn("Not all digpoints have the same coordinate frame. " "Skipping electrodes.tsv writing...") return # get the accepted mne-python coordinate frames coord_frame_int = int(digpoint['coord_frame']) mne_coord_frame = MNE_FRAME_TO_STR.get(coord_frame_int, None) coord_frame = MNE_TO_BIDS_FRAMES.get(mne_coord_frame, None) if verbose: print("Writing electrodes file to... ", electrodes_path) print("Writing coordsytem file to... ", coordsystem_path) if datatype == "ieeg": if coord_frame is not None: # XXX: To improve when mne-python allows coord_frame='unknown' if coord_frame not in BIDS_IEEG_COORDINATE_FRAMES: coordsystem_path = BIDSPath(subject=subject_id, session=session_id, acquisition=acquisition, space=coord_frame, suffix='coordsystem', extension='.json', root=root) electrodes_path = BIDSPath(subject=subject_id, session=session_id, acquisition=acquisition, space=coord_frame, suffix='electrodes', extension='.tsv', root=root) coord_frame = 'Other' # Now write the data to the elec coords and the coordsystem _electrodes_tsv(raw, electrodes_path, datatype, overwrite, verbose) _coordsystem_json(raw, unit, 'n/a', coord_frame, coordsystem_path, datatype, overwrite, verbose) else: # default coordinate frame to mri if not available warn("Coordinate frame of iEEG coords missing/unknown " "for {}. Skipping reading " "in of montage...".format(electrodes_path)) elif datatype == 'eeg': # We only write EEG electrodes.tsv and coordsystem.json # if we have LPA, RPA, and NAS available to rescale to a known # coordinate system frame coords = _extract_landmarks(raw.info['dig']) landmarks = set(['RPA', 'NAS', 'LPA']) == set(list(coords.keys())) # XXX: to be improved to allow rescaling if landmarks are present # mne-python automatically converts unknown coord frame to head if coord_frame_int == FIFF.FIFFV_COORD_HEAD and landmarks: # Now write the data _electrodes_tsv(raw, electrodes_path, datatype, overwrite, verbose) _coordsystem_json(raw, 'm', 'RAS', 'CapTrak', coordsystem_path, datatype, overwrite, verbose) else: warn("Skipping EEG electrodes.tsv... " "Setting montage not possible if anatomical " "landmarks (NAS, LPA, RPA) are missing, " "and coord_frame is not 'head'.")