Repository: mne-tools/mne-python

Issue:
Not all events included after mne.io.read_raw_eeglab()
After reading in a dataset from eeglab, I notice not all events have been read.

I do:
raw = mne.io.read_raw_eeglab(raw, event_id=ev_id, preload=True)                             
raw.drop_channels(['L_GAZE_X','L_GAZE_Y']) # Drop eye tracker channels
montage = mne.channels.read_montage(mfile)                              
mne.channels.montage._set_montage(raw.info, montage)
evs = mne.find_events(raw,shortest_event = 0,stim_channel = 'STI 014')

With ev_id containing all possible types, as found by unique({EEG.event.type}) in Matlab

the resulting evs is 7 positions shorter than length(EEG.event) in Matlab.

It is NOT that certain types (from EEG.event.type) are not being read in at all, rather that not all events of all types are read (so I'm missing 4 of one type, 1 each of three other types). Any idea what might be the cause of this?

Apart from the obvious potential problems of missing triggers, I want to do something similar to #3837 but I can not line up the information between EEGlab and MNE if not all triggers are imported.



Discussion:
I assume this has something to do with how the event ids are mapped to integers. You could run ``read_raw_eeglab`` with ``verbose=True`` to see if it throws any warnings.
Thank you for the suggestion, I just tried this but no warnings are issued.

raw = mne.io.read_raw_eeglab(raw, event_id=ev_id, preload=True, verbose=True)

results in:

Reading /Volumes/(...)/ss0018_ET_EEG.fdt
Reading 0 ... 2582049  =      0.000 ...  2582.049 secs...

edit:
Perhaps it is useful to know that (as I mentioned somewhat cryptically before) I am manually specifying the mapping of all types of events that are in the data (according to EEGlab), to integers, by specifying them in event_id.
Hmm. That's surprising since there is a check for ``if len(events) < len(types):`` and it should issue a warning.

I would start debugging by checking which events exactly are excluded and where exactly are they dropped. If you can share the data I can also take a look.
@jona-sassenhagen perhaps you can help? It's part of the code you wrote

@timmidee can you share the file + small code to facilitate debugging. Also, take a look at `event_id_func`. Maybe that will help


I got some IRL help from @jona-sassenhagen on this. It's not resolved yet, but he has a copy of the file.

As far as I understand this, the triggers in the .set file seem to be ok and to be read correctly by the internal functions in mne.io.eeglab.eeblab, but somehow they seem to end up in the stimulus channel incorrectly, or to be incorrectly retrieved by mne.find_events().

For now I can read out the events with a work around (in case others have this problem):
`mat =  io.loadmat(raws[0], struct_as_record=False, squeeze_me=True, uint16_codec=None)['EEG']
evs = mne.io.eeglab.eeglab._read_eeglab_events(mat,ev_id)`

This gives me evs with all events and that lines up with the custom event info that I want to align from the eeglab events structure.
this is partly related to `original_events` idea from #3837
Ok, I think I've found the problem.

It's due to perfectly overlapping events. `events` is first constructed correctly by _read_eeglab_events. But then, by the conversion to a stim channel, and then by reconstructing an events array from the stim channel, events with identical time points are lost.

Proposed solution:

- check if len(set(latencies)) == len(latencies) and if not, warn
- @timmidee , the problem is not trivial to solve because we simply cannot store two simultaneous events in the stim channel. It's probably easiest to work with the hand-read events array (with _read_eeglab_events) for now


> On 27 Jan 2017, at 14:35, Mainak Jas <notifications@github.com> wrote:
> 
> @jona-sassenhagen <https://github.com/jona-sassenhagen> perhaps you can help? It's part of the code you wrote
> 
> @timmidee <https://github.com/timmidee> can you share the file + small code to facilitate debugging. Also, take a look at event_id_func. Maybe that will help
> 
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub <https://github.com/mne-tools/mne-python/issues/3938#issuecomment-275666318>, or mute the thread <https://github.com/notifications/unsubscribe-auth/AEHyItaF9FWKnTypCs3Sgc9f6JoAVvArks5rWfKvgaJpZM4LvQOC>.
> 


So what's the plan - I think we discussed this at the sprint, but I forgot the outcome ... How about making _read_eeglab_events public + a warning linking to it?
Long term I think one way to tackle both this problem and other issues, is if there is additional information one can store with an event. The stim channel can carry the single event that "survives", the overlapping events can be offloaded as "additional information" without getting completely lost.

Such a solution would require a warning and perhaps be much more elaborate, but it would not require making additional functions public. Nor would it be necessary for the user to handle the events separate from the objects and constantly adapt them along with the objects (as I am constantly doing myself at the moment, but that's a different problem from just reading in the events)

May I, by the way, be so bold to ask why the events "time line" has to take the form of a data channel at all?

As an intermediate solution I would be in favour (as far as I have a say in this) of @jona-sassenhagen 's solution of having _read_eeglab_events() available in one form or another. That way at least the events are accessible and the user can do with them as they like.
> Long term I think one way to tackle both this problem and other issues, is if there is additional information one can store with an event... [can I] ask why the events "time line" has to take the form of a data channel at all?

Yeah at the recent code sprint we had some discussions along these lines (see also #3837, #4125). One problem is that our output format (FIF) doesn't have a good way of storing arbitrary event-related information. The events get stored as a data channel because that's how FIF stores events, and until recently, the drawbacks of this approach haven't been so glaringly obvious :) @jona-sassenhagen recently came up with

In expanding our own support for arbitrary event-related information coding, we risk recreating an inferior version of a Pandas DataFrame, which wouldn't be great. But at the same time, telling people "use this other package and have parallel files" isn't entirely satisfying, and also doesn't work well with e.g. `read_raw_eeglab`.

In the meantime, I agree with @jona-sassenhagen, @timmidee, and others (probably also @mmagnuski) that for `RawEEGLAB` we can make `read_eeglab_events` public, or just expose it as a property like `raw.eeglab_events` or so. The critical thing is that we need to make it clear that such data will not be saved with `.save()` and so people should save it elsewhere themselves if they need it. This would allow us to punt on the issue of richer events for a little bit longer (hopefully in a future-compatible way) while not holding up @timmidee and @nbara.

Is there some volunteer familiar with EEGLAB for trying this? @jona-sassenhagen?
Yup, me or @jasmainak . IIRC I wrote the function anyways.

@Eric89GXL see the last reply here: https://github.com/mne-tools/mne-python/issues/4124 for "rich predictor" data set.
Maybe I'm missing something, but overlapping events can now be handled using `annotation` objects. Why do we not make use of that?
annotations and events are different concepts.
Events refer to paradigm why annotation refer to the data:
- presence of blink / movement artifact /
- sleep spindle, epileptic spike etc.


Resolving patch:
diff --git a/mne/io/__init__.py b/mne/io/__init__.py
--- a/mne/io/__init__.py
+++ b/mne/io/__init__.py
@@ -40,7 +40,7 @@
 from .fiff import read_raw_fif
 from .nicolet import read_raw_nicolet
 from .artemis123 import read_raw_artemis123
-from .eeglab import read_raw_eeglab, read_epochs_eeglab
+from .eeglab import read_raw_eeglab, read_epochs_eeglab, read_events_eeglab
 
 # for backward compatibility
 from .fiff import Raw
diff --git a/mne/io/eeglab/__init__.py b/mne/io/eeglab/__init__.py
--- a/mne/io/eeglab/__init__.py
+++ b/mne/io/eeglab/__init__.py
@@ -2,4 +2,4 @@
 
 # Author: Mainak Jas <mainak.jas@telecom-paristech.fr>
 
-from .eeglab import read_raw_eeglab, read_epochs_eeglab
+from .eeglab import read_raw_eeglab, read_epochs_eeglab, read_events_eeglab
diff --git a/mne/io/eeglab/eeglab.py b/mne/io/eeglab/eeglab.py
--- a/mne/io/eeglab/eeglab.py
+++ b/mne/io/eeglab/eeglab.py
@@ -347,8 +347,8 @@ def __init__(self, input_fname, montage, eog=(), event_id=None,
         info['chs'].append(stim_chan)
         info._update_redundant()
 
-        events = _read_eeglab_events(eeg, event_id=event_id,
-                                     event_id_func=event_id_func)
+        events = read_events_eeglab(eeg, event_id=event_id,
+                                    event_id_func=event_id_func)
         self._create_event_ch(events, n_samples=eeg.pnts)
 
         # read the data
@@ -381,6 +381,15 @@ def __init__(self, input_fname, montage, eog=(), event_id=None,
 
     def _create_event_ch(self, events, n_samples=None):
         """Create the event channel."""
+        n_dropped = len(events[:, 0]) - len(set(events[:, 0]))
+        if n_dropped > 0:
+            warn(str(n_dropped) + " events will be dropped because they "
+                 "occur on the same time sample as another event. "
+                 "`mne.io.Raw` objects store events on an event channel, "
+                 "which cannot represent two events on the same sample. You "
+                 "can extract the original event structure using "
+                 "`mne.io.eeglab.read_events_eeglab`. Then, you can e.g. "
+                 "subset the extracted events for constructing epochs.")
         if n_samples is None:
             n_samples = self.last_samp - self.first_samp + 1
         events = np.array(events, int)
@@ -576,19 +585,75 @@ def __init__(self, input_fname, events=None, event_id=None, tmin=0,
         logger.info('Ready.')
 
 
-def _read_eeglab_events(eeg, event_id=None, event_id_func='strip_to_integer'):
-    """Create events array from EEGLAB structure.
+def read_events_eeglab(eeg, event_id=None, event_id_func='strip_to_integer',
+                       uint16_codec=None):
+    r"""Create events array from EEGLAB structure.
 
     An event array is constructed by looking up events in the
     event_id, trying to reduce them to their integer part otherwise, and
     entirely dropping them (with a warning) if this is impossible.
     Returns a 1x3 array of zeros if no events are found.
+
+    Usually, the EEGLAB readers will automatically construct event information
+    for you. However, the reader for continuous data stores event information
+    in the stimulus channel, which can only code one event per time sample.
+    Use this function if your EEGLAB file has events happening at the
+    same time (sample) point to manually create an events array.
+
+    Parameters
+    ----------
+    eeg : str | object
+        The EEGLAB object from which events are read in.
+        If str, path to the (EEGLAB) .set file.
+        Else, the "EEG" field of a MATLAB EEGLAB structure as read in by
+        scipy.io.loadmat.
+    event_id : dict | None
+        The ids of the events to consider. If None (default), an empty dict is
+        used and ``event_id_func`` (see below) is called on every event value.
+        If dict, the keys will be mapped to trigger values on the stimulus
+        channel and only keys not in ``event_id`` will be handled by
+        ``event_id_func``. Keys are case-sensitive.
+        Example::
+
+            {'SyncStatus': 1; 'Pulse Artifact': 3}
+
+    event_id_func : None | str | callable
+        What to do for events not found in ``event_id``. Must take one ``str``
+        argument and return an ``int``. If string, must be 'strip-to-integer',
+        in which case it defaults to stripping event codes such as "D128" or
+        "S  1" of their non-integer parts and returns the integer.
+        If the event is not in the ``event_id`` and calling ``event_id_func``
+        on it results in a ``TypeError`` (e.g. if ``event_id_func`` is
+        ``None``) or a ``ValueError``, the event is dropped.
+    uint16_codec : str | None
+        If your \*.set file contains non-ascii characters, sometimes reading
+        it may fail and give rise to error message stating that "buffer is
+        too small". ``uint16_codec`` allows to specify what codec (for example:
+        'latin1' or 'utf-8') should be used when reading character arrays and
+        can therefore help you solve this problem.
+
+    Returns
+    -------
+    events : array, shape = (n_events, 3)
+        All events that were found. The first column contains the event time
+        in samples and the third column contains the event id. The center
+        column is zero.
+
+    See Also
+    --------
+    find_events : Extract events from a stim channel. Note that stim channels
+        can only code for one event per time point.
     """
     if event_id_func is 'strip_to_integer':
         event_id_func = _strip_to_integer
     if event_id is None:
         event_id = dict()
 
+    if isinstance(eeg, string_types):
+        from scipy import io
+        eeg = io.loadmat(eeg, struct_as_record=False, squeeze_me=True,
+                         uint16_codec=uint16_codec)['EEG']
+
     if isinstance(eeg.event, np.ndarray):
         types = [str(event.type) for event in eeg.event]
         latencies = [event.latency for event in eeg.event]


Test patch:
diff --git a/mne/io/eeglab/tests/test_eeglab.py b/mne/io/eeglab/tests/test_eeglab.py
--- a/mne/io/eeglab/tests/test_eeglab.py
+++ b/mne/io/eeglab/tests/test_eeglab.py
@@ -14,7 +14,7 @@
 from mne import write_events, read_epochs_eeglab, Epochs, find_events
 from mne.io import read_raw_eeglab
 from mne.io.tests.test_raw import _test_raw_reader
-from mne.io.eeglab.eeglab import _read_eeglab_events
+from mne.io.eeglab.eeglab import read_events_eeglab
 from mne.datasets import testing
 from mne.utils import _TempDir, run_tests_if_main, requires_version
 
@@ -77,9 +77,9 @@ def test_io_set():
                      squeeze_me=True)['EEG']
     for event in eeg.event:  # old version allows integer events
         event.type = 1
-    assert_equal(_read_eeglab_events(eeg)[-1, -1], 1)
+    assert_equal(read_events_eeglab(eeg)[-1, -1], 1)
     eeg.event = eeg.event[0]  # single event
-    assert_equal(_read_eeglab_events(eeg)[-1, -1], 1)
+    assert_equal(read_events_eeglab(eeg)[-1, -1], 1)
 
     with warnings.catch_warnings(record=True) as w:
         warnings.simplefilter('always')
@@ -121,6 +121,27 @@ def test_io_set():
     read_raw_eeglab(input_fname=one_event_fname, montage=montage,
                     event_id=event_id, preload=True)
 
+    # test overlapping events
+    overlap_fname = op.join(temp_dir, 'test_overlap_event.set')
+    io.savemat(overlap_fname, {'EEG':
+               {'trials': eeg.trials, 'srate': eeg.srate,
+                'nbchan': eeg.nbchan, 'data': 'test_overlap_event.fdt',
+                'epoch': eeg.epoch, 'event': [eeg.event[0], eeg.event[0]],
+                'chanlocs': eeg.chanlocs, 'pnts': eeg.pnts}})
+    shutil.copyfile(op.join(base_dir, 'test_raw.fdt'),
+                    op.join(temp_dir, 'test_overlap_event.fdt'))
+    event_id = {'rt': 1, 'square': 2}
+    with warnings.catch_warnings(record=True) as w:
+        warnings.simplefilter('always')
+        raw = read_raw_eeglab(input_fname=overlap_fname,
+                              montage=montage, event_id=event_id,
+                              preload=True)
+    assert_equal(len(w), 1)  # one warning for the dropped event
+    events_stimchan = find_events(raw)
+    events_read_events_eeglab = read_events_eeglab(overlap_fname, event_id)
+    assert_true(len(events_stimchan) == 1)
+    assert_true(len(events_read_events_eeglab) == 2)
+
     # test reading file with one channel
     one_chan_fname = op.join(temp_dir, 'test_one_channel.set')
     io.savemat(one_chan_fname, {'EEG':