Hello everyone!
Our PsychoPy task is doing this thing where hard-coded epoch durations within the trial (e.g., a 2.5 second decision phase) are logging much longer durations if the participant takes a slightly longer time to respond. For example in a 2.5 second decision epoch, if the participant responds in 3.03 seconds, it’s logging the duration as 3.03 seconds, and the next onset is thus also delayed relative to the extended previous duration, and thus the whole task is often lengthened by quite a bit.
To be clear, the task does move on to the next epoch and count that trial as missed eventually, but it’s unclear to me how long it’s allowing these late trials before it actually classifies it as missed. Additionally, fully missed trials seem to be very accurately timed (i.e., exactly matching the hard-coded duration), so these lengthened durations seem to only occur if the button press comes in relatively close to the hard-coded end of that phase/screen. The most egregious I’ve seen is an extended duration of ~0.43 seconds (duration hard-coded to be 2.75 seconds, response time logged as 3.2642114, duration of decision phase screen logged as 3.2645233), but most are less off of the hard-coded duration than that.
My concern for the fMRI analyses is if I specify that the decision phase has a duration of 2.5 seconds, but some of those durations are actually 3 seconds or more, then I might be pushing some of that epoch into the implicit baseline. Thus, I need the task to cut people off, log the trial as missed, and move on, even if their button press comes in a touch late.
We have encountered this on different computers (some Mac, some PC) with a a range of different PsychoPy versions (we most commonly use v2023.2.2), so I think this is not an issue specific to any one set-up, but more about me not understanding how to enforce specific durations! Any help or direction is sincerely appreciated.
Here is where we specify the length of the decision epoch:
## How long participants have to make their decision
if task_type == 'Encoding':
dur_decision = 2.5
elif task_type == "Recall":
dur_decision = 2.75
Here is an example of how we set the timing for the whole trial:
trial_timer = core.CountdownTimer(dur_decision + dur_isi + dur_feedback + dur_iti)
And here is an example for the main set up of a decision phase (apologies for the long block of code):
## While there's still time on the trial clock, do the following
while trial_timer.getTime() > 0:
# ----- Decision -----
## Start a countdown timer according to the duration of this event
timer = core.CountdownTimer(dur_decision)
## Look for keys that we put on our response list
resp = event.getKeys(keyList = responseKeys)
## Get the time when this event starts
decision_onset = clock.getTime()
## Pulling frame and stimulus information from the reference document
if task_type == "Encoding":
if trial_type[row_counter] == 'win':
top_frame = top_frame_win
bottom_frame = bottom_frame_win
left_frame = left_frame_win
right_frame = right_frame_win
else:
top_frame = top_frame_loss
bottom_frame = bottom_frame_loss
left_frame = left_frame_loss
right_frame = right_frame_loss
## While the timer for this event is still counting, do the following
while timer.getTime() > 0:
## Draw task-specific objects (i.e., text, frames, pictures, etc.)
if task_type == "Encoding":
pic_L.draw()
pic_R.draw()
top_frame.draw()
bottom_frame.draw()
left_frame.draw()
right_frame.draw()
elif task_type == "Recall":
pic.draw()
feedback_positive_L.draw()
feedback_negative_L.draw()
feedback_neutral_L.draw()
feedback_positive_R.draw()
feedback_negative_R.draw()
feedback_neutral_R.draw()
text_left.draw()
text_right.draw()
win.flip()
## Accept keys on the response key list
resp = event.getKeys(keyList = responseKeys)
## If a key from that list is pressed ...
if len(resp) > 0:
## ... and it's a z ...
if 'z' in resp:
if task_type == "Recall" and vers != "practice":
## ... quit the task and save our current progress as a .csv
bidsEvents.to_csv(os.path.join("data",subj_id, f"sub-{subj_id}_{task_type}_{domain}_run{run}_{date}.tsv"), sep='\t', index = False)
else:
## ... quit the task and save our current progress as a .csv
bidsEvents.to_csv(os.path.join("data",subj_id, f"sub-{subj_id}_{task_type}_{domain}_run{run}_{vers}_{date}.tsv"), sep='\t', index = False)
core.quit()
## If it's an encoding task, record the responses, draw the frames that people select
if task_type == "Encoding":
if selected == 7 or 2:
# If the keypad was used, remove the "num_" prefix from the response
if resp[0].startswith("num_"):
resp[0] = resp[0][4:]
selected = int(resp[0])
if selected == 7:
pic_L.draw()
pic_R.draw()
top_frame.draw()
bottom_frame.draw()
left_frame.draw()
right_frame.draw()
l_r = 'left'
select_2.draw()
win.flip()
core.wait(.5)
elif selected == 2:
pic_R.draw()
pic_L.draw()
top_frame.draw()
bottom_frame.draw()
left_frame.draw()
right_frame.draw()
l_r = 'right'
select_3.draw()
win.flip()
core.wait(.5)
## Get the time at which the response was recorded
resp_onset = clock.getTime()
## Calculate the response time
rt = resp_onset - decision_onset
## Redraw relevant objects
border.autoDraw=True
border2.autoDraw=True
pic_L.draw()
pic_R.draw()
## Wait the rest of the time for this event
core.wait(dur_decision - rt)
break
And lastly here is an example of logging trials as missed (this “else” belongs to the “if len(resp) > 0:” line in the above code block)
# If no response was given, consider this event missed
else:
resp = 'missed'
selected = 'missed'
rt = 'missed'
decision_eventtime = clock.getTime() - decision_onset
border.autoDraw=False
Let me know if there is any other code snippets or info I can provide. Thank you so very much in advance!!