Using waitKeys for precise response times in Builder

I’ve developed a modified Simon Task using the Builder (using PsychoPy 1.84 on Mac OS X), with various code components. In short, trial value is presented followed by either a blue or yellow circle presented to the left or right side of the screen and responses are indicated using the z (blue) or m (yellow) key.

However, I’m now aware (from reading Jon’s comments on an old thread: https://groups.google.com/forum/#!topic/psychopy-users/Y_zF3Q2Oxws) that when using getKeys() with the Builder, the program checks whether a key is pressed each frame, and consequently, the temporal resolution is restricted to the frame rate (i.e., every ~16ms).

Using “event.waitKeys” appears to offer a solution to; however, I’m struggling to work out how this could be best added to what I have already. Is it possible to add this as a code component in the Builder view? And if so, how exactly might I be able to do this?

Thanks in advance,
Harry

p.s., alternatively, if the only solution is to insert this in the coder, I have also appended the code (below) for the trial where I would like to include this.

# ------Prepare to start Routine "trial"-------
t = 0
trialClock.reset()  # clock
frameN = -1
continueRoutine = True
# update component parameters for each repeat
polygon.setFillColor(targetcolour)
polygon.setPos((targetx, 0))
key_resp_2 = event.BuilderKeyResponse()

# keep track of which components have finished
trialComponents = [text, polygon, key_resp_2]
for thisComponent in trialComponents:
    if hasattr(thisComponent, 'status'):
        thisComponent.status = NOT_STARTED

# -------Start Routine "trial"-------
while continueRoutine:
    # get current time
    t = trialClock.getTime()
    frameN = frameN + 1  # number of completed frames (so 0 is the first frame)
    # update/draw components on each frame
    
    # *text* updates
    if t >= 0.0 and text.status == NOT_STARTED:
        # keep track of start time/frame for later
        text.tStart = t
        text.frameNStart = frameN  # exact frame index
        text.setAutoDraw(True)
    frameRemains = 0.0 + 1.0- win.monitorFramePeriod * 0.75  # most of one frame period left
    if text.status == STARTED and t >= frameRemains:
        text.setAutoDraw(False)
    
    # *polygon* updates
    if t >= 1 and polygon.status == NOT_STARTED:
        # keep track of start time/frame for later
        polygon.tStart = t
        polygon.frameNStart = frameN  # exact frame index
        polygon.setAutoDraw(True)
    
    # *key_resp_2* updates
    if t >= 1 and key_resp_2.status == NOT_STARTED:
        # keep track of start time/frame for later
        key_resp_2.tStart = t
        key_resp_2.frameNStart = frameN  # exact frame index
        key_resp_2.status = STARTED
        # keyboard checking is just starting
        win.callOnFlip(key_resp_2.clock.reset)  # t=0 on next screen flip
        event.clearEvents(eventType='keyboard')
    if key_resp_2.status == STARTED:
        theseKeys = event.getKeys(keyList=['m', 'z'])
        
        # check for quit:
        if "escape" in theseKeys:
            endExpNow = True
        if len(theseKeys) > 0:  # at least one key was pressed
            key_resp_2.keys = theseKeys[-1]  # just the last key pressed
            key_resp_2.rt = key_resp_2.clock.getTime()
            # was this 'correct'?
            if (key_resp_2.keys == str(CorrectAns)) or (key_resp_2.keys == CorrectAns):
                key_resp_2.corr = 1
            else:
                key_resp_2.corr = 0
            # a response ends the routine
            continueRoutine = False
    
    
    # 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 trialComponents:
        if hasattr(thisComponent, "status") and thisComponent.status != FINISHED:
            continueRoutine = True
            break  # at least one component has not yet finished
    
    # check for quit (the Esc key)
    if endExpNow or event.getKeys(keyList=["escape"]):
        core.quit()
    
    # refresh the screen
    if continueRoutine:  # don't flip if this routine is over or we'll get a blank screen
        win.flip()

# -------Ending Routine "trial"-------
for thisComponent in trialComponents:
    if hasattr(thisComponent, "setAutoDraw"):
        thisComponent.setAutoDraw(False)
# check responses
if key_resp_2.keys in ['', [], None]:  # No response was made
    key_resp_2.keys=None
    # was no response the correct answer?!
    if str(CorrectAns).lower() == 'none':
       key_resp_2.corr = 1  # correct non-response
    else:
       key_resp_2.corr = 0  # failed to respond (incorrectly)
# store data for trials (TrialHandler)
trials.addData('key_resp_2.keys',key_resp_2.keys)
trials.addData('key_resp_2.corr', key_resp_2.corr)
if key_resp_2.keys != None:  # we had a response
    trials.addData('key_resp_2.rt', key_resp_2.rt)
if Condition == 1:#If this is a congruent trial
    if key_resp_2.corr and key_resp_2.rt <= ResponseSpeed_CON: #check if response was correct and faster than current threshold for CONGRUENT TRIALs
        ResponseSpeed_CON = ResponseSpeed_CON - .025 #if RT was quick enough, reduce threshold
    elif key_resp_2.corr and key_resp_2.rt >= ResponseSpeed_CON: #check if response was correct but too slow, is so then increase RT.
        ResponseSpeed_CON = ResponseSpeed_CON + .025
else: #if it is not a congruent trial then adjust RT for incgronguent trials
    if key_resp_2.corr and key_resp_2.rt <= ResponseSpeed_INC: #check if response was correct and faster than current threshold for CONGRUENT TRIALs
        ResponseSpeed_INC = ResponseSpeed_INC - .025 #if RT was quick enough and correct, reduce threshold
    elif key_resp_2.corr and key_resp_2.rt >= ResponseSpeed_INC: #check if response was correct but too slow, is so then increase RT.
        ResponseSpeed_INC = ResponseSpeed_INC + .025
trials.addData('ResponseSpeed_CON', ResponseSpeed_CON) # save updated response 
trials.addData('ResponseSpeed_INC', ResponseSpeed_INC) # save updated response

Hi Harry,

No, you can’t use event.waitKeys(). Builder-generated code is inherently structured around a drawing loop that runs once every screen refresh. Inserting a call to event.waitKeys() creates an indefinite pause, and completely screws up the timing logic in the script.

Secondly, it wouldn’t really give you much more accurate (vs precise) timing. What is the hardware that you are using to gather your response? If it is a typical computer keyboard, then polling the keyboard at the frame rate is the least of your worries, due to the uncertain lag attached to that. You could do some reading on the topic of low-resolution temporal sampling (e.g. http://www.psy.gla.ac.uk/~martinl/Assets/MCMPS/Ulrich&Giray89.pdf ) that might allay some concerns, and you could look more into the hardware limitations of keyboards (which will give you some well-founded concerns about the accuracy and precision with which we can actually measure reaction times), and you could bear in mind that the trial-to-trial physiological variability in reaction time often swamps these hardware and software issues anyway.

You could also look at the ioHub system built in to PsychoPy that provides an alternative way of getting responses that escapes the limitation of the once-per-refresh polling cycle. But it won’t solve the inherent issues of consumer hardware.

Lastly, you really should look into this wonderful post by @lindeloev, using a mechanical key pressing setup to get objective measurements of both software and hardware influences on keyboard response latency (both accuracy and precision):
https://groups.google.com/forum/#!topic/psychopy-dev/u3WyDfnIYBo

Hi Michael,

Thanks for your response and I appreciated the link to the great post by Jonas (and your comment beneath it).

We have a somewhat meagre supply of hardware in our department and you’re correct that in the absence of using a button box / ioHub then the frame rate is perhaps not the main concern here. I’ve been testing this with a basic Lenova USB keyboard which undoubtedly has a serious lag.

I’m not convinced I can get my department to order a button box (at least in the immediate future), but I will buy a mechanical keyboard with lower lag and a monitor with a faster refresh rate than the 60Hz one I’m currently using. There’s a large number of trials in the experiment and I don’t need sub-millisecond precision to detect the effects so hopefully this will do the trick for now (I’ll stop polishing the cannonball and buy a [slightly] better cannon).

Best,
Harry

Those all seem to be sensible strategies. Good luck.

I made myself a simplistic list, mostly from the article Michael cited. I apologize if this is too simplistic.
(To my list for a PC, a fast graphic card. Not much choice with Macs, but they are ok.)
1. Turn wi-fi off
2. Make a “clean” log-in (or computer) that has never had mail, dropbox, etc.
3. Make sure on the “clean” login there is nothing on the desktop. No clutter, nothing.
4. Notifications turned off (Macintosh)
5. Make sure that there are no programs running in any login.
6. Use program "Activity Monitor” on Mac (Task Manager on a PC) to kill anything that looks suspicious (but be careful, you could kill the system). You might see something that doesn’t belong like “Google update demon”.
7. Make sure Windows automatic update is off.
8. Turn off virus scanning on every log in on the computer.
For PsychoPy:
1. Only have one window or experiment open on your desktop.
2. Output should be a .cvs file, not .xls or .xlsx file. If you need an input file for experiment trials, that should also be a .cvs file.
3. Beware of loops with 100’s of iterations.

2 Likes

@Bill_Prinzmetal’s list above is a bunch of excellent advice for all experiments.

on the topic of ‘speed ups’ or at least avoiding error variance…

I have noticed that if I do

timer1.getTime()

in coder at least once before it is actually needed (e.g., soon after intiializing
timer1 = core.Clock()
) , that the values on the early trials are more consistent/reasonable. It seems for lack of a better
term that this ‘primes’ the process of reading from the clock. As for
Harry_Manley’s response time worries, I have used the method described
in

Voss, A., Leonhart, R., & Stahl, C. (2007).
How to make your own response boxes: A step-by-step guide for the construction of reliable and inexpensive parallel-port response pads from computer mice .
Behavior Research Methods, 39, 4, 797-801.

to make 1ms accurate response keys ( on mac, this is likely a nonstarter )

That seems pretty strange. Do you have any code that reliably demonstrates that?

I took one of my actual experiments (hand-coded) and modified it so it randomly
does or does not ‘prime’ the timer on each run. I ran the experiment 1000 times
using a bash script and saved the time differences from 2 sequential timer reads
e.g.,

sw0=timer1.getTime()
sw1=timer1.getTime()
delta=sw1-sw0

five times per run. On average, the MEAN first delta per run was slower for unprimed runs than for primed runs, and over the replications the MAX (and SD) for unprimed was larger. The good news is, that on the slowest of my current hardware, ATHLON X2 5600+, the deltas were sub-millisecond. The same pattern shows up on
AMD FX-6300 3.5 GHz Six-Core Processor and FX-8350 4.0GHz Eight-Core Processor
but even lower in the mud ( in the microseconds if the numbers are to be believed).

By “five times per run” above I mean that 5 deltas were taken each run. Only the first
of the 5 deltas showed the timer primer effect: the other 4 deltas per run were almost
identical.

Short story:

  1. I don’t see how priming the timer can hurt in any way
  2. The faster the machine, the more likely it doesn’t matter
  3. Sternberg’s dictum #3.7
    ( http://www.psych.upenn.edu/~saul/rt.experimentation.pdf )

" 3.7 Calibration
Don’t trust the computer or the program. "