Repository: mne-tools/mne-python

Issue:
eeglab latency to event without sfreq
when working on annotations to events I realize that the eeglab reader
convert `eeg.latency` to sample index with `int(latency)`

see: https://github.com/mne-tools/mne-python/blob/master/mne/io/eeglab/eeglab.py#L697

reading https://sccn.ucsd.edu/wiki/Chapter_03:_Event_Processing it seems that latency is in seconds to converting to events would require `sfreq`

@jona-sassenhagen @smgutstein  @mmagnuski thoughts? is this a bug?


Discussion:
have a look at https://github.com/mne-tools/mne-python/pull/4936
@agramfort - latency is stored in samples in eeglab. But then different guis and functions use sometimes ms, sometimes s and sometimes samples.
The only issue might be matlab vs python indexing (if that is not dealt with when reading latencies).
And it seems that it is not taken care of in _read_annotations_eeglab:
`onset = [event.latency for event in eeg.event]`

event.latency has digits after the commas in our test file so it does not suggest it's samples but more seconds.

can you investigate with one of your files?
The file might have been downsampled - then events are then represented as happening between the samples in some cases. I can test that later today but I am almost 100% sure these are samples. But I'll check indexing offsets.
please have a look as there is something currently maybe broken.

I confirm that eeglab latency is in samples and that there is a one sample error caused by 0-based indexing in python and 1-based indexing in matlab. I can open a PR in the evening.

As I earlier mentioned - after resampling eeglab event latencies are often between the samples so `latency` becomes a float, not int. We should take care of this too - if currently we just cast these values to int, we can have other single sample errors. When `latency` is for example 2015.8 and it is cast to 2015 integer (or rather - 2014, given indexing differences between matlab and python :) ).

Resolving patch:
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
@@ -670,6 +670,10 @@ def read_events_eeglab(eeg, event_id=None, event_id_func='strip_to_integer',
         logger.info('No events found, returning empty stim channel ...')
         return np.zeros((0, 3))
 
+    if (latencies < 0).any():
+        raise ValueError('At least one event sample index is negative. Please'
+                         ' check if EEG.event.sample values are correct.')
+
     not_in_event_id = set(x for x in types if x not in event_id)
     not_purely_numeric = set(x for x in not_in_event_id if not x.isdigit())
     no_numbers = set([x for x in not_purely_numeric
@@ -717,7 +721,7 @@ def _read_annotations_eeglab(eeg):
         description = []
     elif isinstance(eeg.event, np.ndarray):
         description = [str(event.type) for event in eeg.event]
-        onset = [event.latency for event in eeg.event]
+        onset = [event.latency - 1 for event in eeg.event]
         if (len(onset) > 0) and hasattr(eeg.event[0], 'duration'):
             duration = [event.duration for event in eeg.event]
         else:
@@ -725,7 +729,7 @@ def _read_annotations_eeglab(eeg):
     else:
         # only one event - TypeError: 'mat_struct' object is not iterable
         description = [str(eeg.event.type)]
-        onset = [eeg.event.latency]
+        onset = [eeg.event.latency - 1]
         duration = getattr(eeg.event, 'duration', np.zeros(1))
 
     return Annotations(onset=onset, duration=duration, description=description)


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
@@ -3,6 +3,7 @@
 #
 # License: BSD (3-clause)
 
+from copy import deepcopy
 from distutils.version import LooseVersion
 import os.path as op
 import shutil
@@ -119,10 +120,28 @@ def test_io_set():
                 'epoch': eeg.epoch, 'event': eeg.event[0],
                 'chanlocs': eeg.chanlocs, 'pnts': eeg.pnts}})
     shutil.copyfile(op.join(base_dir, 'test_raw.fdt'),
-                    op.join(temp_dir, 'test_one_event.fdt'))
+                    one_event_fname.replace('.set', '.fdt'))
     event_id = {eeg.event[0].type: 1}
-    read_raw_eeglab(input_fname=one_event_fname, montage=montage,
-                    event_id=event_id, preload=True)
+    test_raw = read_raw_eeglab(input_fname=one_event_fname, montage=montage,
+                               event_id=event_id, preload=True)
+
+    # test that sample indices are read python-wise (zero-based)
+    assert find_events(test_raw)[0, 0] == round(eeg.event[0].latency) - 1
+
+    # test negative event latencies
+    negative_latency_fname = op.join(temp_dir, 'test_negative_latency.set')
+    evnts = deepcopy(eeg.event[0])
+    evnts.latency = 0
+    io.savemat(negative_latency_fname, {'EEG':
+               {'trials': eeg.trials, 'srate': eeg.srate,
+                'nbchan': eeg.nbchan, 'data': 'test_one_event.fdt',
+                'epoch': eeg.epoch, 'event': evnts,
+                'chanlocs': eeg.chanlocs, 'pnts': eeg.pnts}})
+    shutil.copyfile(op.join(base_dir, 'test_raw.fdt'),
+                    negative_latency_fname.replace('.set', '.fdt'))
+    event_id = {eeg.event[0].type: 1}
+    assert_raises(ValueError, read_raw_eeglab, montage=montage, preload=True,
+                  event_id=event_id, input_fname=negative_latency_fname)
 
     # test overlapping events
     overlap_fname = op.join(temp_dir, 'test_overlap_event.set')
@@ -132,7 +151,7 @@ def test_io_set():
                 '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'))
+                    overlap_fname.replace('.set', '.fdt'))
     event_id = {'rt': 1, 'square': 2}
     with warnings.catch_warnings(record=True) as w:
         warnings.simplefilter('always')