Skip to content

Commit f4a3f3f

Browse files
FIX: Handle an Eyelink File with blank lines injected throughout file (mne-tools#13469)
1 parent 65a717c commit f4a3f3f

File tree

3 files changed

+45
-3
lines changed

3 files changed

+45
-3
lines changed

doc/changes/dev/13469.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Make :func:`mne.preprocessing.eyetracking.read_eyelink_calibration` robust to files with blank lines, by `Scott Huberty`_.

mne/io/eyelink/_utils.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -992,17 +992,24 @@ def _parse_calibration(
992992

993993
n_points = int(regex.search(model).group()) # e.g. 13
994994
n_points *= 2 if "LR" in line else 1 # one point per eye if "LR"
995+
995996
# The next n_point lines contain the validation data
996997
points = []
997-
for validation_index in range(n_points):
998-
subline = lines[line_number + validation_index + 1]
999-
if "!CAL VALIDATION" in subline:
998+
line_idx = line_number + 1
999+
read_points = 0
1000+
while read_points < n_points and line_idx < len(lines):
1001+
subline = lines[line_idx].strip()
1002+
line_idx += 1
1003+
1004+
if not subline or "!CAL VALIDATION" in subline:
10001005
continue # for bino mode, skip the second eye's validation summary
1006+
10011007
subline_eye = subline.split("at")[0].split()[-1].lower() # e.g. 'left'
10021008
if subline_eye != this_eye:
10031009
continue # skip the validation lines for the other eye
10041010
point_info = _parse_validation_line(subline)
10051011
points.append(point_info)
1012+
read_points += 1
10061013
# Convert the list of validation data into a numpy array
10071014
positions = np.array([point[:2] for point in points])
10081015
offsets = np.array([point[2] for point in points])

mne/preprocessing/eyetracking/tests/test_calibration.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
# License: BSD-3-Clause
33
# Copyright the MNE-Python contributors.
44

5+
from pathlib import Path
6+
57
import numpy as np
68
import pytest
79

@@ -251,3 +253,35 @@ def test_plot_calibration(fname, axes):
251253
scatter2.get_offsets(), np.column_stack((gaze_x, gaze_y))
252254
)
253255
plt.close(fig)
256+
257+
258+
@requires_testing_data
259+
@pytest.mark.parametrize("fname", [(fname)])
260+
def test_calibration_newlines(fname, tmp_path):
261+
"""Test reading a calibration with blank lines between each data line."""
262+
# Calibration reading should be robust to what we are about to do to this file
263+
want_cals = read_eyelink_calibration(fname)
264+
265+
# Let's interleave blank lines into this file
266+
lines = Path(fname).read_text().splitlines()
267+
cal_start = lines.index(">>>>>>> CALIBRATION (HV13,P-CR) FOR LEFT: <<<<<<<<<")
268+
cal_end = lines.index("INPUT\t5509657\t0")
269+
cal_block = lines[cal_start : cal_end + 1]
270+
271+
# Inject empty strings between each line in the calibration block
272+
interleaved = [elem for line in cal_block for elem in (line, "")]
273+
274+
# Replace the original calibration block with the interleaved one
275+
new_lines = lines[:cal_start] + interleaved + lines[cal_end + 1 :]
276+
277+
out_fname = tmp_path / "weird_calibration.asc"
278+
out_fname.write_text("\n".join(new_lines))
279+
cals = read_eyelink_calibration(out_fname)
280+
281+
# The added blank lines should not affect the values that we read in...
282+
assert len(cals) == len(want_cals)
283+
assert cals[1]["eye"] == want_cals[1]["eye"]
284+
np.testing.assert_allclose(cals[0]["onset"], want_cals[0]["onset"])
285+
np.testing.assert_allclose(cals[0]["positions"], want_cals[0]["positions"])
286+
np.testing.assert_allclose(cals[1]["offsets"], want_cals[1]["offsets"])
287+
np.testing.assert_allclose(cals[0]["gaze"], want_cals[0]["gaze"])

0 commit comments

Comments
 (0)