Summary: I used time.sleep(3) to sustain a visual stimulus while keyboard RTs were supposedly being recorded. However they weren’t, there is a “deadzone” in the data around that sleep time, and then some very very long RTs are being reported post-sleep. I am hopeful that these erroneously-long RTs can be “repaired” with a simple subtraction of 3 seconds, but don’t know enough about how these times were logged to know if that’s a reasonable assumption.
This is on PsychoPy version 2022.1.4 on a macOSMonterey version 12.4
This issue is similar to this post from 5 years ago.
Full disclosure that this code is very rough, this experiment was a student’s first learning experience. However, 70+ participants have been collected, so any amount of data that can be reasonably salvaged would be marvelous.
The project is a audio-tactile dual-task paradigm, meaning that on each trial, the participant simultaneously A) listens to an audiofile, and B) feels a small motor vibrate in their hand, and responds as quickly as possible on the keyboard. There is also a fixation cross on screen that alternates between white and green. Critically, the fixation cross is white for the duration of the audiofile plus 3 seconds, then turns green. This 3 second “delay” was implemented using time.sleep(3), and I believe this is the cause of my very strange RT distribution, which I have attached below.
(The left panel (ST) is from a simply-coded “practice” where the participant ONLY performs the touch-task, and the fixation cross is not manipulated during the trial. Note the reasonable distribution of the RTs. The right panel (DT) is from the rougher-coded dual-task experiment where the code is managing the audio, vibration, and fixation cross. Note the bimodal distribution of the RTs. There was no experimental manipulation that would create this distribution, I am 100% sure it is a coding mishap, and am 95% sure it’s due to the fixation cross delay.
As far as I’ve sussed out, I believe that any RTs occurring after the audiofile has ended are being sucked into limbo during time.sleep(3) and spat out afterward with inaccurate values. As such, my question is this: for those abnormally-long RTs, could they be “repaired” by subtracting 3seconds? Or is this post-sleep processing too variable and unreliable too repair that upper cloud of data?
Here’s the full code for running the routine, I’ve bolded the hypothesized offending portion but I’ve included it all since it’s admittedly very janky and that might be relevant.
# -------Run Routine "DT"-------
while continueRoutine:
# get current time
t = DTClock.getTime()
tThisFlip = win.getFutureFlipTime(clock=DTClock)
tThisFlipGlobal = win.getFutureFlipTime(clock=None)
frameN = frameN + 1 # number of completed frames (so 0 is the first frame)
# update/draw components on each frame
# *text_fix* updates
if text_fix.status == NOT_STARTED and tThisFlip >= 0.0-frameTolerance:
# keep track of start time/frame for later
text_fix.frameNStart = frameN # exact frame index
text_fix.tStart = t # local t and not account for scr refresh
text_fix.tStartRefresh = tThisFlipGlobal # on global time
win.timeOnFlip(text_fix, 'tStartRefresh') # time at next scr refresh
text_fix.setAutoDraw(True)
win.flip()
# start/stop sound_LP
if sound_LP.status == NOT_STARTED and tThisFlip >= 0.0-frameTolerance:
# keep track of start time/frame for later
sound_LP.frameNStart = frameN # exact frame index
sound_LP.tStart = t # local t and not account for scr refresh
sound_LP.tStartRefresh = tThisFlipGlobal # on global time
sound_LP.play(when=win) # sync with win flip
win.flip()
time.sleep(start_interval)
motoron()
time.sleep(duration)
motoroff()
# *key_resp_DT* updates
waitOnFlip = False
if key_resp_DT.status == NOT_STARTED and tThisFlip >= 0.0-frameTolerance:
# keep track of start time/frame for later
key_resp_DT.frameNStart = frameN # exact frame index
key_resp_DT.tStart = t # local t and not account for scr refresh
key_resp_DT.tStartRefresh = tThisFlipGlobal # on global time
win.timeOnFlip(key_resp_DT, 'tStartRefresh') # time at next scr refresh
key_resp_DT.status = STARTED
# keyboard checking is just starting
waitOnFlip = True
win.callOnFlip(key_resp_DT.clock.reset) # t=0 on next screen flip
win.callOnFlip(key_resp_DT.clearEvents, eventType='keyboard') # clear events on next screen flip
if key_resp_DT.status == STARTED and not waitOnFlip:
theseKeys = key_resp_DT.getKeys(keyList=["j", "k", "l"], waitRelease=False)
_key_resp_DT_allKeys.extend(theseKeys)
if len(_key_resp_DT_allKeys):
key_resp_DT.keys = _key_resp_DT_allKeys[-1].name # just the last key pressed
key_resp_DT.rt = _key_resp_DT_allKeys[-1].rt
# was this correct?
if (key_resp_DT.keys == str(correct_key)) or (key_resp_DT.keys == correct_key):
key_resp_DT.corr = 1
else:
key_resp_DT.corr = 0
**# *text_fixRepeat* updates**
** if text_fixRepeat.status == NOT_STARTED and t>= (sound_LP.getDuration() + sound_LP.tStart):**
** # keep track of start time/frame for later**
** text_fixRepeat.frameNStart = frameN # exact frame index**
** text_fixRepeat.tStart = t # local t and not account for scr refresh**
** text_fixRepeat.tStartRefresh = tThisFlipGlobal # on global time**
** win.timeOnFlip(text_fixRepeat, 'tStartRefresh') # time at next scr refresh**
** time.sleep(3.0) #this is the pause after the sentence before the green _fixRepeat appears**
** text_fixRepeat.setAutoDraw(True)**
# *key_resp_ADV* updates, advancing via spacebar to next trial
waitOnFlip = False
if key_resp_ADV.status == NOT_STARTED and tThisFlip >= 0.0-frameTolerance:
# keep track of start time/frame for later
key_resp_ADV.frameNStart = frameN # exact frame index
key_resp_ADV.tStart = t # local t and not account for scr refresh
key_resp_ADV.tStartRefresh = tThisFlipGlobal # on global time
win.timeOnFlip(key_resp_ADV, 'tStartRefresh') # time at next scr refresh
key_resp_ADV.status = STARTED
# keyboard checking is just starting
waitOnFlip = True
win.callOnFlip(key_resp_ADV.clock.reset) # t=0 on next screen flip
win.callOnFlip(key_resp_ADV.clearEvents, eventType='keyboard') # clear events on next screen flip
if key_resp_ADV.status == STARTED and not waitOnFlip:
theseKeys = key_resp_ADV.getKeys(keyList=["space"], waitRelease=False)
_key_resp_ADV_allKeys.extend(theseKeys)
if len(_key_resp_ADV_allKeys):
key_resp_ADV.keys = _key_resp_ADV_allKeys[-1].name # just the last key pressed
key_resp_ADV.rt = _key_resp_ADV_allKeys[-1].rt
# a response ends the routine
time.sleep(0.5) #pauses a quick bit after the space press
text_fixPause.setAutoDraw(True)
win.flip()
time.sleep(1.5) #pauses on the white fix before the sentence starts
continueRoutine = False
# check for quit (typically the Esc key)
if endExpNow or defaultKeyboard.getKeys(keyList=["escape"]):
core.quit()
# check if all components have finished
if not continueRoutine: # a component has requested a forced-end of Routine
break
continueRoutine = False # will revert to True if at least one component still running
for thisComponent in DTComponents:
if hasattr(thisComponent, "status") and thisComponent.status != FINISHED:
continueRoutine = True
break # at least one component has not yet finished
# refresh the screen
if continueRoutine: # don't flip if this routine is over or we'll get a blank screen
win.flip()