Skip to content
1 change: 1 addition & 0 deletions doc/changes/dev/13357.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Make :func:`~mne.io.read_raw_eyelink` work with ASCII files collected with Eye Event Mode set to "HREF" by `Scott Huberty`_.
37 changes: 30 additions & 7 deletions mne/io/eyelink/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"sacc_visual_angle",
"peak_velocity",
),
"messages": ("time", "offset", "event_msg"),
}


Expand Down Expand Up @@ -356,6 +357,7 @@ def _infer_col_names(raw_extras):
col_names = {}
# initiate the column names for the sample lines
col_names["samples"] = list(EYELINK_COLS["timestamp"])
col_names["messages"] = list(EYELINK_COLS["messages"])

# and for the eye message lines
col_names["blinks"] = list(EYELINK_COLS["eye_event"])
Expand Down Expand Up @@ -410,12 +412,33 @@ def _assign_col_names(col_names, df_dict):
col_names : dict
Dictionary of column names for each dataframe.
"""
skipped_types = []
for key, df in df_dict.items():
if key in ("samples", "blinks", "fixations", "saccades"):
df.columns = col_names[key]
elif key == "messages":
cols = ["time", "offset", "event_msg"]
df.columns = cols
if key in ("samples", "blinks", "fixations", "saccades", "messages"):
cols = col_names[key]
else:
skipped_types.append(key)
continue
max_cols = len(cols)
if len(df.columns) != len(cols):
if key in ("saccades", "fixations") and len(df.columns) >= 4:
# see https://github.com/mne-tools/mne-python/pull/13357
logger.debug(
f"{key} events have more columns ({len(df.columns)}) than "
f"expected ({len(cols)}). Using first 4 (eye, time, end_time, "
"duration)."
)
max_cols = 4
else:
raise ValueError(
f"Expected the {key} data in this file to have {len(cols)} columns "
f"of data, but got {len(df.columns)}. Expected columns: {cols}."
)
new_col_names = {
old: new for old, new in zip(df.columns[:max_cols], cols[:max_cols])
}
df.rename(columns=new_col_names, inplace=True)
logger.debug(f"Skipped assigning column names to {skipped_types} dataframes.")
return df_dict


Expand Down Expand Up @@ -474,10 +497,10 @@ def _convert_times(df, first_samp, col="time"):
"""
_sort_by_time(df, col)
for col in df.columns:
if col.endswith("time"): # 'time' and 'end_time' cols
if str(col).endswith("time"): # 'time' and 'end_time' cols
df[col] -= first_samp
df[col] /= 1000
if col in ["duration", "offset"]:
if str(col) in ["duration", "offset"]:
df[col] /= 1000
return df

Expand Down
25 changes: 25 additions & 0 deletions mne/io/eyelink/tests/test_eyelink.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,3 +339,28 @@ def test_no_datetime(tmp_path):
# Sanity check that a None meas_date doesn't change annotation times
# First annotation in this file is a fixation at 0.004 seconds
np.testing.assert_allclose(raw.annotations.onset[0], 0.004)


@requires_testing_data
def test_href_eye_events(tmp_path):
"""Test Parsing file where Eye Event Data option was set to 'HREF'."""
out_file = tmp_path / "tmp_eyelink.asc"
lines = fname_href.read_text("utf-8").splitlines()
for li, line in enumerate(lines):
if not line.startswith(("ESACC", "EFIX")):
continue
tokens = line.split()
if line.startswith("ESACC"):
href_sacc_vals = ["9999", "9999", "9999", "9999", "99.99", "999"]
tokens[5:5] = href_sacc_vals # add href saccade values
elif line.startswith("EFIX"):
tokens = line.split()
href_fix_vals = ["9999.9", "9999.9", "999"]
tokens[5:3] = href_fix_vals
new_line = "\t".join(tokens) + "\n"
lines[li] = new_line
out_file.write_text("\n".join(lines), encoding="utf-8")
raw = read_raw_eyelink(out_file)
# Just check that we actually parsed the Saccade and Fixation events
assert "saccade" in raw.annotations.description
assert "fixation" in raw.annotations.description
Loading