Repository: mne-tools/mne-python

Issue:
`response_trig_shift` conflicts with `event_id` in `read_raw_brainvision`
**edited:**
This was brought up on the listserv by @bburle and is being hammered out now. One problem is that there is a conflict in `mne.io.read_raw_brainvision` when `response_trig_shift` is not `None` (default is 0) and `event_id` is a `dict`. 

We should set a constraint that response_trig_shift must be `None` when event_id is a `dict` otherwise there will be an error.



Discussion:
sorry but I don't see add_response_shift in the code base with git grep.

can you clarify the issue?

sorry, it's `response_trig_shift`, just edited above. what happens is that `response_trig_shift` looks `Response` type of marker message. It is expecting an integer to shift a given amount. If the user `Response` message is an annotation (str), there will be a conflict.

what do you propose?

To follow on Teon's comment, it is actually inconsistent to use both `event_id` and `response_trig_shift`: `response_trig_shift` is useful to disambiguate Stimulus and Response events that would have the same integer code (e.g. 'S 1' and 'R 1'). The use of  `event_id` removes any ambiguity. So, as Teon proposed, one should prevent the simultaneous use of both `event_id` and `response_trig_shift`, maybe, indeed by forcing  `response_trig_shift` to be `None` when `event_id` is defined. Issuing a warning would be useful so that the user is informed of the inconsistence. 
If this choice is made, the documentation should be updated accordingly. 
(hey, that's my first contribution to github ;-)

+1 for forcing response_trig_shift to None if event_id is defined. It
should raise a ValueError

I can try to do that (although probably not today...), if you agree to check a beginner's first steps ;-) 

+1 would be great to help out with your first contrib. afterwards we send a follow up email to the listserv for archive purpose on how the problem was addressed

I don't understand how response shift must be None if event_id is given. Couldn't it be that you have Rint and Sint type triggers for which you want to apply response shift, but also break_recording and fmri_onset triggers?

My feeling is that  if one uses `event_id` for some events, it is better to use it for all. In case you have `break_recording` or `fmri_onset`, given them a explicit code would make the link explicit. In your example, one could have:
`event_id = {'S 1': 1,'R 1': 11, 'break_recording': 100,'fmri_onset': 200}`
In such case, the use of `response_trig_shift` is useless, and actually replaced by the explicit mapping in `event_id`.

> My feeling is that if one uses event_id for some events, it is better to use it for all

My typical scenario is: I have 100 or 200 or so regular triggers, and 4 or so for which I need `event_id`. This was actually the scenario for which the functionality was conceived ...

I personally never use response_shift, but I don't think we should assume we'll never have a situation where somebody uses a nontrivial number of S and R triggers in the fMRI or such things.

In the eeglab trigger reading PR, I do it like this:

```
for t, latency in zip(types, latencies):
    event_code = event_id[t] if t in event_id else event_id_func(t)
    events.append([int(latency), 1, event_code])
```

Wouldn't that be a rather straight-forward solution?

> I don't understand how response shift must be None if event_id is given. Couldn't it be that you have Rint and Sint type triggers for which you want to apply response shift, but also break_recording and fmri_onset triggers?

the problem arises with marker events like this:

```
Mk16=Response,Left_Resp,3180,1,0
```

vs.

```
Mk10=Response,R255,6000,1,0
```

You're right, it could possibly be a great imposition if you have multiple triggers to have to specify them in the `event_id`. Are you often faced with both at the same time? The source of the issue is how `response_trig_shift` is applied, it assumes that any marker with `Response` will be Rint. One possible fix would be something like this: 

``` python
try:
    trigger = int(string[1:])
   trigger = trigger + response_trig_shift
except ValueError:
    pass
```

What about my suggestion above? E.g. something like

```
triggers_ = [event_id[t_] if t_ in event_id
             else t_[1:] + (response_trig_shit if t_[0] == "R" else 0)
             for t_ in triggers]
```

Or with `try` to catch ValueErrors and raise a warning that an event will be dropped.

Let's try to summarize the situation: If all triggers are `Sint` or `Rint`, extracting `int` and adding `response_trig_shit` if the triggers's type is `Response` works well. For abitrary triggers names (_e.g._, `LeftResp`), one needs `event_id` to translate such names into integers. In case of `event_id` and no `response_trig_shit`, this works well. The problem arises when triggers are arbitrary (as above) **and** `response_trig_shit` is not `None`. In this case, one tries to add `response_trig_shit` to a `None` object and the code fails. As it stands, even if one defines an `event_id`, setting `response_trig_shit` to something else than `None` will fail with arbitrary names, since the evaluation of `event_id` occurs after adding `response_trig_shit`. One simple solution would be to evaluate `event_id` before attempting to apply `response_trig_shit`. This is pretty similar to the proposition of @jona-sassenhagen, but the current code is a bit more complex than simply evaluating the first letter of the event `description`. I think code like that works (on line 200 of brainvision.py):
     if mdesc in event_id:
            trigger = event_id[mdesc]
        else:
            try:
                trigger = int(re.findall('[A-Za-z]_\s_?(\d+)',mdesc)[0])
            except IndexError:
                trigger = None
        if mtype.lower().startswith('response'):
            if response_trig_shift is not None:
                trigger += response_trig_shift
            else:
                trigger = None

@bburle , note your spelling error :) (I do that one all the time.)

To have your code appear as code on github, wrap it in three backticks on start and end.

Something like this?

```
if mdesc in event_id:
    trigger = event_id[mdesc]
else:
    try:
        trigger = int(re.findall('[A-Za-z]\s?(\d+)',mdesc)[0])
    except IndexError:
        trigger = None
    if mtype.lower().startswith('response'):
        if response_trig_shift is not None:
            trigger += response_trig_shift
    else:
        trigger = None
```

I think that should work. It could be coded a bit more pythonic, but I think it would cover the expectable usage cases in a comfortable manner.

I think we should consider raising warnings or possibly errors for dropped events though. Possibly, a high priority warning like "the following events were dropped: ..., use event_id to include them".

ok, seems like we are all on board and understand what's going on.

+1 warning and +.5 noting events dropped (some might be dropped intentionally).

just need a PR now :)

@bburle are you still on this?

I've been overloaded those weeks, and will still be for a couple of weeks more. i'll take care of that early march is that's not too late...

Sorry for that,

@bburle, are you able to get around to solving this?

@jona-sassenhagen do you have time to attempt a fix in the next week? If not I'll push to 0.13

I can (and should) attempt a fix (unless @bburle says he can do it), but we have deadlines upcoming, so no promises ... Please move to .13. I will PR later.

Will do, if you get it in for 0.12 great, if not, no problem from my end

Can you assign me Eric? (Or @teonbrooks )? :)

Sure, I have to add you to the org, accept the subscription (you should have an email?) and I can assign it to you

@jona-sassenhagen done


Resolving patch:
diff --git a/mne/io/brainvision/brainvision.py b/mne/io/brainvision/brainvision.py
--- a/mne/io/brainvision/brainvision.py
+++ b/mne/io/brainvision/brainvision.py
@@ -55,11 +55,12 @@ class RawBrainVision(_BaseRaw):
         triggers will be ignored. Default is 0 for backwards compatibility, but
         typically another value or None will be necessary.
     event_id : dict | None
-        The id of the event to consider. If None (default),
-        only stimulus events are added to the stimulus channel. If dict,
-        the keys will be mapped to trigger values on the stimulus channel
-        in addition to the stimulus events. Keys are case-sensitive.
-        Example: {'SyncStatus': 1; 'Pulse Artifact': 3}.
+        The id of special events to consider in addition to those that
+        follow the normal Brainvision trigger format ('SXXX').
+        If dict, the keys will be mapped to trigger values on the stimulus
+        channel. Example: {'SyncStatus': 1; 'Pulse Artifact': 3}. If None
+        or an empty dict (default), only stimulus events are added to the
+        stimulus channel. Keys are case sensitive.
     verbose : bool, str, int, or None
         If not None, override default verbose level (see mne.verbose).
 
@@ -170,10 +171,12 @@ def _read_vmrk_events(fname, event_id=None, response_trig_shift=0):
     fname : str
         vmrk file to be read.
     event_id : dict | None
-        The id of the event to consider. If dict, the keys will be mapped to
-        trigger values on the stimulus channel. Example:
-        {'SyncStatus': 1; 'Pulse Artifact': 3}. If empty dict (default),
-        only stimulus events are added to the stimulus channel.
+        The id of special events to consider in addition to those that
+        follow the normal Brainvision trigger format ('SXXX').
+        If dict, the keys will be mapped to trigger values on the stimulus
+        channel. Example: {'SyncStatus': 1; 'Pulse Artifact': 3}. If None
+        or an empty dict (default), only stimulus events are added to the
+        stimulus channel. Keys are case sensitive.
     response_trig_shift : int | None
         Integer to shift response triggers by. None ignores response triggers.
 
@@ -206,25 +209,38 @@ def _read_vmrk_events(fname, event_id=None, response_trig_shift=0):
 
     # extract event information
     items = re.findall("^Mk\d+=(.*)", mk_txt, re.MULTILINE)
-    events = []
+    events, dropped = list(), list()
     for info in items:
         mtype, mdesc, onset, duration = info.split(',')[:4]
         onset = int(onset)
         duration = (int(duration) if duration.isdigit() else 1)
-        try:
-            trigger = int(re.findall('[A-Za-z]*\s*?(\d+)', mdesc)[0])
-        except IndexError:
-            trigger = None
-
-        if mtype.lower().startswith('response'):
-            if response_trig_shift is not None:
-                trigger += response_trig_shift
-            else:
-                trigger = None
         if mdesc in event_id:
             trigger = event_id[mdesc]
+        else:
+            try:
+                trigger = int(re.findall('[A-Za-z]*\s*?(\d+)', mdesc)[0])
+            except IndexError:
+                trigger = None
+            if mtype.lower().startswith('response'):
+                if response_trig_shift is not None:
+                    trigger += response_trig_shift
+                else:
+                    trigger = None
         if trigger:
             events.append((onset, duration, trigger))
+        else:
+            if len(mdesc) > 0:
+                dropped.append(mdesc)
+
+    if len(dropped) > 0:
+        dropped = list(set(dropped))
+        examples = ", ".join(dropped[:5])
+        if len(dropped) > 5:
+            examples += ", ..."
+        warn("Currently, {0} trigger(s) will be dropped, such as [{1}]. "
+             "Consider using ``event_id`` to parse triggers that "
+             "do not follow the 'SXXX' pattern.".format(
+                 len(dropped), examples))
 
     events = np.array(events).reshape(-1, 3)
     return events
@@ -502,11 +518,12 @@ def read_raw_brainvision(vhdr_fname, montage=None,
         triggers will be ignored. Default is 0 for backwards compatibility, but
         typically another value or None will be necessary.
     event_id : dict | None
-        The id of the event to consider. If None (default),
-        only stimulus events are added to the stimulus channel. If dict,
-        the keys will be mapped to trigger values on the stimulus channel
-        in addition to the stimulus events. Keys are case-sensitive.
-        Example: {'SyncStatus': 1; 'Pulse Artifact': 3}.
+        The id of special events to consider in addition to those that
+        follow the normal Brainvision trigger format ('SXXX').
+        If dict, the keys will be mapped to trigger values on the stimulus
+        channel. Example: {'SyncStatus': 1; 'Pulse Artifact': 3}. If None
+        or an empty dict (default), only stimulus events are added to the
+        stimulus channel. Keys are case sensitive.
     verbose : bool, str, int, or None
         If not None, override default verbose level (see mne.verbose).
 


Test patch:
diff --git a/mne/io/brainvision/tests/test_brainvision.py b/mne/io/brainvision/tests/test_brainvision.py
--- a/mne/io/brainvision/tests/test_brainvision.py
+++ b/mne/io/brainvision/tests/test_brainvision.py
@@ -7,6 +7,7 @@
 
 import os.path as op
 import inspect
+import warnings
 
 from nose.tools import assert_equal, assert_raises, assert_true
 import numpy as np
@@ -30,6 +31,8 @@
 eeg_bin = op.join(data_dir, 'test_bin_raw.fif')
 eog = ['HL', 'HR', 'Vb']
 
+warnings.simplefilter('always')
+
 
 def test_brainvision_data_filters():
     """Test reading raw Brain Vision files
@@ -88,77 +91,98 @@ def test_events():
     tempdir = _TempDir()
 
     # check that events are read and stim channel is synthesized correcly
-    raw = read_raw_brainvision(vhdr_path, eog=eog, preload=True)
-    events = raw._get_brainvision_events()
-    assert_array_equal(events, [[487, 1, 253],
-                                [497, 1, 255],
-                                [1770, 1, 254],
-                                [1780, 1, 255],
-                                [3253, 1, 254],
-                                [3263, 1, 255],
-                                [4936, 1, 253],
-                                [4946, 1, 255],
-                                [6000, 1, 255],
-                                [6620, 1, 254],
-                                [6630, 1, 255]])
+    with warnings.catch_warnings(record=True) as w:
+        warnings.simplefilter('always')
+        raw = read_raw_brainvision(vhdr_path, eog=eog, preload=True)
+        events = raw._get_brainvision_events()
+        assert_array_equal(events, [[487, 1, 253],
+                                    [497, 1, 255],
+                                    [1770, 1, 254],
+                                    [1780, 1, 255],
+                                    [3253, 1, 254],
+                                    [3263, 1, 255],
+                                    [4936, 1, 253],
+                                    [4946, 1, 255],
+                                    [6000, 1, 255],
+                                    [6620, 1, 254],
+                                    [6630, 1, 255]])
+        assert_equal(len(w), 1)  # for dropping Sync & R255 events
 
     # check that events are read and stim channel is synthesized correcly and
     # response triggers are shifted like they're supposed to be.
-    raw = read_raw_brainvision(vhdr_path, eog=eog, preload=True,
-                               response_trig_shift=1000)
-    events = raw._get_brainvision_events()
-    assert_array_equal(events, [[487, 1, 253],
-                                [497, 1, 255],
-                                [1770, 1, 254],
-                                [1780, 1, 255],
-                                [3253, 1, 254],
-                                [3263, 1, 255],
-                                [4936, 1, 253],
-                                [4946, 1, 255],
-                                [6000, 1, 1255],
-                                [6620, 1, 254],
-                                [6630, 1, 255]])
+    with warnings.catch_warnings(record=True) as w:
+        warnings.simplefilter('always')
+        raw = read_raw_brainvision(vhdr_path, eog=eog, preload=True,
+                                   response_trig_shift=1000)
+        events = raw._get_brainvision_events()
+        assert_array_equal(events, [[487, 1, 253],
+                                    [497, 1, 255],
+                                    [1770, 1, 254],
+                                    [1780, 1, 255],
+                                    [3253, 1, 254],
+                                    [3263, 1, 255],
+                                    [4936, 1, 253],
+                                    [4946, 1, 255],
+                                    [6000, 1, 1255],
+                                    [6620, 1, 254],
+                                    [6630, 1, 255]])
+        assert_equal(len(w), 1)  # for dropping Sync & R255 events
 
     # check that events are read and stim channel is synthesized correcly and
     # response triggers are ignored.
-    raw = read_raw_brainvision(vhdr_path, eog=eog, preload=True,
-                               response_trig_shift=None)
-    events = raw._get_brainvision_events()
-    assert_array_equal(events, [[487, 1, 253],
-                                [497, 1, 255],
-                                [1770, 1, 254],
-                                [1780, 1, 255],
-                                [3253, 1, 254],
-                                [3263, 1, 255],
-                                [4936, 1, 253],
-                                [4946, 1, 255],
-                                [6620, 1, 254],
-                                [6630, 1, 255]])
+    with warnings.catch_warnings(record=True) as w:
+        warnings.simplefilter('always')
+        raw = read_raw_brainvision(vhdr_path, eog=eog, preload=True,
+                                   response_trig_shift=None)
+        events = raw._get_brainvision_events()
+        assert_array_equal(events, [[487, 1, 253],
+                                    [497, 1, 255],
+                                    [1770, 1, 254],
+                                    [1780, 1, 255],
+                                    [3253, 1, 254],
+                                    [3263, 1, 255],
+                                    [4936, 1, 253],
+                                    [4946, 1, 255],
+                                    [6620, 1, 254],
+                                    [6630, 1, 255]])
+        assert_equal(len(w), 1)  # for dropping Sync & R255 events
+
     # check that events are read properly when event_id is specified for
     # auxiliary events
-    raw = read_raw_brainvision(vhdr_path, eog=eog, preload=True,
-                               response_trig_shift=None,
-                               event_id={'Sync On': 5})
-    events = raw._get_brainvision_events()
-    assert_array_equal(events, [[487, 1, 253],
-                                [497, 1, 255],
-                                [1770, 1, 254],
-                                [1780, 1, 255],
-                                [3253, 1, 254],
-                                [3263, 1, 255],
-                                [4936, 1, 253],
-                                [4946, 1, 255],
-                                [6620, 1, 254],
-                                [6630, 1, 255],
-                                [7630, 1, 5]])
+    with warnings.catch_warnings(record=True) as w:
+        warnings.simplefilter('always')
+        raw = read_raw_brainvision(vhdr_path, eog=eog, preload=True,
+                                   response_trig_shift=None,
+                                   event_id={'Sync On': 5})
+        events = raw._get_brainvision_events()
+        assert_array_equal(events, [[487, 1, 253],
+                                    [497, 1, 255],
+                                    [1770, 1, 254],
+                                    [1780, 1, 255],
+                                    [3253, 1, 254],
+                                    [3263, 1, 255],
+                                    [4936, 1, 253],
+                                    [4946, 1, 255],
+                                    [6620, 1, 254],
+                                    [6630, 1, 255],
+                                    [7630, 1, 5]])
+        assert_equal(len(w), 1)  # parsing Sync event, missing R255
 
     assert_raises(TypeError, read_raw_brainvision, vhdr_path, eog=eog,
                   preload=True, response_trig_shift=0.1)
     assert_raises(TypeError, read_raw_brainvision, vhdr_path, eog=eog,
                   preload=True, response_trig_shift=np.nan)
 
-    mne_events = find_events(raw, stim_channel='STI 014')
-    assert_array_equal(events[:, [0, 2]], mne_events[:, [0, 2]])
+    # Test that both response_trig_shit and event_id can be set
+    with warnings.catch_warnings(record=True) as w:
+        warnings.simplefilter('always')
+        read_raw_brainvision(vhdr_path, eog=eog, preload=False,
+                             response_trig_shift=100,
+                             event_id={'Sync On': 5})
+
+        mne_events = find_events(raw, stim_channel='STI 014')
+        assert_array_equal(events[:, [0, 2]], mne_events[:, [0, 2]])
+        assert_equal(len(w), 0)  # parsing the Sync event
 
     # modify events and check that stim channel is updated
     index = events[:, 2] == 255