From cb45659aff768d046ef948b231c5d7dab66994fe Mon Sep 17 00:00:00 2001 From: varunkasyap Date: Sun, 21 Dec 2025 18:02:56 +0530 Subject: [PATCH 01/12] FIX: handle empty first Eyelink recording block --- doc/changes/dev/13555.bugfix.rst | 1 + doc/changes/names.inc | 1 + mne/io/eyelink/_utils.py | 19 +++++++++++++------ mne/io/eyelink/eyelink.py | 6 ++++-- mne/io/eyelink/tests/test_eyelink.py | 21 +++++++++++++++++++++ 5 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 doc/changes/dev/13555.bugfix.rst diff --git a/doc/changes/dev/13555.bugfix.rst b/doc/changes/dev/13555.bugfix.rst new file mode 100644 index 00000000000..0ba15a93d88 --- /dev/null +++ b/doc/changes/dev/13555.bugfix.rst @@ -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`). \ No newline at end of file diff --git a/doc/changes/names.inc b/doc/changes/names.inc index 77e665ec6ed..cc6800a1b49 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -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 diff --git a/mne/io/eyelink/_utils.py b/mne/io/eyelink/_utils.py index e66b1855886..2b9f0f1c769 100644 --- a/mne/io/eyelink/_utils.py +++ b/mne/io/eyelink/_utils.py @@ -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 @@ -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: @@ -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." ) @@ -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"] ): @@ -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() @@ -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) diff --git a/mne/io/eyelink/eyelink.py b/mne/io/eyelink/eyelink.py index 192a5555465..af610e5f43b 100644 --- a/mne/io/eyelink/eyelink.py +++ b/mne/io/eyelink/eyelink.py @@ -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 ---------- @@ -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) diff --git a/mne/io/eyelink/tests/test_eyelink.py b/mne/io/eyelink/tests/test_eyelink.py index 596c4468b7a..911a52708a2 100644 --- a/mne/io/eyelink/tests/test_eyelink.py +++ b/mne/io/eyelink/tests/test_eyelink.py @@ -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) From c6a624f530703146bc97d8b4d8fd946afd7e82fc Mon Sep 17 00:00:00 2001 From: Qian Chu <97355086+qian-chu@users.noreply.github.com> Date: Sat, 27 Dec 2025 20:10:46 +0100 Subject: [PATCH 02/12] fix bug where empty first element leads to error --- mne/io/eyelink/_utils.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/mne/io/eyelink/_utils.py b/mne/io/eyelink/_utils.py index 2b9f0f1c769..9604b6904ea 100644 --- a/mne/io/eyelink/_utils.py +++ b/mne/io/eyelink/_utils.py @@ -566,10 +566,20 @@ def _drop_status_col(samples_df): status_cols = [] # we know the first 3 columns will be the time, xpos, ypos for col in samples_df.columns[3:]: - if samples_df[col][0][0].isnumeric(): - # if the value is numeric, it's not a status column + first_valid_index = samples_df[col].first_valid_index() + if first_valid_index is None: + # The entire column is NaN, so we can drop it + status_cols.append(col) continue - if len(samples_df[col][0]) in [3, 5, 13, 17]: + value = samples_df.loc[first_valid_index, col] + try: + float(value) + continue # if the value is numeric, it's not a status column + except (ValueError, TypeError): + # cannot convert to float, so it might be a status column + pass + # further check the length of the string value + if len(value) in [3, 5, 13, 17]: status_cols.append(col) return samples_df.drop(columns=status_cols) From 90e172d13b58d676192b5c5912d3d266adc6ef39 Mon Sep 17 00:00:00 2001 From: Varun Kasyap Pentamaraju <77223408+varunkasyap@users.noreply.github.com> Date: Wed, 24 Dec 2025 18:44:36 +0530 Subject: [PATCH 03/12] FIX: Corrected typos across codebase (#13563) Co-authored-by: Mathieu Scheltienne --- mne/_fiff/meas_info.py | 2 +- mne/channels/channels.py | 2 +- mne/channels/layout.py | 2 +- mne/epochs.py | 10 +++++----- mne/evoked.py | 2 +- mne/io/eeglab/_eeglab.py | 2 +- mne/morph.py | 2 +- mne/preprocessing/eyetracking/calibration.py | 2 +- mne/source_estimate.py | 2 +- mne/utils/docs.py | 4 ++-- 10 files changed, 15 insertions(+), 15 deletions(-) diff --git a/mne/_fiff/meas_info.py b/mne/_fiff/meas_info.py index 90454b9a699..8baae82f4b0 100644 --- a/mne/_fiff/meas_info.py +++ b/mne/_fiff/meas_info.py @@ -1461,7 +1461,7 @@ class Info(ValidatedDict, SetChannelsMixin, MontageMixin, ContainsMixin): Eyetrack Element ``[3]`` contains information about which eye was tracked (-1 for left, 1 for right), and element ``[4]`` contains information - about the the axis of coordinate data (-1 for x-coordinate data, 1 for + about the axis of coordinate data (-1 for x-coordinate data, 1 for y-coordinate data). Dipole Elements ``[3:6]`` contain dipole orientation information. diff --git a/mne/channels/channels.py b/mne/channels/channels.py index ef98efd1731..08c35dd310e 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -1478,7 +1478,7 @@ class _BuiltinChannelAdjacency: def get_builtin_ch_adjacencies(*, descriptions=False): """Get a list of all FieldTrip neighbor definitions shipping with MNE. - The names of the these neighbor definitions can be passed to + The names of these neighbor definitions can be passed to :func:`read_ch_adjacency`. Parameters diff --git a/mne/channels/layout.py b/mne/channels/layout.py index 20ef3809207..85b7913c1c6 100644 --- a/mne/channels/layout.py +++ b/mne/channels/layout.py @@ -1192,7 +1192,7 @@ def _merge_opm_data(data, merged_names): """Merge data from multiple opm channel by just using the radial component. Channel names that end in "MERGE_REMOVE" (ie non-radial channels) will be - removed. Only the the radial channel is kept. + removed. Only the radial channel is kept. Parameters ---------- diff --git a/mne/epochs.py b/mne/epochs.py index 6c3935097bf..4bd94ffa2c5 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -394,7 +394,7 @@ class BaseEpochs( %(event_id)s %(epochs_tmin_tmax)s %(baseline_epochs)s - Defaults to ``(None, 0)``, i.e. beginning of the the data until + Defaults to ``(None, 0)``, i.e. beginning of the data until time point zero. %(raw_epochs)s %(picks_all)s @@ -748,7 +748,7 @@ def apply_baseline(self, baseline=(None, 0), *, verbose=None): Parameters ---------- %(baseline_epochs)s - Defaults to ``(None, 0)``, i.e. beginning of the the data until + Defaults to ``(None, 0)``, i.e. beginning of the data until time point zero. %(verbose)s @@ -3248,7 +3248,7 @@ def _diff_input_strings_vs_event_id(input_strings, input_name, event_id): # Determine which events fall into the current time window if start_sample is None and isinstance(tmin, list): - # Lower bound is the the current or the closest previpus event with a name + # Lower bound is the current or the closest previous event with a name # in "tmin"; if there is no such event (e.g., beginning of the recording is # being approached), the upper lower becomes the last event in the # recording. @@ -3272,7 +3272,7 @@ def _diff_input_strings_vs_event_id(input_strings, input_name, event_id): window_start_sample = row_event.sample + start_sample if stop_sample is None and isinstance(tmax, list): - # Upper bound is the the current or the closest following event with a name + # Upper bound is the current or the closest following event with a name # in "tmax"; if there is no such event (e.g., end of the recording is being # approached), the upper bound becomes the last event in the recording. next_matching_events = events_df.loc[ @@ -3436,7 +3436,7 @@ class Epochs(BaseEpochs): %(event_id)s %(epochs_tmin_tmax)s %(baseline_epochs)s - Defaults to ``(None, 0)``, i.e. beginning of the the data until + Defaults to ``(None, 0)``, i.e. beginning of the data until time point zero. %(picks_all)s preload : bool diff --git a/mne/evoked.py b/mne/evoked.py index c016efd59f3..32475b37818 100644 --- a/mne/evoked.py +++ b/mne/evoked.py @@ -369,7 +369,7 @@ def apply_baseline(self, baseline=(None, 0), *, verbose=None): Parameters ---------- %(baseline_evoked)s - Defaults to ``(None, 0)``, i.e. beginning of the the data until + Defaults to ``(None, 0)``, i.e. beginning of the data until time point zero. %(verbose)s diff --git a/mne/io/eeglab/_eeglab.py b/mne/io/eeglab/_eeglab.py index 78df0be366b..77ad6e681a7 100644 --- a/mne/io/eeglab/_eeglab.py +++ b/mne/io/eeglab/_eeglab.py @@ -195,7 +195,7 @@ def _readmat(fname, uint16_codec=None, *, preload=False): # and handle the 'data' field specially # the files in eeglab are always the same field names - # the the fields were taken from the eeglab sample reference + # the fields were taken from the eeglab sample reference # available at the eeglab github: # https://github.com/sccn/eeglab/blob/develop/sample_data/eeglab_data.set # The sample reference is the big reference for the field names diff --git a/mne/morph.py b/mne/morph.py index a8278731f3c..e2a48350f1b 100644 --- a/mne/morph.py +++ b/mne/morph.py @@ -412,7 +412,7 @@ class SourceMorph: pre_affine : instance of dipy.align.AffineMap The transformation that is applied before the before ``sdr_morph``. sdr_morph : instance of dipy.align.DiffeomorphicMap - The class that applies the the symmetric diffeomorphic registration + The class that applies the symmetric diffeomorphic registration (SDR) morph. src_data : dict Additional source data necessary to perform morphing. diff --git a/mne/preprocessing/eyetracking/calibration.py b/mne/preprocessing/eyetracking/calibration.py index 867119557a8..54518ff1a63 100644 --- a/mne/preprocessing/eyetracking/calibration.py +++ b/mne/preprocessing/eyetracking/calibration.py @@ -28,7 +28,7 @@ class Calibration(dict): ---------- onset : float The onset of the calibration in seconds. If the calibration was - performed before the recording started, the the onset can be + performed before the recording started, the onset can be negative. model : str A string, which is the model of the eye-tracking calibration that was applied. diff --git a/mne/source_estimate.py b/mne/source_estimate.py index deeb3a43ede..689022fb326 100644 --- a/mne/source_estimate.py +++ b/mne/source_estimate.py @@ -697,7 +697,7 @@ def apply_baseline(self, baseline=(None, 0), *, verbose=None): Parameters ---------- %(baseline_stc)s - Defaults to ``(None, 0)``, i.e. beginning of the the data until + Defaults to ``(None, 0)``, i.e. beginning of the data until time point zero. %(verbose)s diff --git a/mne/utils/docs.py b/mne/utils/docs.py index 898fb9c34e1..54b2c9f1363 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -1232,7 +1232,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): docdict["elevation"] = """ elevation : float - The The zenith angle of the camera rendering the view in degrees. + The zenith angle of the camera rendering the view in degrees. """ docdict["eltc_mode_notes"] = """ @@ -3991,7 +3991,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): docdict["sdr_morph"] = """ sdr_morph : instance of dipy.align.DiffeomorphicMap - The class that applies the the symmetric diffeomorphic registration + The class that applies the symmetric diffeomorphic registration (SDR) morph. """ From bfd14ae13e7c3c84fbcf42810c1afc30c4a2cc65 Mon Sep 17 00:00:00 2001 From: Varun Kasyap Pentamaraju <77223408+varunkasyap@users.noreply.github.com> Date: Fri, 26 Dec 2025 22:07:40 +0530 Subject: [PATCH 04/12] FIX: Fixed incorrect module stripping in logging (#13561) Co-authored-by: Daniel McCloy --- mne/utils/_logging.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/utils/_logging.py b/mne/utils/_logging.py index f4d19655bbf..1a36e043a32 100644 --- a/mne/utils/_logging.py +++ b/mne/utils/_logging.py @@ -511,7 +511,7 @@ def _frame_info(n): except KeyError: # in our verbose dec pass else: - infos.append(f"{name.lstrip('mne.')}:{frame.f_lineno}") + infos.append(f"{name.removeprefix('mne.')}:{frame.f_lineno}") frame = frame.f_back if frame is None: break From a44267a1ba4d141afd62ad49a48f8cf44d9d1120 Mon Sep 17 00:00:00 2001 From: Qian Chu <97355086+qian-chu@users.noreply.github.com> Date: Tue, 30 Dec 2025 15:21:48 +0100 Subject: [PATCH 05/12] Update _utils.py --- mne/io/eyelink/_utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mne/io/eyelink/_utils.py b/mne/io/eyelink/_utils.py index 9604b6904ea..45cad0cc651 100644 --- a/mne/io/eyelink/_utils.py +++ b/mne/io/eyelink/_utils.py @@ -566,6 +566,8 @@ def _drop_status_col(samples_df): status_cols = [] # we know the first 3 columns will be the time, xpos, ypos for col in samples_df.columns[3:]: + # use first valid index to ignore preceding empty values + # see https://github.com/mne-tools/mne-python/issues/13567 first_valid_index = samples_df[col].first_valid_index() if first_valid_index is None: # The entire column is NaN, so we can drop it From fecb1781762ed8a5bb8018bbf2838b52023d187d Mon Sep 17 00:00:00 2001 From: Qian Chu <97355086+qian-chu@users.noreply.github.com> Date: Tue, 30 Dec 2025 15:24:50 +0100 Subject: [PATCH 06/12] Update _utils.py --- mne/io/eyelink/_utils.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/mne/io/eyelink/_utils.py b/mne/io/eyelink/_utils.py index 45cad0cc651..f46aca7e3fb 100644 --- a/mne/io/eyelink/_utils.py +++ b/mne/io/eyelink/_utils.py @@ -566,23 +566,22 @@ def _drop_status_col(samples_df): status_cols = [] # we know the first 3 columns will be the time, xpos, ypos for col in samples_df.columns[3:]: - # use first valid index to ignore preceding empty values + # use first valid index and value to ignore leading empty values # see https://github.com/mne-tools/mne-python/issues/13567 first_valid_index = samples_df[col].first_valid_index() if first_valid_index is None: # The entire column is NaN, so we can drop it status_cols.append(col) continue - value = samples_df.loc[first_valid_index, col] + first_value = samples_df.loc[first_valid_index, col] try: - float(value) + float(first_value) continue # if the value is numeric, it's not a status column except (ValueError, TypeError): # cannot convert to float, so it might be a status column - pass - # further check the length of the string value - if len(value) in [3, 5, 13, 17]: - status_cols.append(col) + # further check the length of the string value + if len(first_value) in [3, 5, 13, 17]: + status_cols.append(col) return samples_df.drop(columns=status_cols) From 911310e8e0b0ba717ea56145162ddd3b9815e68f Mon Sep 17 00:00:00 2001 From: Qian Chu <97355086+qian-chu@users.noreply.github.com> Date: Tue, 30 Dec 2025 22:57:53 +0100 Subject: [PATCH 07/12] Create 13571.bugfix.rst --- doc/changes/dev/13571.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changes/dev/13571.bugfix.rst diff --git a/doc/changes/dev/13571.bugfix.rst b/doc/changes/dev/13571.bugfix.rst new file mode 100644 index 00000000000..a77ef5569a9 --- /dev/null +++ b/doc/changes/dev/13571.bugfix.rst @@ -0,0 +1 @@ +Fix bug where :func:`mne.io.read_raw_eyelink` fails to handle data starting with missing values, by `Qian Chu`_. \ No newline at end of file From 0510accbd6dd685da34b6d7eb56d81da1042406a Mon Sep 17 00:00:00 2001 From: qian-chu Date: Thu, 1 Jan 2026 22:24:58 +0100 Subject: [PATCH 08/12] merge two PR contents --- doc/changes/dev/13555.bugfix.rst | 1 - doc/changes/dev/13571.bugfix.rst | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 doc/changes/dev/13555.bugfix.rst diff --git a/doc/changes/dev/13555.bugfix.rst b/doc/changes/dev/13555.bugfix.rst deleted file mode 100644 index 0ba15a93d88..00000000000 --- a/doc/changes/dev/13555.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -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`). \ No newline at end of file diff --git a/doc/changes/dev/13571.bugfix.rst b/doc/changes/dev/13571.bugfix.rst index a77ef5569a9..c2153593281 100644 --- a/doc/changes/dev/13571.bugfix.rst +++ b/doc/changes/dev/13571.bugfix.rst @@ -1 +1 @@ -Fix bug where :func:`mne.io.read_raw_eyelink` fails to handle data starting with missing values, by `Qian Chu`_. \ No newline at end of file +Fix bug where :func:`mne.io.read_raw_eyelink` fails to handle data starting with empty blocks or blocks starting with missing values, by :newcontrib:`Varun Kasyap Pentamaraju`and `Qian Chu`_. \ No newline at end of file From 629c900191ffe61b1899ffc40f6f1ffc95a5d5f3 Mon Sep 17 00:00:00 2001 From: Qian Chu <97355086+qian-chu@users.noreply.github.com> Date: Fri, 2 Jan 2026 14:30:09 +0100 Subject: [PATCH 09/12] fix typos --- doc/changes/dev/13571.bugfix.rst | 2 +- mne/io/eyelink/_utils.py | 2 +- mne/io/eyelink/tests/test_eyelink.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/changes/dev/13571.bugfix.rst b/doc/changes/dev/13571.bugfix.rst index c2153593281..ffe0f574b38 100644 --- a/doc/changes/dev/13571.bugfix.rst +++ b/doc/changes/dev/13571.bugfix.rst @@ -1 +1 @@ -Fix bug where :func:`mne.io.read_raw_eyelink` fails to handle data starting with empty blocks or blocks starting with missing values, by :newcontrib:`Varun Kasyap Pentamaraju`and `Qian Chu`_. \ No newline at end of file +Fix bug where :func:`mne.io.read_raw_eyelink` fails to handle data starting with empty blocks or blocks starting with missing values, by :newcontrib:`Varun Kasyap Pentamaraju` and `Qian Chu`_. \ No newline at end of file diff --git a/mne/io/eyelink/_utils.py b/mne/io/eyelink/_utils.py index f46aca7e3fb..8f3b2e086c3 100644 --- a/mne/io/eyelink/_utils.py +++ b/mne/io/eyelink/_utils.py @@ -196,7 +196,7 @@ def _validate_data(data_blocks: list): if "HREF" in units: logger.info("Head-referenced eye-angle (HREF) data detected.") elif "PUPIL" in units: - warn("Raw eyegaze coordinates detected. Analyze with caution.") + warn("Raw pupil position data detected. Analyze with caution.") if "AREA" in pupil_units: logger.info("Pupil-size area detected.") elif "DIAMETER" in pupil_units: diff --git a/mne/io/eyelink/tests/test_eyelink.py b/mne/io/eyelink/tests/test_eyelink.py index 911a52708a2..2af4170221f 100644 --- a/mne/io/eyelink/tests/test_eyelink.py +++ b/mne/io/eyelink/tests/test_eyelink.py @@ -399,7 +399,7 @@ def test_multi_block_misc_channels(fname, tmp_path): with ( _record_warnings(), - pytest.warns(RuntimeWarning, match="Raw eyegaze coordinates"), + pytest.warns(RuntimeWarning, match="Raw pupil position data detected"), pytest.warns(RuntimeWarning, match="The eye being tracked changed"), ): raw = read_raw_eyelink(out_file, apply_offsets=True) From e22ebb964515320cca9372408e50a12b578dc14c Mon Sep 17 00:00:00 2001 From: qian-chu Date: Wed, 7 Jan 2026 17:29:02 +0100 Subject: [PATCH 10/12] two more typos --- mne/io/eyelink/tests/test_eyelink.py | 2 +- mne/utils/docs.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mne/io/eyelink/tests/test_eyelink.py b/mne/io/eyelink/tests/test_eyelink.py index 2af4170221f..9faaf1c50f1 100644 --- a/mne/io/eyelink/tests/test_eyelink.py +++ b/mne/io/eyelink/tests/test_eyelink.py @@ -172,7 +172,7 @@ def test_fill_times(fname): def test_find_overlaps(): - """Test finding overlapping occular events between the left and right eyes. + """Test finding overlapping ocular events between the left and right eyes. In the simulated blink df below, the first two rows will be considered an overlap because the diff() of both the 'time' and diff --git a/mne/utils/docs.py b/mne/utils/docs.py index 54b2c9f1363..0e64a8d4008 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -1592,10 +1592,10 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): docdict["eyelink_create_annotations"] = """ create_annotations : bool | list (default True) - Whether to create :class:`~mne.Annotations` from occular events + Whether to create :class:`~mne.Annotations` from ocular events (blinks, fixations, saccades) and experiment messages. If a list, must contain one or more of ``['fixations', 'saccades',' blinks', messages']``. - If True, creates :class:`~mne.Annotations` for both occular events and + If True, creates :class:`~mne.Annotations` for both ocular events and experiment messages. """ From f9af1090f012f42e9fc1ce02038f7bd41f833b81 Mon Sep 17 00:00:00 2001 From: qian-chu Date: Wed, 7 Jan 2026 22:11:09 +0100 Subject: [PATCH 11/12] add simulation of block starting with empty samples without a status column --- mne/io/eyelink/eyelink.py | 4 +--- mne/io/eyelink/tests/test_eyelink.py | 31 ++++++++-------------------- 2 files changed, 10 insertions(+), 25 deletions(-) diff --git a/mne/io/eyelink/eyelink.py b/mne/io/eyelink/eyelink.py index af610e5f43b..1db31f70f4b 100644 --- a/mne/io/eyelink/eyelink.py +++ b/mne/io/eyelink/eyelink.py @@ -123,9 +123,7 @@ def __init__( eye_annots = _make_eyelink_annots( self._raw_extras[0]["dfs"], create_annotations, apply_offsets ) - if self.n_times == 0: - logger.info("No samples found in recording, skipping annotation creation.") - elif gap_annots and eye_annots: # set both + if gap_annots and eye_annots: # set both self.set_annotations(gap_annots + eye_annots) elif gap_annots: self.set_annotations(gap_annots) diff --git a/mne/io/eyelink/tests/test_eyelink.py b/mne/io/eyelink/tests/test_eyelink.py index 9faaf1c50f1..77483aaa5ca 100644 --- a/mne/io/eyelink/tests/test_eyelink.py +++ b/mne/io/eyelink/tests/test_eyelink.py @@ -360,7 +360,14 @@ def _simulate_eye_tracking_data(in_file, out_file): fp.write("START\t7452389\tRIGHT\tSAMPLES\tEVENTS\n") fp.write(f"{new_samples_line}\n") - for timestamp in np.arange(7452389, 7453390): # simulate a second block + # simulate a second block that starts with empty samples + for timestamp in np.arange(7452389, 7453154): + fp.write( + f"{timestamp}\t.\t.\t0.0\t.\t.\t.\t.\t0.0\t" + "...\t.\t.\t.\n" # no last column + ) + + for timestamp in np.arange(7453154, 7453390): # second block continues fp.write( f"{timestamp}\t-2434.0\t-1760.0\t840.0\t100\t20\t45\t45\t127.0\t" "...\t1497\t5189\t512.5\t.............\n" @@ -388,7 +395,7 @@ def _simulate_eye_tracking_data(in_file, out_file): @requires_testing_data @pytest.mark.parametrize("fname", [fname_href]) def test_multi_block_misc_channels(fname, tmp_path): - """Test a file with many edge casses. + """Test a file with many edge cases. This file has multiple acquisition blocks, each tracking a different eye. The coordinates are in raw units (not pixels or radians). @@ -524,23 +531,3 @@ def test_href_eye_events(tmp_path): 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) From bb406ae3e4b093b869d072c154b22e89c1df6d34 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 7 Jan 2026 21:11:42 +0000 Subject: [PATCH 12/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mne/io/eyelink/tests/test_eyelink.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mne/io/eyelink/tests/test_eyelink.py b/mne/io/eyelink/tests/test_eyelink.py index 77483aaa5ca..7df0745fbf8 100644 --- a/mne/io/eyelink/tests/test_eyelink.py +++ b/mne/io/eyelink/tests/test_eyelink.py @@ -364,7 +364,7 @@ def _simulate_eye_tracking_data(in_file, out_file): for timestamp in np.arange(7452389, 7453154): fp.write( f"{timestamp}\t.\t.\t0.0\t.\t.\t.\t.\t0.0\t" - "...\t.\t.\t.\n" # no last column + "...\t.\t.\t.\n" # no last column ) for timestamp in np.arange(7453154, 7453390): # second block continues @@ -530,4 +530,3 @@ 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 -