Epoching eyetracking data caused pupil data to be NaN'd

System setup:

Platform             Windows-10-10.0.19045-SP0
Python               3.13.5 | packaged by conda-forge | (main, Jun 16 2025, 08:20:19) [MSC v.1943 64 bit (AMD64)]
Executable           A:\Subjects\anaconda\envs\mne\python.exe
CPU                  Intel(R) Core(TM) i5-10300H CPU @ 2.50GHz (8 cores)
Memory               7.8 GiB

Core
 + mne               1.10.1 (latest release)
 + numpy             2.2.6 (unknown linalg bindings)
 + scipy             1.16.1
 + matplotlib        3.10.5 (backend=qtagg)

Numerical (optional)
 + sklearn           1.7.1
 + numba             0.61.2
 + nibabel           5.3.2
 + nilearn           0.12.0
 + dipy              1.11.0
 + openmeeg          2.5.15
 + cupy              13.5.1
 + pandas            2.3.1
 + h5io              0.2.5
 + h5py              3.14.0

Problem:

Data: Eyelink 1000+ raw, converted with EDF2ASC using default settings

Issues: As described in the title, after epoching, all or most pupil data became NaN values for some trials, even though the raw data was continuous without NaN segment.

Processing pipeline:

Raw data: interpolate_blinks with 10ms buffer, followed by 4th order Butterworth low-pass, then generate annotations from PsychoPy record (where the experiments were run) to label trials for later rejection

Epoching: events_from_annotations() followed by mne.Epoch. This is where pupil data went missing.

MWE:

### Raw data imports

el_calib = mne.preprocessing.eyetracking.read_eyelink_calibration(f, screen_size=size, screen_distance=scr_dist, screen_resolution=(1920, 1080))

mne.preprocessing.eyetracking.interpolate_blinks(raw=el_raw, buffer=(0.01, 0.01), interpolate_gaze=True) 

# 4th order Butterworth low-pass for pupil data
el_raw = el_raw.filter(l_freq=None, h_freq=4.0, picks='pupil_right', method='iir', iir_params=dict(order=4, ftype='butter'),
      phase='zero-double', verbose=False)
el_rawdict[el_parID] = (el_raw,el_events_stages,el_calib)

raw_stage = el_raw.copy().set_annotations(stage_anno)
    el_stageddict[p] = raw_stage

### Labelling:

for p in el_rawdict: # 
    rt = rt_rawdict[p]
    rt_anno = {}
    cond_anno_dict = {'cond':(blahblah), 
                      'exclude':rt['exclude']}
    # Checking if annotations match between RT and EL files
    el_raw = el_rawdict[p][0] if el_rawdict else None
    orig_time = el_raw.annotations.orig_time
    check_mask = np.isin(el_raw.annotations.description, el_st_conds)
    el_check = el_raw.annotations.description[check_mask]
    assert len(el_check)==len(rt)
    for i in range(len(el_check)):
        assert (cond_anno_mapping[rt['direction'].iloc[i]] in el_check[i])

    for anno_type, anno_desc in cond_anno_dict.items():
        rt_anno[anno_type] = mne.Annotations(
            onset=el_raw.annotations[el_raw.annotations.description=='TARGET_ONSET'].onset,
                duration=(rt['exp.stopped']-rt['exp_target_rect.started']).tolist(),
                description=anno_desc.tolist(),
                orig_time=orig_time)
    
    # Affix P's RT response annotations to EL
    if map_rt_labels_to_el:
        # extract stage anno from EL file
        raw_anno = el_raw.annotations
        stage_mask = np.isin(raw_anno.description, el_stage_desc)
        ocular_mask = np.isin(raw_anno.description, el_ocular_desc)
        stage_anno = raw_anno[stage_mask+ocular_mask] + (rt_anno['cond']+rt_anno['resp']+rt_anno['exclude'])

raw_stage = el_raw.copy().set_annotations(stage_anno)
    el_stageddict[p] = raw_stage

### Epoching: 

for p in el_stageddict:
    el_raw = el_stageddict[p]
    el_stages, el_stages_anno = mne.events_from_annotations(raw=el_raw,event_id=cond_dict)

    # Extract events and create epochs around TARGET_ONSET annotation
    epochs = mne.Epochs(raw=el_raw, events=el_stages, tmin=-0.3, tmax=1.0,
                        reject_by_annotation=True, event_id=cond_dict, baseline=None, preload=True, on_missing='warn')

For some trials, all pupil data were missing after epoching, for others, only pupil data in the last 30ms were preserved. Since epoch.plot() doesn’t show annotations I cannot tell whether these were results of blinks in the raw data - but then all blinks were interpolated beforehand.

I’ve received no error message when running the codes, other than my own debugging messages

On a side note, I’d really appreciate it if MNE-Python could add support for either variable-length epochs, or standardisation of epoch lengths by scaling (e.g. making the start and end of each trial as 0 and 1, as per Ludwig & Gilchrist 2002’s saccade curvature analysis method)

MWE:

Just a friendly tip that other folks cannot run this minimally working example because it does not define all of the variables that are referenced within it.

But more to point, @Cathaway can you try this?

nan_annots = mne.preprocessing.annotate_nan(raw)

raw.set_annotations(raw.annotations + nan_annots)

# Or you can use your epoching code
epochs = mne.make_fixed_length_epochs(raw)
epochs.plot(scalings=dict(eyegaze="auto", pupil="auto"))