Skip to content

Add buttons #13287

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 40 additions & 3 deletions mne/io/eyelink/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this _pl function was supposed to add an s character in the case of multiple button events (because _pl is not defined), but I think the logging message below is good enough?

Suggested change
logger.info(f"Found {n_button} button event{_pl(n_button)} in this file.")
logger.info(f"Found {n_button} button event(s) in this file.")


return df_dict


Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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}."
Expand Down Expand Up @@ -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:
Expand Down
40 changes: 39 additions & 1 deletion mne/io/eyelink/tests/test_eyelink.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
]
Comment on lines +205 to +214
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# 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),
]
# Define a few BUTTON press events
button_events = [
(7453400, 1, 0),
(7453410, 1, 1),
(7453420, 1, 0),
(7453430, 1, 1),
(7453440, 1, 0),
(7453450, 1, 1),
(7453460, 1, 0),
]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am just making sure that the timestamps here line up with the adjustments I made below.

button_idx = 0

with out_file.open("w") as fp:
in_recording_block = False
events = []
Expand All @@ -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"):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
elif event_type in ("EFIX", "ESACC", "BUTTON"):
elif event_type in ("EFIX", "ESACC"):

if event_type == "ESACC":
tokens[5:7] = [".", "."] # pretend start pos is unknown
tokens.extend(["45", "45"]) # resolution
Expand All @@ -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
Comment on lines +240 to +248
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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

Hint: This simulated eyetracking data is built off the actual eyetracking test file ~/mne_data/MNE-testing-data/eyetrack/test_eyelink_HREF.asc, which doesn't have any button events! So the code you added there will never actually be hit : )

elif event_type == "END":
pass
else:
Expand All @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for timestamp in np.arange(5488500, 5488600): # 100 samples
# And let's add some buttons events to this block
for timestamp in np.arange(7453390, 7453490): # 100 more samples

So these new lines you appended to the end of the simulated file, actually had earlier timestamps than the previous lines, which breaks things. So I'm just making sure the timestamps are increasing.

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
):
Comment on lines +277 to +280
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The while statement works, but you don't really need a loop here, you can just use a conditional statement:

Suggested change
while (
button_idx < len(button_events)
and button_events[button_idx][0] == timestamp
):
if 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like reasonable modifications, but later I'd expect some assertions about the raw.annotations or something for example that the right number of button events occurred etc.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@WouterKroot do you need more guidance on this part specifically?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes please, not sure that I follow. What do you mean by assertions, as in tests?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if you look later in the file you see stuff like

assert raw.annotations.description[1] == "SYNCTIME"
assert raw.annotations.description[-1] == "BAD_ACQ_SKIP"

I would expect similar assert statements to work here. So some of the annotations now should have some button description, there should be a specific number that have a button description, etc. So something like the following (untested, needs to be adapted)

button_idx = [ii for ii, desc enumerate(raw.annotations.description) if "button" in desc.lower()]
assert len(button_idx) == 6  # or however many you added
assert_allclose(raw.annotations.onset[button_idx[0]], 1.2356, atol=1e-5)  # or whatever the onset of the first simulated button press was

make sense?

And you can add to the test and test locally with

pytest mne/io/eyelink -k multi_block_misc

to make sure it passes before pushing

fp.write("END\t7453390\tRIGHT\tSAMPLES\tEVENTS\n")


Expand Down
Loading