ValueError read_raw_eyelink missing channel name in annotations

Hi,

I recently started using the mne toolbox for the preprocessing of my Eyelink pupil data and for most files this works fantastically, but I have a few files where the read_raw_eyelink function gives me the following error:

Traceback (most recent call last):
  File "/home/msteijg/Desktop/topstore/2024_vanGaal_FMG-7251_NeuralArousal/Scripts/Scripts_eyetracker/0_preprocess_pupil_block.py", line 260, in <module>
    res = Parallel(n_jobs=n_jobs, verbose=1, backend='loky')(delayed(load_data)(filename, figs_dir) for filename in tqdm(asc_filenames))
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/msteijg/pip3_analysis/lib/python3.12/site-packages/joblib/parallel.py", line 1918, in __call__
    return output if self.return_generator else list(output)
                                                ^^^^^^^^^^^^
  File "/home/msteijg/pip3_analysis/lib/python3.12/site-packages/joblib/parallel.py", line 1847, in _get_sequential_output
    res = func(*args, **kwargs)
          ^^^^^^^^^^^^^^^^^^^^^
  File "/home/msteijg/Desktop/topstore/2024_vanGaal_FMG-7251_NeuralArousal/Scripts/Scripts_eyetracker/0_preprocess_pupil_block.py", line 82, in load_data
    raw_et = mne.io.read_raw_eyelink(filename, verbose="CRITICAL")
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/msteijg/pip3_analysis/lib/python3.12/site-packages/mne/io/eyelink/eyelink.py", line 59, in read_raw_eyelink
    raw_eyelink = RawEyelink(
                  ^^^^^^^^^^^
  File "<decorator-gen-204>", line 10, in __init__
  File "/home/msteijg/pip3_analysis/lib/python3.12/site-packages/mne/io/eyelink/eyelink.py", line 127, in __init__
    self.set_annotations(gap_annots + eye_annots)
  File "<decorator-gen-189>", line 12, in set_annotations
  File "/home/msteijg/pip3_analysis/lib/python3.12/site-packages/mne/io/base.py", line 747, in set_annotations
    new_annotations._prune_ch_names(self.info, on_missing)
  File "/home/msteijg/pip3_analysis/lib/python3.12/site-packages/mne/annotations.py", line 498, in _prune_ch_names
    _on_missing(
  File "/home/msteijg/pip3_analysis/lib/python3.12/site-packages/mne/utils/check.py", line 1218, in _on_missing
    raise error_klass(msg)
ValueError: At least one channel name in annotations missing from info: xpos_left

I checked the asc file that I feed to the function, but the data there looks fine. I think mne doesn’t recognize the channel names, but I can’t find where it gets it from, as xpos_left is also not present in the asc file that does run normally.

I also tried to find other people with this problem, but I couldn’t find anyone with this error while loading pupil data, rather than EEG for example.

Anyone know how I can check if this channel is actually missing or if it isn’t read out properly?

Many thanks!!

The relevant code I use is

# Convert edf to asc file
os.system('edf2asc {}'.format(edf_filename))

# Load asc as raw object
raw_et = mne.io.read_raw_eyelink(asc_filename)
  • MNE version 1.9.0
  • operating system: Linux
  • Python version 3.12.3

Hi @MargotSteijger

Is there any chance you can share the ASCII file? I developed the read_raw_eyelink function, and this would help me to identify the problem!

Scott

Thanks for your prompt response @scott-huberty !

Here is the link to one of the files that won’t run: Eyelink_testfile - Google Drive

Hey @MargotSteijger I think the problem arises because in this recording, the eye that was being tracked switched from left to right midway through the recording, and our reader doesn’t know how to handle that.

We’ve seen this before, and I have a PR open to fix the issue: BUG: read raw eyelink file that switches from binocular to monocular mode by scott-huberty · Pull Request #12847 · mne-tools/mne-python · GitHub

I’ll try to find some time in the next week or so to finish that PR.

Scott

Hi @scott-huberty

Thanks so much for having a look at it so fast! I checked the other files that gave this same error and I indeed see that there is a switch in left and right. I would never have thought of this as a potential issue. For now, I’ll write some code to change the word (most seem to flip before the actual experiment started), see if that gives me a (quick and) dirty solution. But I look forward to your fix!

Margot

Hi @scott-huberty,

I have another fun case that crashes the function :sweat_smile:

This time, half way through the file, the HTARGET option of Eyelink was turned on so there are more columns to deal with. Just dropping the error and a link to the file here. Seems like a less easy fix than replacing some words (which worked for the eye flipping btw!)

File: Eyelink_htarget - Google Drive

TypeError                                 Traceback (most recent call last)
File ~/Desktop/topstore/2024_vanGaal_FMG-7251_NeuralArousal/Scripts/Scripts_eyetracker/0_preprocess_pupil_block.py:258
      0 <Error retrieving source code with stack_data see ipython/ipython#13598>

File ~/pip3_analysis/lib/python3.12/site-packages/joblib/parallel.py:1918, in Parallel.__call__(self, iterable)
   1916     output = self._get_sequential_output(iterable)
   1917     next(output)
-> 1918     return output if self.return_generator else list(output)
   1920 # Let's create an ID that uniquely identifies the current call. If the
   1921 # call is interrupted early and that the same instance is immediately
   1922 # re-used, this id will be used to prevent workers that were
   1923 # concurrently finalizing a task from the previous call to run the
   1924 # callback.
   1925 with self._lock:

File ~/pip3_analysis/lib/python3.12/site-packages/joblib/parallel.py:1847, in Parallel._get_sequential_output(self, iterable)
   1845 self.n_dispatched_batches += 1
   1846 self.n_dispatched_tasks += 1
-> 1847 res = func(*args, **kwargs)
   1848 self.n_completed_tasks += 1
   1849 self.print_progress()

File ~/Desktop/topstore/2024_vanGaal_FMG-7251_NeuralArousal/Scripts/Scripts_eyetracker/0_preprocess_pupil_block.py:82, in load_data(filename, figs_dir)
     74 subj = os.path.basename(filename).split('_')[0]
     75 #ses = os.path.basename(filename).split('.')[0]
     76 #run = os.path.basename(filename).split('_')[-2]
     77 
   (...)
     80 # load pupil data:
     81 # try:
---> 82 raw_et = mne.io.read_raw_eyelink(filename)
     83 # except Exception as e:
     84 #     print(e)
     85 #     print(filename)
     86 #     return filename
     88 raw_et._data[np.isnan(raw_et._data)] = 0

File ~/pip3_analysis/lib/python3.12/site-packages/mne/io/eyelink/eyelink.py:59, in read_raw_eyelink(fname, create_annotations, apply_offsets, find_overlaps, overlap_threshold, verbose)
     29 """Reader for an Eyelink ``.asc`` file.
     30 
     31 Parameters
   (...)
     55 ``'BAD_ACQ_SKIP'``.
     56 """
     57 fname = _check_fname(fname, overwrite="read", must_exist=True, name="fname")
---> 59 raw_eyelink = RawEyelink(
     60     fname,
     61     create_annotations=create_annotations,
     62     apply_offsets=apply_offsets,
     63     find_overlaps=find_overlaps,
     64     overlap_threshold=overlap_threshold,
     65     verbose=verbose,
     66 )
     67 return raw_eyelink

File <decorator-gen-204>:12, in __init__(self, fname, create_annotations, apply_offsets, find_overlaps, overlap_threshold, verbose)

File ~/pip3_analysis/lib/python3.12/site-packages/mne/io/eyelink/eyelink.py:104, in RawEyelink.__init__(self, fname, create_annotations, apply_offsets, find_overlaps, overlap_threshold, verbose)
    101 fname = Path(fname)
    103 # ======================== Parse ASCII file ==========================
--> 104 eye_ch_data, info, raw_extras = _parse_eyelink_ascii(
    105     fname, find_overlaps, overlap_threshold, apply_offsets
    106 )
    107 # ======================== Create Raw Object =========================
    108 super().__init__(
    109     info,
    110     preload=eye_ch_data,
   (...)
    113     raw_extras=[raw_extras],
    114 )

File ~/pip3_analysis/lib/python3.12/site-packages/mne/io/eyelink/_utils.py:55, in _parse_eyelink_ascii(fname, find_overlaps, overlap_threshold, apply_offsets)
     52 _validate_data(raw_extras)
     54 # ======================== Create DataFrames ========================
---> 55 raw_extras["dfs"] = _create_dataframes(raw_extras, apply_offsets)
     56 del raw_extras["sample_lines"]  # free up memory
     57 # add column names to dataframes and set the dtype of each column

File ~/pip3_analysis/lib/python3.12/site-packages/mne/io/eyelink/_utils.py:281, in _create_dataframes(raw_extras, apply_offsets)
    279 # dataframe for samples
    280 df_dict["samples"] = pd.DataFrame(raw_extras["sample_lines"])
--> 281 df_dict["samples"] = _drop_status_col(df_dict["samples"])  # drop STATUS col
    283 # dataframe for each type of occular event
    284 for event, label in zip(
    285     ["EFIX", "ESACC", "EBLINK"], ["fixations", "saccades", "blinks"]
    286 ):

File ~/pip3_analysis/lib/python3.12/site-packages/mne/io/eyelink/_utils.py:341, in _drop_status_col(samples_df)
    339 # we know the first 3 columns will be the time, xpos, ypos
    340 for col in samples_df.columns[3:]:
--> 341     if samples_df[col][0][0].isnumeric():
    342         # if the value is numeric, it's not a status column
    343         continue
    344     if len(samples_df[col][0]) in [3, 5, 13, 17]:

TypeError: 'NoneType' object is not subscriptable

Aha :sweat_smile: thanks for sharing @MargotSteijger !!

So it seems that we can’t assume that the channels present in an EyeLink file are static throughout the recording, which is indeed fun… I’ll set aside some time to see how easy this is to patch, in the next week or so.

1 Like