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:
committed by
Alexandre Gramfort
parent
b9c712b237
commit
3d08007b75
@@ -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
|
||||
~~~
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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]])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user