Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changes/dev/13555.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix bug where :func:`mne.io.read_raw_eyelink` raised an error when reading Eyelink files with an empty first recording block, by :newcontrib:`Varun Kasyap Pentamaraju` (:gh:`13555`).
1 change: 1 addition & 0 deletions doc/changes/names.inc
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@
.. _Tziona NessAiver: https://github.com/TzionaN
.. _user27182: https://github.com/user27182
.. _Valerii Chirkov: https://github.com/vagechirkov
.. _Varun Kasyap Pentamaraju: https://github.com/varunkasyap
.. _Velu Prabhakar Kumaravel: https://github.com/vpKumaravel
.. _Victor Ferat: https://github.com/vferat
.. _Victoria Peterson: https://github.com/vpeterson
Expand Down
19 changes: 13 additions & 6 deletions mne/io/eyelink/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,14 @@ def _parse_eyelink_ascii(
raw_extras["dfs"][key], max_time=overlap_threshold
)
# ======================== Info for BaseRaw ========================
eye_ch_data = raw_extras["dfs"]["samples"][ch_names].to_numpy().T
dfs = raw_extras["dfs"]

if "samples" not in dfs or dfs["samples"].empty:
logger.info("No sample data found, creating empty Raw object.")
eye_ch_data = np.empty((len(ch_names), 0))
else:
eye_ch_data = dfs["samples"][ch_names].to_numpy().T

info = _create_info(ch_names, raw_extras)

return eye_ch_data, info, raw_extras
Expand All @@ -103,7 +110,7 @@ def _parse_recording_blocks(fname):
samples lines start with a posix-like string,
and contain eyetracking sample info. Event Lines
start with an upper case string and contain info
about occular events (i.e. blink/saccade), or experiment
about ocular events (i.e. blink/saccade), or experiment
messages sent by the stimulus presentation software.
"""
with fname.open() as file:
Expand Down Expand Up @@ -182,7 +189,7 @@ def _validate_data(data_blocks: list):
pupil_units.append(block["info"]["pupil_unit"])
if "GAZE" in units:
logger.info(
"Pixel coordinate data detected."
"Pixel coordinate data detected. "
"Pass `scalings=dict(eyegaze=1e3)` when using plot"
" method to make traces more legible."
)
Expand Down Expand Up @@ -369,7 +376,7 @@ def _create_dataframes_for_block(block, apply_offsets):
df_dict["samples"] = pd.DataFrame(block["samples"])
df_dict["samples"] = _drop_status_col(df_dict["samples"]) # drop STATUS col

# dataframe for each type of occular event in this block
# dataframe for each type of ocular event in this block
for event, label in zip(
["EFIX", "ESACC", "EBLINK"], ["fixations", "saccades", "blinks"]
):
Expand Down Expand Up @@ -697,7 +704,7 @@ def _adjust_times(
-----
After _parse_recording_blocks, Files with multiple recording blocks will
have missing timestamps for the duration of the period between the blocks.
This would cause the occular annotations (i.e. blinks) to not line up with
This would cause the ocular annotations (i.e. blinks) to not line up with
the signal.
"""
pd = _check_pandas_installed()
Expand All @@ -723,7 +730,7 @@ def _find_overlaps(df, max_time=0.05):
Parameters
----------
df : pandas.DataFrame
Pandas DataFrame with occular events (fixations, saccades, blinks)
Pandas DataFrame with ocular events (fixations, saccades, blinks)
max_time : float (default 0.05)
Time in seconds. Defaults to .05 (50 ms)

Expand Down
6 changes: 4 additions & 2 deletions mne/io/eyelink/eyelink.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def read_raw_eyelink(

@fill_doc
class RawEyelink(BaseRaw):
"""Raw object from an XXX file.
"""Raw object from an Eyelink file.

Parameters
----------
Expand Down Expand Up @@ -123,7 +123,9 @@ def __init__(
eye_annots = _make_eyelink_annots(
self._raw_extras[0]["dfs"], create_annotations, apply_offsets
)
if gap_annots and eye_annots: # set both
if self.n_times == 0:
logger.info("No samples found in recording, skipping annotation creation.")
elif gap_annots and eye_annots: # set both
self.set_annotations(gap_annots + eye_annots)
elif gap_annots:
self.set_annotations(gap_annots)
Expand Down
21 changes: 21 additions & 0 deletions mne/io/eyelink/tests/test_eyelink.py
Original file line number Diff line number Diff line change
Expand Up @@ -523,3 +523,24 @@ def test_href_eye_events(tmp_path):
# Just check that we actually parsed the Saccade and Fixation events
assert "saccade" in raw.annotations.description
assert "fixation" in raw.annotations.description


@requires_testing_data
def test_empty_first_trial(tmp_path):
"""Test reading a file with an empty first trial."""
out_file = tmp_path / "tmp_eyelink.asc"
# Use a real eyelink file as base
lines = fname.read_text("utf-8").splitlines()
# Find first START and END
end_idx = next(i for i, line in enumerate(lines) if line.startswith("END"))
# Keep headers + START..END but REMOVE all numeric sample lines
first_block = []
for line in lines[: end_idx + 1]:
tokens = line.split()
if line.startswith("START") or not tokens or not tokens[0].isdigit():
first_block.append(line)

# Append rest of file (second trial onwards)
rest = lines[end_idx + 1 :]
out_file.write_text("\n".join(first_block + rest), encoding="utf-8")
read_raw_eyelink(out_file)