def find_current_run_id(subject_id, session_id, kind, bids_root=None): """ Parameters ---------- subject_id : session_id : kind : bids_root : Returns ------- """ if bids_root is None: bids_root = _get_bidsroot_path() subdir = os.path.join(bids_root, "sub-" + subject_id) sesdir = os.path.join(subdir, "ses-" + session_id) kinddir = os.path.join(sesdir, kind) if not os.path.exists(subdir): warn("new subject") return "01" elif not os.path.exists(sesdir): warn("new session") return "01" elif not os.path.exists(kinddir): warn("new kind") return "01" else: runs = [f for f in os.listdir(kinddir) if f.endswith(".edf")] return str(len(runs) + 1).zfill(2)
def annotate_bad_chs(ctx, subject_id, chs, session_id, acquisition_id): """ Annotate (remove) channels from analysis. Command Format: ez annotate_bad_chs --subject_id <subject_id> --chs <chs> --session_id <session_id> --acquisition_id <acquisition_id> """ clear_screen() input_params = { "subject_id": subject_id, "chs": chs, "session_id": session_id, "acquisition_id": acquisition_id, } _check_annotatechannelfunc_params(input_params, "annotate_bad_chs") logger.info( f"ez annotate_bad_chs with subject_id: {subject_id}, chs: {chs}, " f"session_id: {session_id}, and acquisition_id: {acquisition_id}") bids_root = Path(_get_bidsroot_path()) if acquisition_id is None: acquisition_ids = ["ecog", "seeg"] # _get_subject_acquisitions(bids_root, subject_id, session_id) else: acquisition_ids = [acquisition_id] for acquisition_id in acquisition_ids: bad_channels = channels_to_list(chs) add_bad_chs(bids_root, subject_id, session_id, acquisition_id, bad_channels)
def _check_annotatechannelfunc_params(annotate_params, function): """Check channel annotation (view, write, read) function parameters and output user-friendly text.""" bids_root = Path(_get_bidsroot_path()) subject_id = annotate_params["subject_id"] session_id = annotate_params["session_id"] acquisition_id = annotate_params["acquisition_id"] subject_ids = [ os.path.basename(x).split("-")[-1] for x in bids_root.glob("*") if x.is_dir() if "sub" in x.as_posix() ] if subject_id not in subject_ids: message_str = (f"Subject {subject_id} you passed in " f"is not a valid subject in your subject pool. " f"Here are the current subjects {subject_ids}.") error_msg = f"ez {function}: Subject {subject_id} is not a valid subject" logger.error(error_msg) counted("CLI_exceptions") raise CLI_Input_Error(message_str) if function is not "view_bad_chs": chs = annotate_params["chs"] if chs is None: logger.error(f"ez {function}: Must pass in channels to annotate.") counted("CLI_exceptions") raise CLI_Input_Error( f"ez {function}: Must pass in channels to annotate.")
def view_bad_chs(ctx, subject_id, session_id, acquisition_id): """Print bad channels for a specific subject. Command Format: ez view_bad_chs --subject_id <subject_id> --session_id <session_id> --acquisition_id <acquisition_id> """ clear_screen() input_params = { "subject_id": subject_id, "session_id": session_id, "acquisition_id": acquisition_id, } _check_annotatechannelfunc_params(input_params, "view_bad_chs") logger.info( f"ez view_bad_chs with subject_id: {subject_id}, " f"session_id: {session_id}, and acquisition_id: {acquisition_id}") bids_root = Path(_get_bidsroot_path()) print_channel_status(bids_root, subject_id, session_id, acquisition_id)
def _check_metadatafunc_params(run_params, function): """Checks metadata or pat-summary function parameters and outputs user-friendly text.""" # subject_ids = layout.get_subjects() # faster using os.glob if too many subjects bids_root = Path(_get_bidsroot_path()) # Get subject_id from dict subject_id = run_params["subject_id"] # subject_id of None is allowed for these commands if subject_id is None: pass # if not none, check if a valid subject subject_ids = [ os.path.basename(x).split("-")[-1] for x in bids_root.glob("*") if x.is_dir() if "sub" in x.as_posix() ] if subject_id not in subject_ids and subject_id is not "all": message_str = (f"Subject {subject_id} you passed in " f"is not a valid subject in your subject pool. " f"Here are the current subjects {subject_ids}.") error_msg = f"ez {function}: Subject {subject_id} is not a valid subject" logger.error(error_msg) counted("CLI_exceptions") raise CLI_Input_Error(message_str)
def metadata(ctx, toggle_options, display_cols, clear=True): """ Display (filtered) metadata to the terminal. Metadata needs no arguments, unless you wish to see a subset of the entire patient set. When this is desired, use the toggle options. For instance, the below command will display only male subjects over age 20: ez metadata 'sex=M' 'age>20' """ if clear: clear_screen() baseBids = BaseBids(_get_bidsroot_path()) participants_path = baseBids.participants_tsv_fpath dateparse = lambda x: pd.datetime.strptime(x, "%m/%d/%y") participants_df = pd.read_csv(participants_path, sep="\t", parse_dates=True, date_parser=dateparse) # Print out the column names only if display_cols: click.echo(f"Available columns and types: \n {participants_df.dtypes}") return participants_df, cols, bools, keys = filter_metadata( participants_df, toggle_options) toggle_count = len(cols) sub_ids = participants_df["participant_id"] subject_ids = [] for sub in sub_ids: sub_id = sub.split("-")[1] subject_ids.append(sub_id) # display meessage and logger output click.echo("Displaying Subjects: " + ", ".join(subject_ids)) info_msg = f"{toggle_count} filters: {', '.join(''.join(x) for x in zip(cols, bools, keys))}" logger.info(f"ez metadata with {info_msg}") # define layout using pybids layout = BIDSLayout(_get_bidsroot_path()) # subject_ids = layout.get_subjects() # faster using os.glob if too many subjects bids_root = Path(_get_bidsroot_path()) # for each subject, display metadata # query for sessions, tasks and acquisitions before run_ids for ind, subject in enumerate(subject_ids): subj_bids_parser = BidsParser(bids_root, subject) scans_fpaths = subj_bids_parser.get_scans_fpaths() acquisition_ids = subj_bids_parser.get_acquisitions() # display summary for each acquisition for acquisition in acquisition_ids: # get the file paths to datasets _fpaths = [ x.as_posix() for x in Path(bids_root / f"sub-{subject}").rglob("*.vhdr") ] _fpaths = [x for x in _fpaths if acquisition in x] # extract run run_ids = [_extract_run(x) for x in _fpaths] run_ids = ",".join(run_ids) # display message to user about subject, their metadata, acquisition and run_ids message_str = ( f"Subject ID: {subject} - {', '.join([x + ': ' + str(participants_df.at[ind + 1, x]) for x in cols])} \n" f"{acquisition} run_ids: {run_ids} \n" f"Here is a table of each dataset:") click.echo(_linebreak(message_str)) click.echo(message_str) # display summary for all datasets with this acquisition for scans_fpath in scans_fpaths: pd.options.display.max_colwidth = 90 # read in scans.tsv for each subject scans_tsv = _from_tsv(scans_fpath) scans_df = pd.DataFrame.from_dict(scans_tsv) acquisition_scans = scans_df["filename"].str.contains( acquisition) scans_df = scans_df[acquisition_scans] click.echo(scans_df)
def pat_summary(ctx, subject_id, clear=True): """ Display the summary data for a certain patient, or all patients. If subject_id not specified, or listed as all, will simply print metadata again. Command Format: ez pat-summary --subject_id <subject_id> """ if clear: clear_screen() summary_params = {"subject_id": subject_id} _check_metadatafunc_params(summary_params, "pat-summary") logger.info(f"ez pat-summary with subject_id: {subject_id}") # faster using os.glob if too many subjects - compared pybids bids_root = Path(_get_bidsroot_path()) # read in participants.tsv file and display to user bids_parser = BaseBids(bids_root) participants_tsv = _from_tsv(bids_parser.participants_tsv_fpath) participants_df = pd.DataFrame.from_dict(participants_tsv) # summarize all patients if subject_id == "all": click.echo("All Subjects Summary:") click.echo(participants_df) # summarize a specific patient if subject_id is not None and subject_id != "all": # set display options for the pandas dataframes pd.options.display.max_columns = None pd.options.display.max_rows = None pd.options.display.max_colwidth = 500 # find the location of the scans file subj_bids_parser = BidsParser(bids_root, subject_id) scans_fpaths = subj_bids_parser.get_scans_fpaths() # for each set of scans files, print out summary data for i, scans_fpath in enumerate(scans_fpaths): # read in scans.tsv for each subject scans_tsv = _from_tsv(scans_fpath) # get the scans sidecar dictionaries for eeg and ieeg data eeg_dict, ieeg_dict = split_dict(scans_tsv) # load in files and their corresponding sidecar info # and add it to the summary dictionaries _populate_summary_dict(eeg_dict, eeg_dict["filename"], bids_root) _populate_summary_dict(ieeg_dict, ieeg_dict["filename"], bids_root) # summarize a specific subject participants_df = participants_df.loc[participants_df["participant_id"] == f"sub-{subject_id}"] click.echo(f"Subject {subject_id} Summary:") click.echo(participants_df) message_str = "\n\nSummary of Snapshots for %s" % subject_id click.echo(message_str) click.echo(_linebreak(message_str)) ################################################### # Display the summary for either EEG, or iEEG data. ################################################### if eeg_dict["filename"] == []: message_str = "No scalp EEG data to summarize." click.echo(message_str) click.echo(_linebreak(message_str)) else: message_str = "EEG data summary." click.echo(message_str) click.echo(_linebreak(message_str)) eeg_df = pd.DataFrame.from_dict(eeg_dict) click.echo(eeg_df) if ieeg_dict["filename"] == []: message_str = "No iEEG data to summarize." click.echo(message_str) click.echo(_linebreak(message_str)) else: message_str = "iEEG data summary." click.echo(message_str) click.echo(_linebreak(message_str)) ieeg_df = pd.DataFrame.from_dict(ieeg_dict) click.echo(ieeg_df)
def run( ctx, subject_id, session_id, task_id, acquisition_id, run_id, reference, overwrite, colorblind, clear=True, ): """ Run the fragility analysis on a given subject's EEG snapshot. subject_id, session_id, and run_id are required. Command Format: ez run --subject_id <subject_id> --session_id <session_id> --run_id <run_id> """ # Prompt for arguments not passed if clear: clear_screen() # perform a dynamic check on parameters passed in # to give user an insightful feedback run_params = { "subject_id": subject_id, "session_id": session_id, "acquisition_id": acquisition_id, "run_id": run_id, } _check_runfunc_params(run_params, "run") logger.info( f"ez run with subject_id: {subject_id}, session_id: {session_id}, task_id: {task_id}," f"acquisition_id: {acquisition_id}, run_id: {run_id}, reference: {reference}, " f"overwrite: {overwrite}, colorblind: {colorblind}") # determine kind from acquisition. if acquisition_id in ["ecog", "seeg"]: kind = "ieeg" elif acquisition_id == "eeg": kind = "eeg" ext = "vhdr" # files need to be in BV format # initialize bids_root and derivatives directories bids_root = _get_bidsroot_path() # output_path = _get_derivatives_path # find the bids run file bids_root, bids_fname, datapath = find_bids_run_file( subject_id=subject_id, session_id=session_id, task_id=task_id, acquisition_id=acquisition_id, run_id=run_id, kind=kind, datadir=bids_root, ext=ext, ) # run fragility analysis click.echo(f"Analyzing fragility for the file {datapath}...") deriv_path = setup_fragility( subject_id, acquisition_id, run_id, kind, Path(bids_root), reference, overwrite=overwrite, ) # click.echo(f"Analysis complete. Results saved in {deriv_path}...") logger.info("Analysis complete. Plotting.") click.echo("Analysis complete. Plotting.") plot_params = { "subject_id": subject_id, "session_id": session_id, "task_id": task_id, "acquisition_id": acquisition_id, "run_id": run_id, "kind": kind, "datadir": deriv_path, } ch_names = find_run_channels(**plot_params) # Make sure the channels is in a list if isinstance(ch_names, str): channels = ch_names.split(",") # Find the output .npz file output_fname = find_bids_file( deriv_path, subject_id, session_id, task_id, acquisition_id, run_id, kind, "output", True, ) # Create the filename fig_name = output_fname.replace(".npz", "_heatmap.pdf") results = np.load(output_fname) pertmats = results["pertmats"] # TODO: remove once version is stable if pertmats.shape[1] == len(ch_names): pertmats = pertmats.T # Normalize the perturbation matrix fragmat = Normalize.compute_fragilitymetric(pertmats) # Plot the heatmap with save on plotter = PlotFragilityHeatmap(figure_dir=os.path.dirname(output_fname)) plotter.plot_fragility_heatmap(fragmat, ch_names, colorblind=colorblind, output_fpath=fig_name, fontsize=16) logger.info("Plotting finished") click.echo("Plotting finished!")
def update_channels_file(subject_id, session_id, task_id, acquisition_id, run_id, new_data): """ Parameters ---------- subject_id : session_id : task_id : acquisition_id : run_id : new_data : """ colnames = new_data.columns for col in colnames: new_data[col] = new_data[col].str.lower() bids_fname = make_bids_basename( subject=subject_id, session=session_id, task=task_id, acquisition=acquisition_id, run=run_id, ) bidsRun = BidsRun(_get_bidsroot_path(), bids_fname) current_run_channels = bidsRun.get_channels_metadata() for ind, row in new_data.iterrows(): for sind, val in row.items(): if "bad" in val: bad_channel = ind + sind bidsRun.modify_channel_info("status", bad_channel, "bad") if "description" not in current_run_channels[0].keys(): bidsRun.append_channel_info("description", bad_channel, "bad") else: bidsRun.modify_channel_info("description", bad_channel, "bad") elif "wm" in val: bad_channel = ind + sind bidsRun.modify_channel_info("status", bad_channel, "bad") if "description" not in current_run_channels[0].keys(): bidsRun.append_channel_info("description", bad_channel, "WM") else: bidsRun.modify_channel_info("description", bad_channel, "WM") elif "out" in val: bad_channel = ind + sind bidsRun.modify_channel_info("status", bad_channel, "bad") if "description" not in current_run_channels[0].keys(): bidsRun.append_channel_info("description", bad_channel, "out") else: bidsRun.modify_channel_info("description", bad_channel, "out") elif "csf" in val: bad_channel = ind + sind bidsRun.modify_channel_info("status", bad_channel, "bad") if "description" not in current_run_channels[0].keys(): bidsRun.append_channel_info("description", bad_channel, "csf") else: bidsRun.modify_channel_info("description", bad_channel, "csf") elif "ventricle" in val: bad_channel = ind + sind bidsRun.modify_channel_info("status", bad_channel, "bad") if "description" not in current_run_channels[0].keys(): bidsRun.append_channel_info("description", bad_channel, "ventricle") else: bidsRun.modify_channel_info("description", bad_channel, "ventricle") else: channel = ind + sind if "description" not in current_run_channels[0].keys(): bidsRun.append_channel_info("description", channel, val) else: bidsRun.modify_channel_info("description", channel, val)
def _check_runfunc_params(run_params, function): """Check run or plot parameters and outputs user-friendly text.""" bids_root = Path(_get_bidsroot_path()) subject_id = run_params["subject_id"] session_id = run_params["session_id"] acquisition_id = run_params["acquisition_id"] run_id = run_params["run_id"] if any(x is None for x in [subject_id, session_id, run_id]): logger.error( f"ez {function} with subject_id: {subject_id}, session_id: {session_id}, " f"acquisition_id: {acquisition_id}, and run_id: {run_id}. \n" f"subject_id, session_id, and run_id must be set.") counted("CLI_exceptions") raise CLI_Input_Error( "\n\nEZTrack run requires subject_id, session_id, and run_id to be " "specified to analyze a specifically uploaded dataset. " "To see all subjects available, use the 'pat-summary' command. " "To see all datasets available, use the 'metadata' command. \n\n" "An example command looks like: " f"'ez {function} --subject_id <sub_id> --session_id <session_id> --run_id <run_id>'" "\n\nReplace words in brackets with your desired data identifiers." ) subject_ids = [ os.path.basename(x).split("-")[-1] for x in bids_root.glob("*") if x.is_dir() if "sub" in x.as_posix() ] if subject_id not in subject_ids: message_str = (f"Subject {subject_id} you passed in " f"is not a valid subject in your subject pool. " f"Here are the current subjects {subject_ids}.") error_msg = f"ez {function}: Subject {subject_id} is not a valid subject" logger.error(error_msg) counted("CLI_exceptions") raise CLI_Input_Error(message_str) acquisitions = ["ecog", "seeg", "eeg"] if acquisition_id not in acquisitions: error_msg = f"ez {function}: Acquisition {acquisition_id} is not supported yet." logger.error(error_msg) counted("CLI_exceptions") raise CLI_Input_Error(f"{acquisition_id} is not supported yet... " f"Pass 'ecog', 'seeg', or 'eeg'.") subj_bids_parser = BidsParser(bids_root, subject_id) # get the file paths to datasets _fpaths = [ x.as_posix() for x in Path(bids_root / f"sub-{subject_id}").rglob("*.vhdr") ] _fpaths = [x for x in _fpaths if acquisition_id in x] # extract run run_ids = [_extract_run(x) for x in _fpaths] run_ids = ",".join(run_ids) if run_id not in run_ids: message_str = ( f"Run {run_id} you passed in " f"is not a valid run for subject {subject_id} and acquisition {acquisition_id}. " f"Here are the current run ids {run_ids}.") error_msg = f"ez {function}: Run {run_id} is not a valid run" logger.error(error_msg) counted("CLI_exceptions") raise CLI_Input_Error(message_str) if function == "plot": colorblind = run_params["colorblind"] cmap = run_params["cmap"] # If colorblind is turned on, cmap should be None if colorblind and cmap is not None: logger.error( f"ez {function} with colorblind: True and cmap: {cmap}. Only one option can" f" be turned on.") counted("CLI_exceptions") raise CLI_Input_Error( "The colorblind option was turned on. You cannot also set a colormap." ) # Check if a valid colormap was entered cmap_opts = plt.colormaps() if cmap not in cmap_opts and cmap is not None: logger.error( f"ez {function} with cmap: {cmap}. This is not a valid matplotlib cmap." ) counted("CLI_exceptions") raise CLI_Input_Error( f"{cmap} is not a value colormap. Options are: {cmap_opts}")