diff --git a/mne/io/eyelink/_utils.py b/mne/io/eyelink/_utils.py index 7ac03e088fd..a4d5f5bf16d 100644 --- a/mne/io/eyelink/_utils.py +++ b/mne/io/eyelink/_utils.py @@ -324,7 +324,22 @@ def _create_dataframes(raw_extras, apply_offsets): cols = ["time", "end_time", "block"] df_dict["recording_blocks"] = pd.DataFrame(blocks, columns=cols) - # TODO: Make dataframes for other eyelink events (Buttons) + # make dataframes for other button events + if raw_extras["event_lines"]["BUTTON"]: + button_events = raw_extras["event_lines"]["BUTTON"] + parsed = [] + for entry in button_events: + parsed.append( + { + "onset": float(entry[0]), + "button_id": int(entry[1]), + "button_pressed": int(entry[2]), # 1 = press, 0 = release + } + ) + df_dict["buttons"] = pd.DataFrame(parsed) + n_button = len(df_dict.get("buttons", [])) + logger.info(f"Found {n_button} button event{_pl(n_button)} in this file.") + return df_dict @@ -416,6 +431,10 @@ def _assign_col_names(col_names, df_dict): elif key == "messages": cols = ["time", "offset", "event_msg"] df.columns = cols + # added for buttons + elif key == "buttons": + cols = ["time", "button_id", "button_pressed"] + df.columns = cols return df_dict @@ -677,7 +696,7 @@ def _make_eyelink_annots(df_dict, create_annots, apply_offsets): "pupil_right", ), } - valid_descs = ["blinks", "saccades", "fixations", "messages"] + valid_descs = ["blinks", "saccades", "fixations", "buttons", "messages"] msg = ( "create_annotations must be True or a list containing one or" f" more of {valid_descs}." @@ -718,11 +737,29 @@ def _make_eyelink_annots(df_dict, create_annots, apply_offsets): onsets = df["time"] durations = [0] * onsets descriptions = df["event_msg"] + this_annot = Annotations( + onset=onsets, duration=durations, description=descriptions + ) + elif (key == "buttons") and (key in descs): + required_cols = {"time", "button_id", "button_pressed"} + if not required_cols.issubset(df.columns): + raise ValueError(f"Missing column: {required_cols - set(df.columns)}") + + def get_button_description(row): + button_id = int(row["button_id"]) + action = "press" if row["button_pressed"] == 1 else "release" + return f"button_{button_id}_{action}" + + df = df.sort_values("time") + onsets = df["time"] + durations = np.zeros_like(onsets) + descriptions = df.apply(get_button_description, axis=1) + this_annot = Annotations( onset=onsets, duration=durations, description=descriptions ) else: - continue # TODO make df and annotations for Buttons + continue if not annots: annots = this_annot elif annots: diff --git a/mne/io/eyelink/tests/test_eyelink.py b/mne/io/eyelink/tests/test_eyelink.py index b5239c7fe14..08ed52f98dc 100644 --- a/mne/io/eyelink/tests/test_eyelink.py +++ b/mne/io/eyelink/tests/test_eyelink.py @@ -201,6 +201,19 @@ def _simulate_eye_tracking_data(in_file, out_file): "SAMPLES\tPUPIL\tLEFT\tVEL\tRES\tHTARGET\tRATE\t1000.00" "\tTRACKING\tCR\tFILTER\t2\tINPUT" ) + + # Define your known BUTTON events + button_events = [ + (5488529, 1, 0), + (5488532, 1, 1), + (5488540, 1, 0), + (5488543, 1, 1), + (5488550, 1, 0), + (5488553, 1, 1), + (5488571, 1, 0), + ] + button_idx = 0 + with out_file.open("w") as fp: in_recording_block = False events = [] @@ -214,7 +227,7 @@ def _simulate_eye_tracking_data(in_file, out_file): if event_type.isnumeric(): # samples tokens[4:4] = ["100", "20", "45", "45", "127.0"] # vel, res, DIN tokens.extend(["1497.0", "5189.0", "512.5", "............."]) - elif event_type in ("EFIX", "ESACC"): + elif event_type in ("EFIX", "ESACC", "BUTTON"): if event_type == "ESACC": tokens[5:7] = [".", "."] # pretend start pos is unknown tokens.extend(["45", "45"]) # resolution @@ -224,6 +237,15 @@ def _simulate_eye_tracking_data(in_file, out_file): tokens.append("INPUT") elif event_type == "EBLINK": continue # simulate no blink events + elif event_type == "BUTTON": + # simulate a button event + tokens[1] = "BUTTON" # simulate button press + tokens[2] = "1" # simulate button 1 + tokens[3] = "1" # simulate button pressed + tokens[4:4] = ["100", "20", "45", "45", "127.0"] + tokens.extend(["1497.0", "5189.0", "512.5", "............."]) + + continue elif event_type == "END": pass else: @@ -246,6 +268,22 @@ def _simulate_eye_tracking_data(in_file, out_file): "...\t1497\t5189\t512.5\t.............\n" ) + for timestamp in np.arange(5488500, 5488600): # 100 samples + 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" + ) + # Check and insert button events at this timestamp + while ( + button_idx < len(button_events) + and button_events[button_idx][0] == timestamp + ): + t, btn_id, state = button_events[button_idx] + fp.write( + f"BUTTON\t{t}\t{btn_id}\t{state}\t100\t20\t45\t45\t127.0\t" + "1497.0\t5189.0\t512.5\t.............\n" + ) + button_idx += 1 fp.write("END\t7453390\tRIGHT\tSAMPLES\tEVENTS\n")