Hi everybody,
I’m programming a sustained attention task, but I have reaction times longer than expected. They are in the range of 2 - 4 s, while I’m sure I’m pressing the response key (SPACE) a lot sooner (< 1s). My guess is that core.wait() is adding time to the RTs, but if I subtract the trial by trial core.wait() times to the RTs I obtain a value far shorter than expected, in the implausible order of 1 * 10 ^ -4. What I’m doing wrong?
I’m running PsychoPy v1.85.3 on Debian GNU/Linux 8 (jessie). A minimal example of the code is the following:
trial = 1
while trial < n_trials:
event.clearEvents()
# Set height and letter of the stimulus
stimuli_text.setHeight(variable_height)
stimuli_text.setText('%s' % variable_stim)
stimuli_text.draw()
mainWindows.flip()
# Initialise the counter
startTime = time.time()
# Display the stimulus for 100 milisecond
core.wait(0.1)
# Draw a fixation cross
stimuli_text.setText('+')
stimuli_text.draw()
mainWindows.flip()
# Display the fixation cross for a random interval between 1900 and 3900 milliseconds
core.wait(fixation_dewell_times[trial])
# Begin to listen for a key press
key = event.getKeys(keyList=keyList)
if key:
# Get the time when a key is pressed
endTime = time.time()
# Count a hit if space is pressed and the letter is not the target
if key == ['space'] and stim != target:
hit_counter += 1
# Count an error if space is pressed and the letter is the target
if key == ['space'] and stim == target:
error_counter += 1
# Get the reaction time
RT = endTime - startTime
else:
RT = -1
print RT
# Add to trial and initialise a new trial
trial += 1
You need to abandon your use of core.wait() if you care about timing. It’s just a convenience function for non-time critical purposes.
e.g. it makes no sense to display a stimulus, then use a period of core.wait() to have it shown for a variable amount of time, and then check for a response with event.getKeys(). What will happen is that you press a key, at some time likely quite early in the wait period, which then simply goes into a buffer waiting to be processed. Because your PsychoPy code is doing nothing active during that period, that keyboard event will indeed just sit there in the buffer going stale. Then when the wait period is over, you call event.getKeys() and, if there was a keypress, record the current time. Note that this time bears no relationship to when the key was actually pressed. If the wait period was, say, 1900 ms, regardless of the key actually being pressed at 200 ms, your measured RT value will be slightly later than 1900 ms, as you are measuring when the keypress was processed, not when it occurred.
What you need to do is actively monitor the keyboard throughout the stimulus period, so that the time of processing the key press occurs very soon after it actually occurred. There are two ways of doing this. Either draw the stimulus repeatedly and call win.flip() in a loop. This means that your keypress detection happens once on every screen refresh. Or you draw the stimulus once and then have a tight loop just checking the keyboard. This will give you slightly better RT resolution, as you can be checking multiple times within a single screen refresh period, but makes the code more complicated (and you probably will need to sleep for a millisecond or so on each iteration to avoid issues with losing access to the CPU, due to competing processes getting starved, which is avoided automatically if you use the win.flip() approach).
Look through the list of demos available from the Coder “demos” menu for some examples of how to do this active drawing/keyboard monitoring, to when yourself off the core.wait() approach.
The problem is that you calculate the RT incorrectly.
Your trial flow is like:
get startTime ->
wait 100ms ->
show the stimulus ->
wait for a random interval between 1900 and 3900 milliseconds ->
get endTime ->
RT = endTime - startTime
Your “RT” is acutally real RT + 100 ms plus the random jitter between 1900~3900 ms.
If you respond really fast (~500 ms), you will approximately get 2.5 s as the result.
A simple but less precise solution is to use core.Clock:
# create a Clock instance
timer = core.Clock()
trial = 1
while trial < n_trials:
event.clearEvents()
# Set height and letter of the stimulus
stimuli_text.setHeight(variable_height)
stimuli_text.setText('%s' % variable_stim)
stimuli_text.draw()
mainWindows.flip()
# Display the stimulus for 100 milisecond
core.wait(0.1)
# Draw a fixation cross
stimuli_text.setText('+')
stimuli_text.draw()
mainWindows.flip()
# Display the fixation cross for a random interval between 1900 and 3900 milliseconds
core.wait(fixation_dewell_times[trial])
# reset the timer
timer.reset()
# Begin to collect all the key responses
waitKey = True
while waitKey:
keys = event.getKeys(keyList=keyList)
for key in keys:
# Get the time when space is pressed
if key == ['space']:
# get RT
RT = timer.getTime()
# Count a hit if space is pressed and the letter is not the target
if stim != target:
hit_counter += 1
else:
error_counter += 1
# break the loop
waitKey = False
print RT
# Add to trial and initialise a new trial
trial += 1
But it’s still a bad practice to use core.Clock for critical timing. I agree with Michael’s point; you should use frames for better timing.
Hi Michael and Yuhan,
thanks a lot for your replies.
The RTs improved a lot including your advices. I guess that the better way to rewrote it is as following:
#####################
# Create the main windows
mainWindows = visual.Window(monitor='testMonitor', size=screen_size, units='cm', color=background_color, fullscr=True)
refresh_rate = int(mainWindows.getActualFrameRate(nIdentical=100, nMaxFrames=1000, nWarmUpFrames=10, threshold=1))
# Game parameters
n_trials = 50
stim_dwell_frames = int(ceil(0.1 * refresh_rate))
fixation_dewell_frames = [int(random.uniform(1.9, 3.9) * refresh_rate) for dewell in range(n_trials)]
def mainFlow(n_trials = n_trials):
keyList=['space', 'q']
hit_counter = 0
miss_counter = 0
for trial in range (n_trials):
event.clearEvents()
# Draw target stimulus
stimuli_text.setText('target')
stimuli_text.draw()
# Define trial length
trial_length = stim_dwell_frames + fixation_dewell_frames[trial]
RTs = []
# Iterate across the whole trial length
for frame in xrange(trial_length):
# Change to fixation point when the frame number is greater than the numbers of frames defined for the stimulus
if frame == stim_dwell_frames + 1:
stimuli_text.setText('')
stimuli_text.draw()
# Flips on every iteration
mainWindows.flip()
# Listen for a key press
key = event.getKeys(keyList=keyList)
if key:
# Collect the frames number where a key is pressed, and convert it to milliseconds (only the first press will be used)
RTs.append(frame * refresh_rate)
if key == ['space'] and stim != target:
hit_counter += 1
if key == ['space'] and stim == target:
miss_counter += 1
# If it is the last frame, take the RT for the first key press, or -1 if none was pressed
if frame == trial_length - 1:
if len(RTs) > 0:
RT = RTs[0]
else:
RT = -1
print RT
Am I right? Please let me know if I am making some mistake, of if there is a better way to write it.
Hi Mauricio, this is essentially correct: you’re now actively redrawing on every screen refresh and monitoring for responses in the same cycle, rather than having periods of static stimuli with no response monitoring possible.
Tip 1: don’t keep alternating the text value of your text stimulus. At present, this is a relatively slow operation compared to updating the attributes of other stimuli. In this case, better performance would be to create two constant stimuli, one containing 'target' and one for the fixation point (not sure why it contains an empty string above, if you don’t want it to appear, simply don’t draw it). Just keep those with static values and draw them when required.
Tip 2: use a clock for timing rather than calculating a time yourself. e.g. initialise a timer with something like rt_timer = core.Clock() at the beginning of the function, call rt_timer.reset() when you want to start timing from, and RTs.append(rt_timer.getTime() when the keypress is detected. I’m not sure when you want the to do the reset. Is that at the beg onset of the stimulus, or when the fixation appears?
Tip 3: The current structure of your code will draw the target text for only one screen refresh. It will then only draw the fixation text for one screen refresh also. Maybe those brief flashes are what you intend though? If not, you need to un-indent the stimuli_text.draw() line so that you draw it unconditionally on each iteration of your inner loop. Of course, if you switch to having two text stimuli, then the drawing does need to be conditional, but you still need to have a call to draw either of those stimuli within that inner loop.
It’s not clear to me what the stim and target variables represent. I guess they are defined elsewhere (personally I like to encapsulate functions so that all needed variables are passed in as parameters, it makes things easier to control and debug).
Hello Michael, can you clarify a portion of what you have said?
Note that this time bears no relationship to when the key was actually pressed. If the wait period was, say, 1900 ms, regardless of the key actually being pressed at 200 ms, your measured RT value will be slightly later than 1900 ms, as you are measuring when the keypress was processed, not when it occurred.
Does this mean the RT cannot be reasonably “restored”? For example, if there was a trial where the RT was faultily recorded as 2000ms but I knew for certain that the wait period was exactly 1900ms, could I conclude that the RT was 100ms? Or is the margin of error in the delayed processing simply too great?
That advice is ancient (from 5 years ago) and based on PsychoPy’s then-current event module. PsychoPy now has a much better way of measuring keypresses, via the Keyboard class, which I think is the default method used in Builder scripts now:
This can return the time that a key was pressed, regardless of when you actually check for it. You should read that documentation and then see whether your question is still relevant.
Thank you for that resource. After giving it a look, I think my question is still relevant, but the mechanics are different enough to warrant posting my own thread. Thanks again for the response, any further help there would be greatly appreciated.