Skip to content

Commit

Permalink
[MRG] Don't look for an offset in an eyelink message if the message c…
Browse files Browse the repository at this point in the history
…ontains only 2 elements (mne-tools#12003)
  • Loading branch information
mscheltienne authored and snwnde committed Mar 20, 2024
1 parent c3ee827 commit b917495
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 66 deletions.
1 change: 1 addition & 0 deletions doc/changes/devel.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ Bugs
- Fix bug with delayed checking of :class:`info["bads"] <mne.Info>` (:gh:`12038` by `Eric Larson`_)
- Fix handling of channel information in annotations when loading data from and exporting to EDF file (:gh:`11960` :gh:`12017` by `Paul Roujansky`_)
- Add missing ``overwrite`` and ``verbose`` parameters to :meth:`Transform.save() <mne.transforms.Transform.save>` (:gh:`12004` by `Marijn van Vliet`_)
- Fix parsing of eye-link :class:`~mne.Annotations` when ``apply_offsets=False`` is provided to :func:`~mne.io.read_raw_eyelink` (:gh:`12003` by `Mathieu Scheltienne`_)
- Correctly prune channel-specific :class:`~mne.Annotations` when creating :class:`~mne.Epochs` without the channel(s) included in the channel specific annotations (:gh:`12010` by `Mathieu Scheltienne`_)
- Correctly handle passing ``"eyegaze"`` or ``"pupil"`` to :meth:`mne.io.Raw.pick` (:gh:`12019` by `Scott Huberty`_)

Expand Down
33 changes: 20 additions & 13 deletions mne/io/eyelink/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@
}


def _parse_eyelink_ascii(fname, find_overlaps=True, overlap_threshold=0.05):
def _parse_eyelink_ascii(
fname, find_overlaps=True, overlap_threshold=0.05, apply_offsets=False
):
# ======================== Parse ASCII File =========================
raw_extras = dict()
raw_extras.update(_parse_recording_blocks(fname))
Expand All @@ -49,7 +51,7 @@ def _parse_eyelink_ascii(fname, find_overlaps=True, overlap_threshold=0.05):
_validate_data(raw_extras)

# ======================== Create DataFrames ========================
raw_extras["dfs"] = _create_dataframes(raw_extras)
raw_extras["dfs"] = _create_dataframes(raw_extras, apply_offsets)
del raw_extras["sample_lines"] # free up memory
# add column names to dataframes and set the dtype of each column
col_names, ch_names = _infer_col_names(raw_extras)
Expand Down Expand Up @@ -252,7 +254,7 @@ def _get_sfreq_from_ascii(rec_info):
return float(rec_info[rec_info.index("RATE") + 1])


def _create_dataframes(raw_extras):
def _create_dataframes(raw_extras, apply_offsets):
"""Create pandas.DataFrame for Eyelink samples and events.
Creates a pandas DataFrame for sample_lines and for each
Expand Down Expand Up @@ -280,17 +282,22 @@ def _create_dataframes(raw_extras):
# make dataframe for experiment messages
if raw_extras["event_lines"]["MSG"]:
msgs = []
for tokens in raw_extras["event_lines"]["MSG"]:
timestamp = tokens[0]
# if offset token exists, it will be the 1st index and is numeric
if tokens[1].lstrip("-").replace(".", "", 1).isnumeric():
offset = float(tokens[1])
msg = " ".join(str(x) for x in tokens[2:])
else:
# there is no offset token
for token in raw_extras["event_lines"]["MSG"]:
if apply_offsets and len(token) == 2:
ts, msg = token
offset = np.nan
msg = " ".join(str(x) for x in tokens[1:])
msgs.append([timestamp, offset, msg])
elif apply_offsets:
ts = token[0]
try:
offset = float(token[1])
msg = " ".join(str(x) for x in token[2:])
except ValueError:
offset = np.nan
msg = " ".join(str(x) for x in token[1:])
else:
ts, offset = token[0], np.nan
msg = " ".join(str(x) for x in token[1:])
msgs.append([ts, offset, msg])
df_dict["messages"] = pd.DataFrame(msgs)

# make dataframe for recording block start, end times
Expand Down
61 changes: 12 additions & 49 deletions mne/io/eyelink/eyelink.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,35 +28,15 @@ def read_raw_eyelink(
overlap_threshold=0.05,
verbose=None,
):
"""Reader for an Eyelink .asc file.
"""Reader for an Eyelink ``.asc`` file.
Parameters
----------
fname : path-like
Path to the eyelink file (.asc).
create_annotations : bool | list (default True)
Whether to create mne.Annotations from occular events
(blinks, fixations, saccades) and experiment messages. If a list, must
contain one or more of ['fixations', 'saccades',' blinks', messages'].
If True, creates mne.Annotations for both occular events and experiment
messages.
apply_offsets : bool (default False)
Adjusts the onset time of the mne.Annotations created from Eyelink
experiment messages, if offset values exist in the ASCII file.
find_overlaps : bool (default False)
Combine left and right eye :class:`mne.Annotations` (blinks, fixations,
saccades) if their start times and their stop times are both not
separated by more than overlap_threshold.
overlap_threshold : float (default 0.05)
Time in seconds. Threshold of allowable time-gap between both the start and
stop times of the left and right eyes. If the gap is larger than the threshold,
the :class:`mne.Annotations` will be kept separate (i.e. ``"blink_L"``,
``"blink_R"``). If the gap is smaller than the threshold, the
:class:`mne.Annotations` will be merged and labeled as ``"blink_both"``.
Defaults to ``0.05`` seconds (50 ms), meaning that if the blink start times of
the left and right eyes are separated by less than 50 ms, and the blink stop
times of the left and right eyes are separated by less than 50 ms, then the
blink will be merged into a single :class:`mne.Annotations`.
%(eyelink_fname)s
%(eyelink_create_annotations)s
%(eyelink_apply_offsets)s
%(eyelink_find_overlaps)s
%(eyelink_overlap_threshold)s
%(verbose)s
Returns
Expand Down Expand Up @@ -95,28 +75,11 @@ class RawEyelink(BaseRaw):
Parameters
----------
fname : path-like
Path to the data file (.XXX).
create_annotations : bool | list (default True)
Whether to create mne.Annotations from occular events
(blinks, fixations, saccades) and experiment messages. If a list, must
contain one or more of ['fixations', 'saccades',' blinks', messages'].
If True, creates mne.Annotations for both occular events and experiment
messages.
apply_offsets : bool (default False)
Adjusts the onset time of the mne.Annotations created from Eyelink
experiment messages, if offset values exist in the ASCII file.
find_overlaps : boolean (default False)
Combine left and right eye :class:`mne.Annotations` (blinks, fixations,
saccades) if their start times and their stop times are both not
separated by more than overlap_threshold.
overlap_threshold : float (default 0.05)
Time in seconds. Threshold of allowable time-gap between the start and
stop times of the left and right eyes. If gap is larger than threshold,
the :class:`mne.Annotations` will be kept separate (i.e. "blink_L",
"blink_R"). If the gap is smaller than the threshold, the
:class:`mne.Annotations` will be merged (i.e. "blink_both").
%(eyelink_fname)s
%(eyelink_create_annotations)s
%(eyelink_apply_offsets)s
%(eyelink_find_overlaps)s
%(eyelink_overlap_threshold)s
%(verbose)s
See Also
Expand All @@ -141,7 +104,7 @@ def __init__(

# ======================== Parse ASCII file ==========================
eye_ch_data, info, raw_extras = _parse_eyelink_ascii(
fname, find_overlaps, overlap_threshold
fname, find_overlaps, overlap_threshold, apply_offsets
)
# ======================== Create Raw Object =========================
super(RawEyelink, self).__init__(
Expand Down
30 changes: 29 additions & 1 deletion mne/io/eyelink/tests/test_eyelink.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import pytest

import numpy as np
from numpy.testing import assert_allclose

from mne.datasets.testing import data_path, requires_testing_data
from mne.io import read_raw_eyelink
Expand Down Expand Up @@ -254,7 +255,7 @@ def test_multi_block_misc_channels(fname, tmp_path):
_simulate_eye_tracking_data(fname, out_file)

with pytest.warns(RuntimeWarning, match="Raw eyegaze coordinates"):
raw = read_raw_eyelink(out_file)
raw = read_raw_eyelink(out_file, apply_offsets=True)

chs_in_file = [
"xpos_right",
Expand Down Expand Up @@ -286,3 +287,30 @@ def test_multi_block_misc_channels(fname, tmp_path):
def test_basics(this_fname):
"""Test basics of reading."""
_test_raw_reader(read_raw_eyelink, fname=this_fname, test_preloading=False)


def test_annotations_without_offset(tmp_path):
"""Test read of annotations without offset."""
out_file = tmp_path / "tmp_eyelink.asc"

# create fake dataset
with open(fname_href, "r") as file:
lines = file.readlines()
ts = lines[-3].split("\t")[0]
line = f"MSG\t{ts} test string\n"
lines = lines[:-3] + [line] + lines[-3:]

with open(out_file, "w") as file:
file.writelines(lines)

raw = read_raw_eyelink(out_file, apply_offsets=False)
assert raw.annotations[-1]["description"] == "test string"
onset1 = raw.annotations[-1]["onset"]
assert raw.annotations[1]["description"] == "-2 SYNCTIME"
onset2 = raw.annotations[1]["onset"]

raw = read_raw_eyelink(out_file, apply_offsets=True)
assert raw.annotations[-1]["description"] == "test string"
assert raw.annotations[1]["description"] == "SYNCTIME"
assert_allclose(raw.annotations[-1]["onset"], onset1)
assert_allclose(raw.annotations[1]["onset"], onset2 - 2 / raw.info["sfreq"])
2 changes: 1 addition & 1 deletion mne/preprocessing/realign.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def realign_raw(raw, other, t_raw, t_other, verbose=None):
t_raw : array-like, shape (n_events,)
The times of shared events in ``raw`` relative to ``raw.times[0]`` (0).
Typically these could be events on some TTL channel like
``find_events(raw)[:, 0] - raw.first_event``.
``find_events(raw)[:, 0] - raw.first_samp``.
t_other : array-like, shape (n_events,)
The times of shared events in ``other`` relative to ``other.times[0]``.
%(verbose)s
Expand Down
50 changes: 50 additions & 0 deletions mne/utils/docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -1518,6 +1518,56 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75):
the head circle.
"""

docdict[
"eyelink_apply_offsets"
] = """
apply_offsets : bool (default False)
Adjusts the onset time of the :class:`~mne.Annotations` created from Eyelink
experiment messages, if offset values exist in the ASCII file. If False, any
offset-like values will be prepended to the annotation description.
"""

docdict[
"eyelink_create_annotations"
] = """
create_annotations : bool | list (default True)
Whether to create :class:`~mne.Annotations` from occular events
(blinks, fixations, saccades) and experiment messages. If a list, must
contain one or more of ``['fixations', 'saccades',' blinks', messages']``.
If True, creates :class:`~mne.Annotations` for both occular events and
experiment messages.
"""

docdict[
"eyelink_find_overlaps"
] = """
find_overlaps : bool (default False)
Combine left and right eye :class:`mne.Annotations` (blinks, fixations,
saccades) if their start times and their stop times are both not
separated by more than overlap_threshold.
"""

docdict[
"eyelink_fname"
] = """
fname : path-like
Path to the eyelink file (``.asc``)."""

docdict[
"eyelink_overlap_threshold"
] = """
overlap_threshold : float (default 0.05)
Time in seconds. Threshold of allowable time-gap between both the start and
stop times of the left and right eyes. If the gap is larger than the threshold,
the :class:`mne.Annotations` will be kept separate (i.e. ``"blink_L"``,
``"blink_R"``). If the gap is smaller than the threshold, the
:class:`mne.Annotations` will be merged and labeled as ``"blink_both"``.
Defaults to ``0.05`` seconds (50 ms), meaning that if the blink start times of
the left and right eyes are separated by less than 50 ms, and the blink stop
times of the left and right eyes are separated by less than 50 ms, then the
blink will be merged into a single :class:`mne.Annotations`.
"""

# %%
# F

Expand Down
4 changes: 2 additions & 2 deletions tutorials/preprocessing/90_eyetracking_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,8 @@
event_dict = dict(Flash=2)

# %%
# Align the eye-tracking data with EEG the data
# ---------------------------------------------
# Align the eye-tracking data with EEG data
# -----------------------------------------
#
# In this dataset, eye-tracking and EEG data were recorded simultaneously, but on
# different systems, so we'll need to align the data before we can analyze them
Expand Down

0 comments on commit b917495

Please sign in to comment.