Add support for offsets for arbitrary BrainVision marker types (#5391)

* add trig_shift_by_type parameter for read_raw_brainvision and deprecate response_trig_shift

* change formatting for docs
This commit is contained in:
Henrich Kolkhorst
2018-08-03 22:30:25 +02:00
committed by Alexandre Gramfort
parent b9c712b237
commit 3d08007b75
4 changed files with 133 additions and 36 deletions
+2
View File
@@ -33,6 +33,8 @@ Changelog
- Add support for reading MATLAB ``v7.3+`` files in :func:`mne.io.read_raw_eeglab` and :func:`mne.read_epochs_eeglab` via `pymatreader`_ by `Steven Gutstein`_, `Eric Larson`_, and `Thomas Hartmann`_
- Add `trig_shift_by_type` parameter in :func:`mne.io.read_raw_brainvision` to allow to specify offsets for arbitrary marker types by `Henrich Kolkhorst`_
Bug
~~~
+72 -17
View File
@@ -14,6 +14,7 @@
import os
import os.path as op
import re
import warnings
from datetime import datetime
from math import modf
@@ -60,9 +61,11 @@ class RawBrainVision(BaseRaw):
If False, data are not read until save.
response_trig_shift : int | None
An integer that will be added to all response triggers when reading
events (stimulus triggers will be unaffected). If None, response
triggers will be ignored. Default is 0 for backwards compatibility, but
typically another value or None will be necessary.
events (stimulus triggers will be unaffected). This parameter was
deprecated in version 0.17 and will be removed in 0.19. Use
``trig_shift_by_type={'response': ...}`` instead. If None, response
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 special events to consider in addition to those that
follow the normal Brainvision trigger format ('S###').
@@ -73,6 +76,14 @@ class RawBrainVision(BaseRaw):
verbose : bool, str, int, or None
If not None, override default verbose level (see :func:`mne.verbose`
and :ref:`Logging documentation <tut_logging>` for more).
trig_shift_by_type: dict | None
The names of marker types to which an offset should be added.
If dict, the keys specify marker types (case is ignored), so that the
corresponding value (an integer) will be added to the trigger value of
all events of this type. If the value for a key is in the dict is None,
all markers of this type will be ignored. If None (default), no offset
is added, which may lead to different marker types being mapped to the
same event id.
See Also
--------
@@ -84,7 +95,24 @@ class RawBrainVision(BaseRaw):
def __init__(self, vhdr_fname, montage=None,
eog=('HEOGL', 'HEOGR', 'VEOGb'), misc='auto',
scale=1., preload=False, response_trig_shift=0,
event_id=None, verbose=None): # noqa: D107
event_id=None, verbose=None,
trig_shift_by_type=None): # noqa: D107
if response_trig_shift != 0:
warnings.warn(
"'response_trig_shift' was deprecated in version "
"0.17 and will be removed in 0.19. Use "
"trig_shift_by_type={{'response': {} }} instead".format(
response_trig_shift), DeprecationWarning)
if trig_shift_by_type and 'response' in (
key.lower() for key in trig_shift_by_type):
raise ValueError(
'offset for response markers has been specified twice, '
'both using "trig_shift_by_type" and '
'"response_trig_shift"')
else:
if trig_shift_by_type is None:
trig_shift_by_type = dict()
trig_shift_by_type['response'] = response_trig_shift
# Channel info and events
logger.info('Extracting parameters from %s...' % vhdr_fname)
vhdr_fname = op.abspath(vhdr_fname)
@@ -92,7 +120,7 @@ class RawBrainVision(BaseRaw):
_get_vhdr_info(vhdr_fname, eog, misc, scale, montage)
self._order = order
self._n_samples = n_samples
events = _read_vmrk_events(mrk_fname, event_id, response_trig_shift)
events = _read_vmrk_events(mrk_fname, event_id, trig_shift_by_type)
_check_update_montage(info, montage)
with open(data_filename, 'rb') as f:
if isinstance(fmt, dict): # ASCII, this will be slow :(
@@ -198,7 +226,7 @@ def _read_segments_c(raw, data, idx, fi, start, stop, cals, mult):
_mult_cal_one(data, block, idx, cals, mult)
def _read_vmrk_events(fname, event_id=None, response_trig_shift=0):
def _read_vmrk_events(fname, event_id=None, trig_shift_by_type=None):
"""Read events from a vmrk file.
Parameters
@@ -224,6 +252,23 @@ def _read_vmrk_events(fname, event_id=None, response_trig_shift=0):
"""
if event_id is None:
event_id = dict()
if trig_shift_by_type is None:
trig_shift_by_type = dict()
if not isinstance(trig_shift_by_type, dict):
raise TypeError("'trig_shift_by_type' must be None or dict")
for mrk_type in list(trig_shift_by_type.keys()):
cur_shift = trig_shift_by_type[mrk_type]
if not isinstance(cur_shift, int) and cur_shift is not None:
raise TypeError('shift for type {} must be int or None'.format(
mrk_type
))
mrk_type_lc = mrk_type.lower()
if mrk_type_lc != mrk_type:
if mrk_type_lc in trig_shift_by_type:
raise ValueError('marker type {} specified twice with'
'different case'.format(mrk_type_lc))
trig_shift_by_type[mrk_type_lc] = cur_shift
del trig_shift_by_type[mrk_type]
# read vmrk file
with open(fname, 'rb') as fid:
txt = fid.read()
@@ -233,9 +278,6 @@ def _read_vmrk_events(fname, event_id=None, response_trig_shift=0):
# same in Latin-1 and UTF-8
header = txt.decode('ascii', 'ignore').split('\n')[0].strip()
_check_mrk_version(header)
if (response_trig_shift is not None and
not isinstance(response_trig_shift, int)):
raise TypeError("response_trig_shift must be an integer or None")
# although the markers themselves are guaranteed to be ASCII (they
# consist of numbers and a few reserved words), we should still
@@ -287,9 +329,10 @@ def _read_vmrk_events(fname, event_id=None, response_trig_shift=0):
trigger = int(re.findall(r'[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
if mtype.lower() in trig_shift_by_type:
cur_shift = trig_shift_by_type[mtype.lower()]
if cur_shift is not None:
trigger += cur_shift
else:
trigger = None
# FIXME: ideally, we would not use the middle column of the events
@@ -783,7 +826,8 @@ def _get_vhdr_info(vhdr_fname, eog, misc, scale, montage):
def read_raw_brainvision(vhdr_fname, montage=None,
eog=('HEOGL', 'HEOGR', 'VEOGb'), misc='auto',
scale=1., preload=False, response_trig_shift=0,
event_id=None, verbose=None):
event_id=None, verbose=None,
trig_shift_by_type=None):
"""Reader for Brain Vision EEG file.
Parameters
@@ -811,9 +855,11 @@ def read_raw_brainvision(vhdr_fname, montage=None,
If False, data are not read until save.
response_trig_shift : int | None
An integer that will be added to all response triggers when reading
events (stimulus triggers will be unaffected). If None, response
triggers will be ignored. Default is 0 for backwards compatibility, but
typically another value or None will be necessary.
events (stimulus triggers will be unaffected). This parameter was
deprecated in version 0.17 and will be removed in 0.19. Use
``trig_shift_by_type={'response': ...}`` instead. If None, response
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 special events to consider in addition to those that
follow the normal Brainvision trigger format ('S###').
@@ -824,6 +870,14 @@ def read_raw_brainvision(vhdr_fname, montage=None,
verbose : bool, str, int, or None
If not None, override default verbose level (see :func:`mne.verbose`
and :ref:`Logging documentation <tut_logging>` for more).
trig_shift_by_type: dict | None
The names of marker types to which an offset should be added.
If dict, the keys specify marker types (case is ignored), so that the
corresponding value (an integer) will be added to the trigger value of
all events of this type. If the value for a key is in the dict is None,
all markers of this type will be ignored. If None (default), no offset
is added, which may lead to different marker types being mapped to the
same event id.
Returns
-------
@@ -838,4 +892,5 @@ def read_raw_brainvision(vhdr_fname, montage=None,
return RawBrainVision(vhdr_fname=vhdr_fname, montage=montage, eog=eog,
misc=misc, scale=scale, preload=preload,
response_trig_shift=response_trig_shift,
event_id=event_id, verbose=verbose)
event_id=event_id, verbose=verbose,
trig_shift_by_type=trig_shift_by_type)
+1
View File
@@ -22,3 +22,4 @@ Mk10=Response,R255,6000,1,0
Mk11=Stimulus,S254,6620,1,0
Mk12=Stimulus,S255,6630,1,0
Mk13=SyncStatus,Sync On,7630,1,0
Mk14=Optic,O 1,7700,1,0
+58 -19
View File
@@ -347,7 +347,8 @@ def test_brainvision_data():
# test loading v2
read_raw_brainvision(vhdr_v2_path, eog=eog, preload=True,
response_trig_shift=1000, verbose='error')
trig_shift_by_type={'response': 1000},
verbose='error')
# For the nanovolt unit test we use the same data file with a different
# header file.
raw_nV = _test_raw_reader(
@@ -420,14 +421,15 @@ def test_events():
[4945, 1, 255],
[5999, 1, 255],
[6619, 1, 254],
[6629, 1, 255]])
[6629, 1, 255],
[7699, 1, 1]])
# check that events are read and stim channel is synthesized correctly and
# response triggers are shifted like they're supposed to be.
raw = read_raw_brainvision(vhdr_path, eog=eog,
response_trig_shift=1000, event_id=event_id)
# response triggers are shifted using the deprecated response_trig_shift.
with pytest.warns(DeprecationWarning):
raw = read_raw_brainvision(vhdr_path, eog=eog,
response_trig_shift=1000, event_id=event_id)
events = raw._get_brainvision_events()
events = events[events[:, 2] != event_id['Sync On']]
assert_array_equal(events, [[486, 0, 253],
[496, 1, 255],
[1769, 1, 254],
@@ -438,13 +440,35 @@ def test_events():
[4945, 1, 255],
[5999, 1, 1255],
[6619, 1, 254],
[6629, 1, 255]])
[6629, 1, 255],
[7629, 1, 5],
[7699, 1, 1]])
# check that trig_shift_by_type works as well
raw = read_raw_brainvision(vhdr_path, eog=eog,
trig_shift_by_type={'response': 1000,
'Optic': 2000},
event_id=event_id)
events = raw._get_brainvision_events()
assert_array_equal(events, [[486, 0, 253],
[496, 1, 255],
[1769, 1, 254],
[1779, 1, 255],
[3252, 1, 254],
[3262, 1, 255],
[4935, 1, 253],
[4945, 1, 255],
[5999, 1, 1255],
[6619, 1, 254],
[6629, 1, 255],
[7629, 1, 5],
[7699, 1, 2001]])
# check that events are read and stim channel is synthesized correctly and
# response triggers are ignored.
with warnings.catch_warnings(record=True): # ignored events
raw = read_raw_brainvision(vhdr_path, eog=eog,
response_trig_shift=None)
trig_shift_by_type={'response': None})
events = raw._get_brainvision_events()
events = events[events[:, 2] != event_id['Sync On']]
assert_array_equal(events, [[486, 0, 253],
@@ -456,13 +480,30 @@ def test_events():
[4935, 1, 253],
[4945, 1, 255],
[6619, 1, 254],
[6629, 1, 255]])
[6629, 1, 255],
[7699, 1, 1]])
# Error handling of trig_shift_by_type
pytest.raises(TypeError, read_raw_brainvision, vhdr_path, eog=eog,
preload=True, trig_shift_by_type=1)
pytest.raises(TypeError, read_raw_brainvision, vhdr_path, eog=eog,
preload=True, trig_shift_by_type={'response': 0.1})
pytest.raises(TypeError, read_raw_brainvision, vhdr_path, eog=eog,
preload=True, trig_shift_by_type={'response': np.nan})
pytest.raises(ValueError, read_raw_brainvision, vhdr_path, eog=eog,
preload=True, trig_shift_by_type={'response': 1000,
'Response': 1001})
with pytest.warns(DeprecationWarning):
pytest.raises(ValueError, read_raw_brainvision, vhdr_path, eog=eog,
preload=True, trig_shift_by_type={'response': 1000},
response_trig_shift=1001)
# check that events are read properly when event_id is specified for
# auxiliary events
with warnings.catch_warnings(record=True): # dropped events
raw = read_raw_brainvision(vhdr_path, eog=eog, preload=True,
response_trig_shift=None,
trig_shift_by_type={'response': None},
event_id=event_id)
events = raw._get_brainvision_events()
assert_array_equal(events, [[486, 0, 253],
@@ -475,12 +516,8 @@ def test_events():
[4945, 1, 255],
[6619, 1, 254],
[6629, 1, 255],
[7629, 1, 5]])
pytest.raises(TypeError, read_raw_brainvision, vhdr_path, eog=eog,
preload=True, response_trig_shift=0.1)
pytest.raises(TypeError, read_raw_brainvision, vhdr_path, eog=eog,
preload=True, response_trig_shift=np.nan)
[7629, 1, 5],
[7699, 1, 1]])
# to handle the min duration = 1 of stim trig (re)construction ...
events = np.array([[486, 1, 253],
@@ -493,11 +530,13 @@ def test_events():
[4945, 1, 255],
[6619, 1, 254],
[6629, 1, 255],
[7629, 1, 5]])
[7629, 1, 5],
[7699, 1, 1]])
# Test that both response_trig_shit and event_id can be set
# Test that both trig_shift_by_type and event_id can be set
read_raw_brainvision(vhdr_path, eog=eog, preload=False,
response_trig_shift=100, event_id=event_id)
trig_shift_by_type={'response': 100},
event_id=event_id)
mne_events = find_events(raw, stim_channel='STI 014')
assert_array_equal(events[:, [0, 2]], mne_events[:, [0, 2]])