psychopy.org | Reference | Downloads | Github

Self paced reading - space doesn't always move window

timing

#1

Hi everyone, and thanks to anyone who can help!

I’ve followed several earlier threads to get a self-paced reading experiment together, but I can’t resolve a problem, I’ve looked in both the coder and builder but I just can’t figure out what can be causing the problem.

I’m working with PsychoPy version 1.85.6 on a MacOS 10.13.3

I need to develop a self-paced reading (SPR) study within the moving window paradigm, where each word of the sentence is hidden, with each letter represented by dashes until the participant gets to that part of the sentence. At this point, I have figured out how to populate the screen with one sentence at a time from an excel file with all of my stimuli, and each word is covered by dashes, with the “moving window” revealing one word at a time. The problem is that the space bar does not move the moving window with each press. Instead, it seems to require a random number of clicks before it triggers the window to move to the next frame.

I have been analyzing the code that I borrowed from several others (special thank you to Michael MacAskill and Kyung Hwan) in previous forum threads, but I can’t determine where why a single space bar press does not move the frame to the next word.

There is no error reported, and the data is recorded correctly, only with large gaps because each click of the space bar is not initiating the next word to appear.

I’ve included a copy of my experiment for builder with the sentences in a corresponding excel (I can only upload 2 attachments or I would include the code, but it is easily generated from the builder version), in case anyone is able to look into the problem. Any help is greatly appreciated!

Tim McCormick

SPRdemo.psyexp (11.6 KB)
stimsinonecell.xlsx (8.1 KB)


#2

Hi Tim,

You might get a faster response if you show the relevant custom code here, rather than people having to download and interpret your experiment files (although they are useful as reference).


#3

@tjmccormick5, if you try using the event.waitKeys function rather than the event.getKeys function , I think the task should work properly. event.getKeys is being called on every frame, and thus no key is registered in the small amount of time available on each refresh - see here.


#4

Thank you @dvbridges, that solved the problem. event.waitKeys seems to be the way to go here!

To make things easier for future psychopy-ers, I’m posting the code here so that others can find it, per @Michael 's suggestion

In the code component:
In the “Begin routine” box:

# remove any keypresses remaining in the buffer from 
# before  this routine started:
event.clearEvents() 
sentenceList = sentence.split() 
# this breaks your sentence's single string of characters into a list of individual 
# words, e.g. 'The quick brown fox.' becomes ['The', 'quick', 'brown', 'fox.'] 

# keep track of which word we are up to: 
wordNumber = -1 # -1 as we haven't started yet 

# now define a function which we can use here and later on to replace letters with '-': 

def replaceWithdash(textList, currentWordNumber): 
    
    dashSentence = '' 
    for index, word in enumerate(textList): # cycle through the words and their index numbers 
        if index != currentWordNumber: 
            dashSentence = dashSentence + '-' * len(word) + ' ' # add a string of dash characters 
        else: 
            dashSentence = dashSentence + word # except for the current word 

    return dashSentence # yields the manipulated sentence 

# now at the very beginning of the trial, we need to run this function for the 
# first time. As the current word number is -1, it should make all words '-'. 
# Use the actual name of your Builder text component here: 

sentencepresentation.text = replaceWithdash(sentenceList, wordNumber) 

# In the Builder interface, specify a "constant" value for the text content, e.g. 
# 'test', so it doesn't conflict with our code. 

In the “Each frame” box:

keypresses = event.waitKeys() # returns a list of keypresses 

if len(keypresses) > 0: # at least one key was pushed 

    if 'space' in keypresses: 
        
        thisResponseTime = t # the current time in the trial 
        wordNumber = wordNumber + 1 
        if wordNumber < len(sentenceList): 

            if wordNumber == 0: # need to initialise a variable: 
                timeOfLastResponse = 0 

            # save the inter response interval for this keypress, 
            # in variables called IRI_0, IRI_1, etc: 
            thisExp.addData('Region_' + str(wordNumber), thisResponseTime - timeOfLastResponse) 
            timeOfLastResponse = thisResponseTime 

            # update the text by masking all but the current word 
            sentencepresentation.text = replaceWithdash(sentenceList, wordNumber) 
        else: 
            continueRoutine = False # time to go on to the next trial 

    elif 'escape' in keypresses: 

        core.quit() # I think you'll need to handle quitting manually now. 

#5

Sorry, but you should never use event.waitKeys() in Builder code, most especially not in the “every frame” tab of a code component. By definition, that is supposed to contain code the can be completed within one screen refresh, whereas .waitKeys() lasts an indefinite amount of time. Using that here will break Builder’s inherent screen-refresh based drawing and event loop, and cause all sorts of disruption to its time keeping.


#6

With only a text and code component built into the routine (no keyboard component), within the code component’s “Each frame” tab:

keypresses = event.getKeys() # returns a list of keypresses 

if len(keypresses) > 0: # at least one key was pushed 

    if 'space' in keypresses: 
        
        thisResponseTime = t # the current time in the trial 
        wordNumber = wordNumber + 1 
        if wordNumber < len(sentenceList): 

            if wordNumber == 0: # need to initialise a variable: 
                timeOfLastResponse = 0 

            # save the inter response interval for this keypress, 
            # in variables called IRI_0, IRI_1, etc: 
            thisExp.addData('IRI_' + str(wordNumber), thisResponseTime - timeOfLastResponse) 
            timeOfLastResponse = thisResponseTime 

            # update the text by masking all but the current word 
            sentences.text = replaceWithdash(sentenceList, wordNumber) 
        else: 
            continueRoutine = False # time to go on to the next trial 

    elif 'escape' in keypresses: 

        core.quit() # I think you'll need to handle quitting manually now. 

#7

So @tjmccormick5 and I are colleagues, and we got together and solved the problem, the short answer is that the Keyboard builder component needed to be removed, since it was clearing events and our event.getKeys() was then messed up.

But I would really appreciate it if @Michael or someone else could check my work on one issue and make sure the way we’ve implemented reaction time calculations is correct.

Recall that a single TextStim is shown, it displays a sentence but the each letter of the word is “covered” by dashes, and as the participant presses the space button, each successive word is “uncovered”. For example, the sentence “I like dogs” would be shown like this:

- ---- ----

They hit “space” and see:

I ---- ----

They hit space again and see:

- like ----

And after hitting space once again:

- ---- dogs 

The initial code component that Tim borrowed used the routine clock to calculate the times between when each word is shown. We’re interested in the variable “iri” below:

# --- Each Frame ----
if len(keypresses) > 0:
    if 'space' in keypresses:
        thisResponseTime = t # the current time in the trial
        #---- SKIPPING SOME CODE-----#
        # calculate inter-response interval:
        iri = thisResponseTime - timeOfLastResponse
        timeOfLastResponse = thisResponseTime

I was worried that this measure of reaction time (inter-response interval) would be inaccurate since the word will actually only be seen on the next screen refresh, and suggested that a better measure of the reaction time would be to create our own clock (wordClock), resetting it with win.callOnFlip(wordClock.reset) after each window flip. Here’s an abbreviated example of the implementation, and the complete code components are copied below if anyone’s interested:

# ---- Begin experiment ---- #
wordClock = core.Clock()

# ----- Begin routine ------#
event.clearEvents()
wordClock.reset()

# ----- Each frame ------ #
keypresses = event.getKeys() # returns a list of keypresses 

if len(keypresses) > 0: # at least one key was pushed 
    if 'space' in keypresses:
        iri = wordClock.getTime()
        win.callOnFlip(wordClock.reset)

Thanks for your time looking at this, I’m hoping I didn’t lead my friend astray.

And here’s the complete code for posterity’s sake:

Begin experiment

# now define a function which we can use here and later on to replace letters with '-': 
def replaceWithdash(textList, currentWordNumber): 
    
    dashSentence = '' 
    for index, word in enumerate(textList): # cycle through the words and their index numbers 
        if index != currentWordNumber: 
            dashSentence = dashSentence + ' ' + '-' * len(word)# add a string of dash characters 
        else:
            dashSentence = dashSentence + ' ' + word #for current word, but space was appearing between period and final word, so put space before each word rather than after
    return dashSentence +'.' # yields the manipulated sentence 

# Creating our own clock to time how reaction times
# based on screen flips (i.e. when the word was actually
# shown
wordClock = core.Clock()

Begin routine

# remove any keypresses remaining in the buffer from 
# before  this routine started:
event.clearEvents() 
sentenceList = sentence.split() 
# this breaks your sentence's single string of characters into a list of individual 
# words, e.g. 'The quick brown fox.' becomes ['The', 'quick', 'brown', 'fox.'] 

# keep track of which word we are up to: 
wordNumber = -1 # -1 as we haven't started yet 

# now at the very beginning of the trial, we need to run this function for the 
# first time. As the current word number is -1, it should make all words '-'. 
# The name of the TextStim in the builder is "sentences"
sentences.text = replaceWithdash(sentenceList, wordNumber) 

# reset our wordClock (it gets set to 0)
wordClock.reset()

# In the Builder interface, specify a "constant" value for the text content, e.g. 
# 'test', so it doesn't conflict with our code. 

Each frame

keypresses = event.getKeys() # returns a list of keypresses 

if len(keypresses) > 0: # at least one key was pushed 

    if 'space' in keypresses: 
        
        thisResponseTime = t # the current time in the trial 

        # Saving the time based on our wordClock,
        # which is reset when the next word is actually shown
        rtFromFlip = wordClock.getTime()
        # Tell psychopy to call the wordClock's reset() method
        # the next time the window flips:
        win.callOnFlip(wordClock.reset)

        wordNumber = wordNumber + 1 
        if wordNumber < len(sentenceList): 

            if wordNumber == 0: # need to initialise a variable: 
                timeOfLastResponse = 0 

            # save the inter response interval for this keypress, 
            # in variables called IRI_0, IRI_1, etc: 
            # Saving our flip-based rt
            thisExp.addData("IRI_" + str(wordNumber), rtFromFlip)

            # Also, since you asked, we'll save the time
            # since the trial started. (i.e. if the 'space' is hit
            # at second 1 and 2:
            #   IRI_1 = 1 
            #   IRI_2 = 1
            #   space_1 = 1
            #   space_2 = 2
            thisExp.addData("space_" + str(wordNumber), thisResponseTime) 

            # update the text by masking all but the current word 
            sentences.text = replaceWithdash(sentenceList, wordNumber) 
        else: 
            continueRoutine = False # time to go on to the next trial 

    elif 'escape' in keypresses: 

        core.quit() # I think you'll need to handle quitting manually now. 

#8

Hi @daniel.riggs1. Your initial code above looked fine for calculating IRIs. Yes, it is subject to the Builder’s temporal granularity (16.7 ms). But I don’t think your subsequent code (creating a custom timer and resetting it on each frame) will do much to improve that despite the greater complexity.

If you measure the reaction time yourself, rather than relying on Builder’s value of t, which is updated only once per refresh, it might look like you are improving the reaction accuracy, but that is really an illusion. Remember that event.getKeys() pulls events out of a queue. The time at which we call that function doesn’t reflect the time at which the event actually occurred (if the buffer hasn’t been cleared recently, then the keypress could have occurred seconds or minutes ago). Given Builder’s event loop, it is likely that the keypress actually occurred sometime during the previous screen refresh interval and is only being collected now. So your solution actually slows down the measured reaction time further (although likely by only a very small amount), as your timer might have advanced slightly beyond t, which was updated very early in the green refresh period.

The bigger problem, however, is that your reaction times will appear more precise than they actually are. Because you’ve introduced some temporal noise, the 16.7 ms granularity will be eroded and it will look more like the RTs have been sampled from a continuous distribution. There is an advantage to keeping them granular: it acts as a reminder to the researcher that their RTs are sampled discretely every 16.7 ms.

Does that make sense? The alternative is to use ioHub code to detect keypresses. ioHub runs in a different process to PsychoPy and isn’t locked in to its screen refresh cycle. When you query for keypress events, you get the timestamp of when it actually occurred.

Of course, all of this is a bit moot if you are using a standard computer keyboard. The lag and variability associated with that make all of the software issues a bit irrelevant.


#9

Ooh, so since psychopy is running this code component every frame refresh anyway, there is no improvement to adding an additional clock which resets on frame flips, did I understand that correctly? That seems pretty obvious now.

Thanks for your help, though I must admit the picture you paint about keyboards and the event cue is a little dire. I don’t want to take up too much more of your time, but is there a feasible way for a grad student to get reliable reaction times without highly specialized equipment? (Any improvement in using the laptop keyboard as opposed to a USB keyboard?). Or are these differences so small that they really don’t matter? Sorry for all the questions, if you knew of a good place for further reading instead of writing a novel here yourself, I’d be happy with any help.

Thanks again.


#10

Hi Daniel,

Start by looking through this thread:
https://groups.google.com/forum/#!topic/psychopy-dev/u3WyDfnIYBo

And this post:
https://groups.google.com/forum/#!topic/psychopy-users/l6LNV3V7-bE

And check out this classic paper on why we can often stop worrying about temporal resolution when measuring reaction times:

Lastly, we can’t assume that modern hardware solves all ills. These days, we often use commodity hardware that was never designed for the needs of scientific research. Here is an interesting paper that compares modern reaction times to those measured in the Victorian era by Galton. Some people claim that over generations since then, our reaction times have gotten longer, while this paper provides evidence that this may be due at least in part to the reduced temporal accuracy of our measurement equipment over that time: