Beispiel #1
0
def _get_BIDSPath(subject, bidsdir):
    from pymento_meg.utils import _construct_path

    _construct_path([bidsdir, f"sub-{subject}/"])
    bids_path = BIDSPath(subject=subject,
                         task="memento",
                         root=bidsdir,
                         suffix="meg",
                         extension=".fif")
    return bids_path
Beispiel #2
0
def motion_estimation(subject, raw, figdir="/tmp/"):
    """
    Calculate head positions from HPI coils as a prerequisite for movement
    correction.
    :param subject: str, subject identifier; used for writing file names &
    logging
    :param raw: Raw data object
    :param figdir: str, path to directory for diagnostic plots
    """
    # Calculate head motion parameters to remove them during maxwell filtering
    # First, extract HPI coil amplitudes to
    logging.info(f"Extracting HPI coil amplitudes for subject sub-{subject}")
    chpi_amplitudes = mne.chpi.compute_chpi_amplitudes(raw)
    # compute time-varying HPI coil locations from amplitudes
    chpi_locs = mne.chpi.compute_chpi_locs(raw.info, chpi_amplitudes)
    logging.info(f"Computing head positions for subject sub-{subject}")
    head_pos = mne.chpi.compute_head_pos(raw.info, chpi_locs, verbose=True)
    # For now, DON'T save headpositions. It is unclear in which BIDS directory.
    # TODO: Figure out whether we want to save them.
    # save head positions
    # outpath = _construct_path(
    #    [
    #        Path(head_pos_outdir),
    #        f"sub-{subject}",
    #        "meg",
    #        f"sub-{subject}_task-memento_headshape.pos",
    #    ]
    # )
    # logging.info(f"Saving head positions as {outpath}")
    # mne.chpi.write_head_pos(outpath, head_pos)

    figpath = _construct_path([
        Path(figdir),
        f"sub-{subject}",
        "meg",
        f"sub-{subject}_task-memento_headmovement.png",
    ])
    fig = mne.viz.plot_head_positions(head_pos, mode="traces")
    fig.savefig(figpath)
    figpath = _construct_path([
        Path(figdir),
        f"sub-{subject}",
        "meg",
        f"sub-{subject}_task-memento_headmovement_scaled.png",
    ])
    fig = mne.viz.plot_head_positions(head_pos,
                                      mode="traces",
                                      destination=raw.info["dev_head_t"],
                                      info=raw.info)
    fig.savefig(figpath)
    return head_pos
Beispiel #3
0
def ZAPline(raw, subject, figdir):
    """
    Prior to running signal space separation, we are removing power-line noise
    at 50Hz, and noise from the presentation monitor at around 58.5Hz
    :return:
    """
    # get all data from MEG sensors as a matrix
    raw.load_data()
    meg_ch_idx = mne.pick_types(raw.info, meg=True)
    data = raw.get_data(picks=meg_ch_idx)
    fig = raw.plot_psd(fmin=1, fmax=150)
    figpath = _construct_path([
        Path(figdir),
        f"sub-{subject}",
        "meg",
        f"sub-{subject}_nofilter.png",
    ])
    fig.savefig(figpath)
    # get the relevant frequencies for each subject
    i = 0
    for freq in ZAPlinefreqs[subject]['freqs']:
        logging.info(f"Filtering noise at {freq}Hz from raw data of subject"
                     f" sub-{subject} with n="
                     f"{ZAPlinefreqs[subject]['components'][i]} components")
        clean, artifact = \
            dss_line(data.T,
                     fline=freq,
                     sfreq=1000,
                     nremove=ZAPlinefreqs[subject]['components'][i])
        # diagnostic plot
        raw._data[meg_ch_idx] = clean.T
        fig = raw.plot_psd(fmin=1, fmax=150)
        figpath = _construct_path([
            Path(figdir),
            f"sub-{subject}",
            "meg",
            f"sub-{subject}_zapline_{freq}hz-filter.png",
        ])
        logging.info(f"Saving PSD plot into {figpath}")
        fig.savefig(figpath)
        # overwrite data with cleaned data for next frequency
        data = clean.T
        i += 1
    del data, clean, artifact
    return raw
Beispiel #4
0
def plot_psd(raw, subject, figdir, filtering):
    """
    Helper to plot spectral densities
    """
    print(f"Plotting spectral density plots for subject sub-{subject}"
          f"after Maxwell filtering.")
    if filtering:
        # append a 'filtered' suffix to the file name
        fname = _construct_path([
            Path(figdir),
            f"sub-{subject}",
            "meg",
            f"sub-{subject}_task-memento_spectral-density_filtered.png",
        ])
    else:
        fname = _construct_path([
            Path(figdir),
            f"sub-{subject}",
            "meg",
            f"sub-{subject}_task-memento_spectral-density.png",
        ])
    fig = raw.plot_psd()
    fig.savefig(fname)
Beispiel #5
0
def remove_eyeblinks_and_heartbeat(raw, subject, figdir, events, eventid, rng):
    """
    Find and repair eyeblink and heartbeat artifacts in the data.
    Data should be filtered.
    Importantly, ICA is fitted on artificially epoched data with reject
    criteria estimates via the autoreject package - this is done to reject high-
    amplitude artifacts to influence the ICA solution.
    The ICA fit is then applied to the raw data.
    :param raw: Raw data
    :param subject: str, subject identifier, e.g., '001'
    :param figdir:
    :param rng: random state instance
    """
    # prior to an ICA, it is recommended to high-pass filter the data
    # as low frequency artifacts can alter the ICA solution. We fit the ICA
    # to high-pass filtered (1Hz) data, and apply it to non-highpass-filtered
    # data
    logging.info("Applying a temporary high-pass filtering prior to ICA")
    filt_raw = raw.copy()
    filt_raw.load_data().filter(l_freq=1., h_freq=None)
    # evoked eyeblinks and heartbeats for diagnostic plots

    eog_evoked = create_eog_epochs(filt_raw).average()
    eog_evoked.apply_baseline(baseline=(None, -0.2))
    if subject == '008':
        # subject 008's ECG channel is flat. It will not find any heartbeats by
        # default. We let it estimate heartbeat from magnetometers. For this,
        # we'll drop the ECG channel
        filt_raw.drop_channels('ECG003')
    ecg_evoked = create_ecg_epochs(filt_raw).average()
    ecg_evoked.apply_baseline(baseline=(None, -0.2))
    # make sure that we actually found sensible artifacts here
    eog_fig = eog_evoked.plot_joint()
    for i, fig in enumerate(eog_fig):
        fname = _construct_path([
            Path(figdir),
            f"sub-{subject}",
            "meg",
            f"evoked-artifact_eog_sub-{subject}_{i}.png",
        ])
        fig.savefig(fname)
    ecg_fig = ecg_evoked.plot_joint()
    for i, fig in enumerate(ecg_fig):
        fname = _construct_path([
            Path(figdir),
            f"sub-{subject}",
            "meg",
            f"evoked-artifact_ecg_sub-{subject}_{i}.png",
        ])
        fig.savefig(fname)
    # Chunk raw data into epochs to fit the ICA
    # No baseline correction as it would interfere with ICA.
    logging.info("Epoching filtered data")
    epochs = mne.Epochs(filt_raw,
                        events,
                        event_id=eventid,
                        tmin=0,
                        tmax=3,
                        picks='meg',
                        baseline=None)

    ## First, estimate rejection criteria for high-amplitude artifacts. This is
    ## done via autoreject
    #logging.info('Estimating bad epochs quick-and-dirty, to improve ICA')
    #ar = AutoReject(random_state=rng)
    # fit on first 200 epochs to save (a bit of) time
    #epochs.load_data()
    #ar.fit(epochs[:200])
    #epochs_ar, reject_log = ar.transform(epochs, return_log=True)

    # run an ICA to capture heartbeat and eyeblink artifacts.
    # set a seed for reproducibility.
    # When left to figure out the component number by itself, it ends up with
    # about 80. I'm setting n_components to 45 to have a chance at checking them
    # by hand.
    # We fit it on a set of epochs excluding the initial bad epochs following
    # https://github.com/autoreject/autoreject/blob/dfbc64f49eddeda53c5868290a6792b5233843c6/examples/plot_autoreject_workflow.py
    logging.info('Fitting the ICA')
    ica = ICA(max_iter='auto', n_components=45, random_state=rng)
    ica.fit(epochs)  #[~reject_log.bad_epochs])
    logging.info("Searching for eyeblink and heartbeat artifacts in the data")
    # get ICA components for the given subject
    if subject == '008':
        eog_indices = [10]
        ecg_indices = [29]
        #eog_indices = ica_comps[subject]['eog']
        #ecg_indices = ica_comps[subject]['ecg']
    # An initially manual component detection did not reproduce after a software
    # update - for now, we have to do the automatic detection for all but sub 8
    else:
        eog_indices, eog_scores = ica.find_bads_eog(filt_raw)
        ecg_indices, ecg_scores = ica.find_bads_ecg(filt_raw)
        logging.info(f"Identified the following EOG components: {eog_indices}")
        logging.info(f"Identified the following ECG components: {ecg_indices}")
    # visualize the components
    components = ica.plot_components()
    for i, fig in enumerate(components):
        fname = _construct_path([
            Path(figdir),
            f"sub-{subject}",
            "meg",
            f"ica-components_sub-{subject}_{i}.png",
        ])
        fig.savefig(fname)
    # visualize the time series of components and save it
    plt.rcParams['figure.figsize'] = 30, 20
    comp_sources = ica.plot_sources(epochs)
    fname = _construct_path([
        Path(figdir),
        f"sub-{subject}",
        "meg",
        f"ica-components_sources_sub-{subject}.png",
    ])
    comp_sources.savefig(fname)
    # reset plotting params
    plt.rcParams['figure.figsize'] = plt.rcParamsDefault['figure.figsize']

    # plot EOG components
    overlay_eog = ica.plot_overlay(eog_evoked,
                                   exclude=ica_comps[subject]['eog'])
    fname = _construct_path([
        Path(figdir),
        f"sub-{subject}",
        "meg",
        f"ica-eog-components_over-avg-epochs_sub-{subject}.png",
    ])
    overlay_eog.savefig(fname)
    # plot ECG components
    overlay_ecg = ica.plot_overlay(ecg_evoked,
                                   exclude=ica_comps[subject]['ecg'])
    fname = _construct_path([
        Path(figdir),
        f"sub-{subject}",
        "meg",
        f"ica-ecg-components_over-avg-epochs_sub-{subject}.png",
    ])
    overlay_ecg.savefig(fname)
    # plot EOG component properties
    figs = ica.plot_properties(filt_raw, picks=eog_indices)
    for i, fig in enumerate(figs):
        fname = _construct_path([
            Path(figdir),
            f"sub-{subject}",
            "meg",
            f"ica-property{i}_artifact-eog_sub-{subject}.png",
        ])
        fig.savefig(fname)
    # plot ECG component properties
    figs = ica.plot_properties(filt_raw, picks=ecg_indices)
    for i, fig in enumerate(figs):
        fname = _construct_path([
            Path(figdir),
            f"sub-{subject}",
            "meg",
            f"ica-property{i}_artifact-ecg_sub-{subject}.png",
        ])
        fig.savefig(fname)

    # Set the indices to be excluded
    ica.exclude = eog_indices
    ica.exclude.extend(ecg_indices)

    # plot ICs applied to the averaged EOG epochs, with EOG matches highlighted
    sources = ica.plot_sources(eog_evoked)
    fname = _construct_path([
        Path(figdir),
        f"sub-{subject}",
        "meg",
        f"ica-sources_artifact-eog_sub-{subject}.png",
    ])
    sources.savefig(fname)

    # plot ICs applied to the averaged ECG epochs, with ECG matches highlighted
    sources = ica.plot_sources(ecg_evoked)
    fname = _construct_path([
        Path(figdir),
        f"sub-{subject}",
        "meg",
        f"ica-sources_artifact-ecg_sub-{subject}.png",
    ])
    sources.savefig(fname)
    # apply the ICA to the raw data
    logging.info('Applying ICA to the raw data.')
    raw.load_data()
    ica.apply(raw)
Beispiel #6
0
def epoch_and_clean_trials(subject,
                           diagdir,
                           bidsdir,
                           datadir,
                           derivdir,
                           epochlength=3,
                           eventid={'visualfix/fixCross': 10}):
    """
    Chunk the data into epochs starting at the eventid specified per trial,
    lasting 7 seconds (which should include all trial elements).
    Do automatic artifact detection, rejection and fixing for eyeblinks,
    heartbeat, and high- and low-amplitude artifacts.
    :param subject: str, subject identifier. takes the form '001'
    :param diagdir: str, path to a directory where diagnostic plots can be saved
    :param bidsdir: str, path to a directory with BIDS data. Needed to load
    event logs from the experiment
    :param datadir: str, path to a directory with SSS-processed data
    :param derivdir: str, path to a directory where cleaned epochs can be saved
    :param epochlength: int, length of epoch
    :param eventid: dict, the event to start an Epoch from
    """
    # construct name of the first split
    raw_fname = Path(datadir) / f'sub-{subject}/meg' / \
                f'sub-{subject}_task-memento_proc-sss_meg.fif'
    logging.info(f"Reading in SSS-processed data from subject sub-{subject}. "
                 f"Attempting the following path: {raw_fname}")
    raw = mne.io.read_raw_fif(raw_fname)
    events, event_dict = get_events(raw)
    # filter the data to remove high-frequency noise. Minimal high-pass filter
    # based on
    # https://www.sciencedirect.com/science/article/pii/S0165027021000157
    # ensure the data is loaded prior to filtering
    raw.load_data()
    if subject == '017':
        logging.info('Setting additional bad channels for subject 17')
        raw.info['bads'] = ['MEG0313', 'MEG0513', 'MEG0523']
        raw.interpolate_bads()
    # high-pass doesn't make sense, raw data has 0.1Hz high-pass filter already!
    _filter_data(raw, h_freq=100)
    # ICA to detect and repair artifacts
    logging.info('Removing eyeblink and heartbeat artifacts')
    rng = np.random.RandomState(28)
    remove_eyeblinks_and_heartbeat(
        raw=raw,
        subject=subject,
        figdir=diagdir,
        events=events,
        eventid=eventid,
        rng=rng,
    )
    # get the actual epochs: chunk the trial into epochs starting from the
    # event ID. Do not baseline correct the data.
    logging.info(f'Creating epochs of length {epochlength}')
    if eventid == {'press/left': 1, 'press/right': 4}:
        # when centered on the response, move back in time
        epochs = mne.Epochs(raw,
                            events,
                            event_id=eventid,
                            tmin=-epochlength,
                            tmax=0,
                            picks='meg',
                            baseline=None)
    else:
        epochs = mne.Epochs(raw,
                            events,
                            event_id=eventid,
                            tmin=0,
                            tmax=epochlength,
                            picks='meg',
                            baseline=None)
    # ADD SUBJECT SPECIFIC TRIAL NUMBER TO THE EPOCH! ONLY THIS WAY WE CAN
    # LATER RECOVER WHICH TRIAL PARAMETERS WE'RE LOOKING AT BASED ON THE LOGS AS
    # THE EPOCH REJECTION WILL REMOVE TRIALS
    logging.info("Retrieving trial metadata.")
    from pymento_meg.proc.epoch import get_trial_features
    metadata = get_trial_features(bids_path=bidsdir,
                                  subject=subject,
                                  column='trial_no')
    # transform to integers
    metadata = metadata.astype(int)
    # this does not work if we start at fixation cross for subject 5, because 1
    # fixation cross trigger is missing from the data, and it becomes impossible
    # to associate the trial metadata to the correct trials in the data
    epochs.metadata = metadata
    epochs.load_data()
    ## downsample the data to 200Hz
    #logging.info('Resampling epoched data down to 200 Hz')
    #epochs.resample(sfreq=200, verbose=True)
    # use autoreject to repair bad epochs
    ar = AutoReject(
        random_state=rng,
        n_interpolate=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15])
    epochs_clean, reject_log = ar.fit_transform(epochs, return_log=True)
    # save the cleaned, epoched data to disk.
    outpath = _construct_path([
        Path(derivdir),
        f"sub-{subject}",
        "meg",
        f"sub-{subject}_task-memento_cleaned_epo.fif",
    ])
    logging.info(f"Saving cleaned, epoched data to {outpath}")
    epochs_clean.save(outpath, overwrite=True)
    # visualize the bad sensors for each trial
    fig = ar.get_reject_log(epochs).plot()
    fname = _construct_path([
        Path(diagdir),
        f"sub-{subject}",
        "meg",
        f"epoch-rejectlog_sub-{subject}.png",
    ])
    fig.savefig(fname)
    # plot the average of all cleaned epochs
    fig = epochs_clean.average().plot()
    fname = _construct_path([
        Path(diagdir),
        f"sub-{subject}",
        "meg",
        f"clean-epoch_average_sub-{subject}.png",
    ])
    fig.savefig(fname)
    # plot psd of cleaned epochs
    psd = epochs_clean.plot_psd()
    fname = _construct_path([
        Path(diagdir),
        f"sub-{subject}",
        "meg",
        f"psd_cleaned-epochs-{subject}.png",
    ])
    psd.savefig(fname)
Beispiel #7
0
def plot_noisy_channel_detection(auto_scores,
                                 subject="test",
                                 ch_type="grad",
                                 outpath="/tmp/"):

    # Select the data for specified channel type
    ch_subset = auto_scores["ch_types"] == ch_type
    ch_names = auto_scores["ch_names"][ch_subset]
    scores = auto_scores["scores_noisy"][ch_subset]
    limits = auto_scores["limits_noisy"][ch_subset]
    bins = auto_scores["bins"]  # The the windows that were evaluated.
    # We will label each segment by its start and stop time, with up to 3
    # digits before and 3 digits after the decimal place (1 ms precision).
    bin_labels = [f"{start:3.3f} – {stop:3.3f}" for start, stop in bins]

    # We store the data in a Pandas DataFrame. The seaborn heatmap function
    # we will call below will then be able to automatically assign the correct
    # labels to all axes.
    data_to_plot = pd.DataFrame(
        data=scores,
        columns=pd.Index(bin_labels, name="Time (s)"),
        index=pd.Index(ch_names, name="Channel"),
    )

    # First, plot the "raw" scores.
    fig, ax = plt.subplots(1, 2, figsize=(12, 8))
    fig.suptitle(
        f"Automated noisy channel detection: {ch_type}, subject sub-{subject}",
        fontsize=16,
        fontweight="bold",
    )
    sns.heatmap(data=data_to_plot,
                cmap="Reds",
                cbar_kws=dict(label="Score"),
                ax=ax[0])
    [
        ax[0].axvline(x, ls="dashed", lw=0.25, dashes=(25, 15), color="gray")
        for x in range(1, len(bins))
    ]
    ax[0].set_title("All Scores", fontweight="bold")

    # Now, adjust the color range to highlight segments that exceeded the limit.
    sns.heatmap(
        data=data_to_plot,
        vmin=np.nanmin(limits),  # bads in input data have NaN limits
        cmap="Reds",
        cbar_kws=dict(label="Score"),
        ax=ax[1],
    )
    [
        ax[1].axvline(x, ls="dashed", lw=0.25, dashes=(25, 15), color="gray")
        for x in range(1, len(bins))
    ]
    ax[1].set_title("Scores > Limit", fontweight="bold")

    # The figure title should not overlap with the subplots.
    fig.tight_layout(rect=[0, 0.03, 1, 0.95])
    fname = _construct_path([
        Path(outpath),
        f"sub-{subject}",
        "meg",
        f"noise_detection_sub-{subject}_{ch_type}.png",
    ])
    fig.savefig(fname)