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
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
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
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)
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)
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)
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)