events shift after raw.add_channels

If you have a question or issue with MNE-Python, please include the following info:

  • MNE-Python version: 0.23.0
  • operating system: windows

Hi!

I found out that the events shift after add_channels operations. The processing includes cropping the EEG signal according to the experimental paradigm with the following codes. The onset point is stored in the alighmentInfo. I plot the eeg_raw before and after cropping to see whether after cropping, the events still match the signals. And the answer is yes. It seems that events from annotations do not change, what changes is the eeg_raw. first_time.

# read and cut EEG #
eeg_raw = mne.io.read_raw_eeglab(eeg_fName, preload=True)
eeg_raw.plot()
eeg_raw.set_montage('standard_1020')
eeg_raw.crop(tmin=alignmentInfo.loc[(alignmentInfo['sessionIdx'] == session_idx) &
                                    (alignmentInfo['contraction_type'] == 'iVC'),
                                    'EEG'].values[0] / eeg_raw.info['sfreq'])
eeg_raw.plot()
events, events_id = mne.events_from_annotations(eeg_raw)

Then I would add EMG channels into the dataset. Basically, this step includes reading, cropping, and filtering the EMG signals.

# read and cut EMG 
ch_types = ['emg'] * 8  
emg_ch_names = ['FDS', 'FCU', 'FCR', 'ECU', 'ECRL', 'BBS', 'TBL', 'LD']
info = mne.create_info(ch_names=emg_ch_names, sfreq=sfreq_emg, ch_types=ch_types)
emg_data = pd.read_csv(emg_fName, header=None, skiprows=3,
                       sep=' ', usecols=np.arange(0, 8), skipfooter=0, engine='python')
emg_data = emg_data.iloc[alignmentInfo.loc[(alignmentInfo['sessionIdx'] == session_idx) &
                                           (alignmentInfo['contraction_type'] == 'iVC'),
                                           'EMG'].values[0]:, :].values
emg_data = emg_data.T / 1e6  # uV to V
emg_raw = mne.io.RawArray(emg_data, info)
emg_raw.filter(l_freq=10, h_freq=250, picks='all')
emg_raw.resample(sfreq=sfreq_eeg)

# data alighment 
t_end = min(len(eeg_raw), len(emg_raw)) / sfreq_eeg - 1/sfreq_eeg  # find the shortest length of EEG and EMG
emg_raw.crop(tmax=t_end)
eeg_raw.crop(tmax=t_end)
emg_raw.info['highpass'] = eeg_raw.info['highpass']
# In reality, EEGs are bandpassed from 1-45Hz, EMGs are bandpassed from 10-200Hz
emg_raw.info['lowpass'] = eeg_raw.info['lowpass']
eeg2merge_raw = eeg_raw.copy()

Here’s the problematic function.
aligned_raw = eeg2merge_raw.add_channels([emg_raw])
After doing add_channels, the events no longer match the signals. I look deeper into what changed. It seems that the aligned_raw.first_time is as same as eeg_raw.first_time. However, mne.events_from_annotations(aligned_raw) = mne.events_from_annotations(aligned_raw) + aligned_raw.first_time.

I think this could be misleading for the users of MNE as the changes is not presumably intended. At the end, I resolve the issue with the following codes.

# calibrate annotation
onsets = events[:, 0] / sfreq_eeg - aligned_raw.first_time
durations = events[:, 1]
descriptions = events[:, 2]
annotations = mne.Annotations(onsets, durations, descriptions) 
aligned_raw.set_annotations(annotations)

I would like to know what happens when we do the add_channels operation.

Hello @GanshengT, do you think you could produce a minimal working example (MWE) based on the MNE sample dataset (or any other publicly available data – could also be your own dataset if you are okay with sharing it) to demonstrate the issue? I had a bit of trouble following what exactly you’re doing and which lines are important and which aren’t. An MWE would help others better understand what and where things are going wrong exactly.

Thank you,
Richard

Of course, here is an MWE. I uploaded files used to reproduce the same results. Please download them at CM-graph/sample_data/subj61 at master · GanshengT/CM-graph · GitHub.

The first thing I do is reading the EEG data.

import pandas as pd
import numpy as np
import mne
eeg_fName = 'subj61/EEG/subj61_iVC_s01.set'  # please define fName accordingly
emg_fName = 'subj61/EMG/subj61_iVC_s01.txt'
alignmentInfo_fName = 'subj61/subj61_alignmentInfo.txt'
eeg_raw = mne.io.read_raw_eeglab(eeg_fName, preload=True)
eeg_raw.set_montage('standard_1020')
eeg_raw.filter(l_freq=1, h_freq=45)
eeg_raw.plot(n_channels=5, scalings=1e-4, duration=15, start=25)

Here is what the raw EEG data looks like.

The next thing I do is cropping the raw EEG data based on the alignment information. In fact, the EEG begins few seconds prior to the EMG recording.

alignmentInfo = pd.read_csv(alignmentInfo_fName, skiprows=0, sep=',', engine='python')
eeg_raw.crop(tmin=alignmentInfo.loc[(alignmentInfo['sessionIdx'] == 's01') &
                                    (alignmentInfo['contraction_type'] == 'iVC'),
                                    'EEG'].values[0] / eeg_raw.info['sfreq'])
eeg_raw.plot(n_channels=5, scalings=1e-4, duration=15, start=25)

And here is the result. So far so good, the events and the signals were shifted forward simultaneously.

When I concatenate the EEG data and the EMG data, the problem occurs.

ch_types = ['emg'] * 8 
emg_ch_names = ['FDS', 'FCU', 'FCR', 'ECU', 'ECRL', 'BBS', 'TBL', 'LD']
info = mne.create_info(ch_names=emg_ch_names, sfreq=1000, ch_types=ch_types)
emg_data = pd.read_csv(emg_fName, header=None, skiprows=3,
                       sep=' ', usecols=np.arange(0, 8), skipfooter=0, engine='python')
emg_data = emg_data.iloc[alignmentInfo.loc[(alignmentInfo['sessionIdx'] == 's01') &
                                           (alignmentInfo['contraction_type'] == 'iVC'),
                                           'EMG'].values[0]:, :].values
emg_data = emg_data.T / 1e6  # uV to V
emg_raw = mne.io.RawArray(emg_data, info)
emg_raw.filter(l_freq=10, h_freq=250, picks='all')
emg_raw.resample(sfreq=500)
t_end = min(len(eeg_raw), len(emg_raw)) / 500 - 1 / 500  # find the shortest length of EEG and EMG
emg_raw.crop(tmax=t_end)
eeg_raw.crop(tmax=t_end)
emg_raw.info['highpass'] = eeg_raw.info['highpass']
# In reality, EEGs are bandpassed from 1-45Hz, EMGs are bandpassed from 10-200Hz
emg_raw.info['lowpass'] = eeg_raw.info['lowpass']
eeg2merge_raw = eeg_raw.copy()
aligned_raw = eeg2merge_raw.add_channels([emg_raw])
aligned_raw.plot(n_channels=5, scalings=1e-4, duration=15, start=25)

As shown in the figure below, we see the data (signals) were shifted forwarded as expected. But the events did not.

BTW, I also tested with mne dataset. But this time everything works fine. I wonder whether there is a problem with data imported from EEGLAB?
Here is an MWE.

from mne.io import concatenate_raws, read_raw_edf
from mne.datasets import eegbci
import mne
%matplotlib qt
sfreq=160
raw_fnames = eegbci.load_data(1, 1)
raws = [read_raw_edf(f, preload=True) for f in raw_fnames]
raw = concatenate_raws(raws)
# define 2 events
onsets = [4, 8]
durations = [0, 0]
descriptions = ['event1', 'event2']
annotations = mne.Annotations(onsets, durations, descriptions) 
raw.set_annotations(annotations)
raw.plot(n_channels=5, scalings=1e-4, duration=10)

emg_data = raw.get_data()[1:3, :]  # define 2 emg chs
ch_types = ['emg'] * 2  
emg_ch_names = ['emg1', 'emg2']
info = mne.create_info(ch_names=emg_ch_names, sfreq=sfreq, ch_types=ch_types)
emg_raw = mne.io.RawArray(emg_data, info)
raw.crop(tmin=3)
raw.plot(n_channels=5, scalings=1e-4, duration=10)

# align eeg and emg data
t_end = min(len(raw),emg_data.shape[1]) / sfreq - 1 / sfreq
emg_raw.crop(tmax=t_end)
raw.crop(tmax=t_end)
eeg_raw = raw.copy()
aligned_raw = eeg_raw.add_channels([emg_raw])
aligned_raw.plot(n_channels=5, scalings=1e-4, duration=10)