psychopy.org | Reference | Downloads | Github

Coder - Reaction times far longer than expected

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

Thanks for you help,
Mauricio.

Hi Mauricio,

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.

Hi Mauricio,

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.

Best,
Yu-Han

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.

All the best,
Mauricio.

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).