diff --git a/doc/changes/dev/13469.bugfix.rst b/doc/changes/dev/13469.bugfix.rst new file mode 100644 index 00000000000..2ac336a2c27 --- /dev/null +++ b/doc/changes/dev/13469.bugfix.rst @@ -0,0 +1 @@ +Make :func:`mne.preprocessing.eyetracking.read_eyelink_calibration` robust to files with blank lines, by `Scott Huberty`_. diff --git a/mne/io/eyelink/_utils.py b/mne/io/eyelink/_utils.py index 0bd650ac8d8..2539baef038 100644 --- a/mne/io/eyelink/_utils.py +++ b/mne/io/eyelink/_utils.py @@ -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]) diff --git a/mne/preprocessing/eyetracking/tests/test_calibration.py b/mne/preprocessing/eyetracking/tests/test_calibration.py index 68ce513cc1f..37132743f00 100644 --- a/mne/preprocessing/eyetracking/tests/test_calibration.py +++ b/mne/preprocessing/eyetracking/tests/test_calibration.py @@ -2,6 +2,8 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. +from pathlib import Path + import numpy as np import pytest @@ -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"])