Skip to content
Merged
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/13469.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Make :func:`mne.preprocessing.eyetracking.read_eyelink_calibration` robust to files with blank lines, by `Scott Huberty`_.
13 changes: 10 additions & 3 deletions mne/io/eyelink/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -992,17 +992,24 @@ def _parse_calibration(

n_points = int(regex.search(model).group()) # e.g. 13
n_points *= 2 if "LR" in line else 1 # one point per eye if "LR"

# The next n_point lines contain the validation data
points = []
for validation_index in range(n_points):
subline = lines[line_number + validation_index + 1]
if "!CAL VALIDATION" in subline:
line_idx = line_number + 1
read_points = 0
while read_points < n_points and line_idx < len(lines):
subline = lines[line_idx].strip()
line_idx += 1

if not subline or "!CAL VALIDATION" in subline:
continue # for bino mode, skip the second eye's validation summary

subline_eye = subline.split("at")[0].split()[-1].lower() # e.g. 'left'
if subline_eye != this_eye:
continue # skip the validation lines for the other eye
point_info = _parse_validation_line(subline)
points.append(point_info)
read_points += 1
# Convert the list of validation data into a numpy array
positions = np.array([point[:2] for point in points])
offsets = np.array([point[2] for point in points])
Expand Down
34 changes: 34 additions & 0 deletions mne/preprocessing/eyetracking/tests/test_calibration.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
# License: BSD-3-Clause
# Copyright the MNE-Python contributors.

from pathlib import Path

import numpy as np
import pytest

Expand Down Expand Up @@ -251,3 +253,35 @@ def test_plot_calibration(fname, axes):
scatter2.get_offsets(), np.column_stack((gaze_x, gaze_y))
)
plt.close(fig)


@requires_testing_data
@pytest.mark.parametrize("fname", [(fname)])
def test_calibration_newlines(fname, tmp_path):
"""Test reading a calibration with blank lines between each data line."""
# Calibration reading should be robust to what we are about to do to this file
want_cals = read_eyelink_calibration(fname)

# Let's interleave blank lines into this file
lines = Path(fname).read_text().splitlines()
cal_start = lines.index(">>>>>>> CALIBRATION (HV13,P-CR) FOR LEFT: <<<<<<<<<")
cal_end = lines.index("INPUT\t5509657\t0")
cal_block = lines[cal_start : cal_end + 1]

# Inject empty strings between each line in the calibration block
interleaved = [elem for line in cal_block for elem in (line, "")]

# Replace the original calibration block with the interleaved one
new_lines = lines[:cal_start] + interleaved + lines[cal_end + 1 :]

out_fname = tmp_path / "weird_calibration.asc"
out_fname.write_text("\n".join(new_lines))
cals = read_eyelink_calibration(out_fname)

# The added blank lines should not affect the values that we read in...
assert len(cals) == len(want_cals)
assert cals[1]["eye"] == want_cals[1]["eye"]
np.testing.assert_allclose(cals[0]["onset"], want_cals[0]["onset"])
np.testing.assert_allclose(cals[0]["positions"], want_cals[0]["positions"])
np.testing.assert_allclose(cals[1]["offsets"], want_cals[1]["offsets"])
np.testing.assert_allclose(cals[0]["gaze"], want_cals[0]["gaze"])
Loading