Continue presenting a visual stimulus for varying duration after response

Windows 7, PsychoPy 1.83.03

Hi folks, I’m working on an experiment involving a temporal discrimination task - i.e. short (100 ms) and long (500 ms) visual and auditory stimuli which the participants shall judge by duration.

The visual stimuli are what’s causing me problems here.

Responses are measured, as usual, starting from stimulus onset. Thus, if a subject recognises the long stimulus as such before 500 ms have elapsed, they should respond - however, the stimulus is to be presented for its full 500 ms.

Since the response-stimulus-interval must remain constant (600 ms) - i.e. the interval between the participant’s keypress and the presentation of the next visual stimulus - I need to create a varying duration of the blank screen between two visual stimuli:

If the subject responds early, the visual stimulus will remain on screen for the majority of the 600 ms interval - if they respond later, the old stimulus will take up some of the time while the remainder of the 600 ms are a blank screen, then the next stimulus appears.

This isn’t a problem for auditory stimuli, since the sound.Sound() function allows me to set a fixed duration, and also I can just cut my audio files to the desired length in Audacity.

The visual.Rect() function, in contrast, does not allow me to enter a fixed duration; to my knowledge, I can only draw the stimulus, flip the window, and then use core.wait() for either 100 or 500 ms.

I thought about using core.wait() before and after the subject’s response, recording the time until the reaction, subtracting it from the total stimulus duration (=500 ms), and then subtracting that duration from the maximum possible blank screen interval (=600 ms). Then I could draw the stimulus again and core.wait() for the remaining stimulus duration, then flip the screen and have it be blank for the remaining “blank duration”.

However, I am not sure how much latency this will create with such calculations going on in the background.

Especially since during the response-stimulus-interval, the experimentor needs to type in the accuracy of vocal responses given by the participant. I’m using the event.getKeys() function for this - so PsychoPy can’t just core.wait() and do nothing during that time period between response keypress and the onset of the next stimulus. Hence, the following still needs to happen while the stimulus and/or blank screen are being presented for a varying duration:

            core.wait(RSI)
            exp_resp = event.getKeys(keyList = ["num_1", "num_3", "escape"])
            try:
                exp_response = "%s" % (exp_resp[0])
            except:
                exp_response = None
            if exp_response == None:
                resp = None
            elif exp_response == "num_1" or "num_3" or "escape":
                if exp_response == "num_1":
                    resp = 'left'
                elif exp_response == "num_3":
                    resp = 'right'
                elif exp_response == "escape":
                    core.quit()

So is there any way to either:

  1. Give a visual stimulus a fixed duration included in the variable of the stimulus itself
  2. Use core.wait() and the calculations I described above without creating latency or preventing the coding of vocal responses as described above?

I also thought about using a while loop, but then I have the problem of not being able to get out of that loop when the subject responds or the experimentor does the coding.

http://www.psychopy.org/coder/codeStimuli.html#timing

The issue is that the stimulus needs to be presented both before and after the subject responds, and both to varying durations (depending on the time of the participant’s response).

I’ve also found this method of going frame-by-frame - though I am not sure whether this is going to result in latency as well:
http://gestaltrevision.be/wiki/python/coding#timing_your_presentations

There is no ‘duration’ attribute for visual stimuli. As far as PsychoPy is concerned, on each frame a given stimulus is drawn or it is not. All you can do is tell it when to draw something or not.

So as I understand it you have two constant periods (the 500ms display time and the 600ms RSI), and the issue is that one of them has a variable start time (when the participant responds). That’s not too difficult to deal with. Here’s a sketch of a structure that could solve the problem.

for i in range(0, numTrials):
    # at the start of the stimulus onset
    startStim = core.getTime() # Gets a timestamp of the start of the display
    respRecorded = core.getTime()+.5 # A placeholder for the time of response, 
    # defaults to end of the stimulus display time
    responded = False # So you only get one response
    while core.getTime() - startStim < .5: # Loops for 500ms, flat.
        stim.draw() # Draw your stimuli and flip the window so it displays. 
        win.flip() 
        resp = event.getKeys()
        if len(event.getKeys()) > 0 and not responded: # Checks both for a response and if they have already responded or not
             # Do whatever you need to do for your responses
             responded = True
             respRecorded = core.getTime() # Creates a new timestamp at the time of response
    while core.getTime() - respRecorded < .6:
        pass # Simply waits until 600ms have passed from respRecorded before 
        # going to the next iteration of the loop

Make sense? Note that the durations don’t actually care about the response. The response happens once and the loop keeps going.

EDIT: Just remembered getTime works in seconds, not milliseconds. So, some of those numbers were a bit off.

1 Like

Thanks a lot, Jonathan! :smiley: I didn’t know about the command “pass” at all!

The responses are actually being registered via a response box using its own custom Python program written by two of our technicians - only the coding happens via event.geyKeys(). However, I don’t think this will matter that much, since your if-clause only refers to the length of the list variable containing the response, so I should be able to adapt this easily.

The response box has its own internal clock, but I also reset PsychoPy’s core.clock() at the onset of each stimulus anyway, so I don’t think the two measurements of time (inside PsychoPy and inside the response box) should interfere.

Okay, I’ve tried this out, and the only remaining issue seems to be: How can I introduce a maximum response time (1500 ms) into this?

I tried replacing each instance of event.getKeys() in @jonathan.kominsky 's code with event.waitKeys(maxWait = 1.5). However, since this command is inside a while loop, the program waits indefinitely if I do so, rather than just waiting for a maximum of 1.5 seconds before moving on.

As said before, in actuality we are working with an external response box, but the idea is the same - the maximum time the box waits for a response before moving on to the response-stimulus-interval (600 ms) and then the next stimulus is 1500 ms.

waitKeys, like core.wait, just stops everything else from executing.

So let me check if I understand this correctly: The stimulus starts and is present for 500ms. Anytime in that 500ms or for 1 second after the end of the stimulus, the participant can respond with a key-press. 600ms after that happens, the next trial should begin. If they do not respond, presumably you just want the next trial to start 600ms later than that.

Very simple modification:

for i in range(0, numTrials):
    # at the start of the stimulus onset
    startStim = core.getTime() # Gets a timestamp of the start of the display
    respRecorded = core.getTime()+1.5 # A placeholder for the time of response, 
    # defaults to end of the 1500ms response window. 
    responded = False # So you only get one response
    trialDone = False # Adding this to do some parallel tracking
    while core.getTime() - startStim < 1.5 and not trialDone: # Now this runs 1500ms OR until trialDone is set to true.
        if core.getTime() - startStim < .5: #For the first 500ms of that time, draw the thing.
            stim.draw() # Draw your stimuli and flip the window so it displays. 
        elif responded: 
            # If the stimuli are done AND the participant has already responded, break this loop.
            trialDone = True 
        win.flip() 
        resp = event.getKeys()
        if len(event.getKeys()) > 0 and not responded: # Checks both for a response and if they have already responded or not
             # Do whatever you need to do for your responses here.
             responded = True
             respRecorded = core.getTime() # Creates a new timestamp at the time of response
    while core.getTime() - respRecorded < .6: # This will start checking whenever the above loop ends.
        pass # Simply waits until 600ms have passed from respRecorded before 
        # going to the next iteration of the loop

If you don’t want the 600ms waiting period if they don’t respond in the 1500ms window, i.e. you want the next trial to start on immediately after the 1500ms window has ended if no response has been recorded, just set respRecorded’s initial value back to core.getTime() + .5. That way, if they don’t respond in 1500ms, when the next loop that checks for the 600ms window starts, it will immediately be false and just go on to the next iteration.

1 Like

Thank you very much once again! :slight_smile: I’m currently at a conference, but I’ll try this out over the Easter weekend and put it to the test in the lab with the response box (which is required to test-run the actual script in its complete context) once I get back to my office.

Then I’m going to report how it went! :wink:

Okay, I tried implementing this with our custom Response Box, which created some problems at first, as expected. So I went back to your original code and let PsychoPy print some of the variables.

This revealed to me that even in your setup, resp = event.getKeys() doesn’t register responses properly within this while-loop? :frowning:

event.getKeys(), for some reason, is just an empty list when I try what you proposed.

The Response Box actually manages to register key presses during the loop, though.

So if anyone else has a similar problem and tries using event.getKeys(), this may not be the perfect solution yet. For me personally, though, with the response box, this loop does what it’s supposed to do, so I consider the issue solved.

Once again, thank you very much for all your help, @jonathan.kominsky ! :smiley:

Oh, whoops! There’s a small error in my code that might be contributing to this. It’s calling event.getKeys() twice in rapid succession but only looking at the second one, which might be blank b/c the first one cleared the buffer. Try this:

for i in range(0, numTrials):
    # at the start of the stimulus onset
    startStim = core.getTime() # Gets a timestamp of the start of the display
    respRecorded = core.getTime()+1.5 # A placeholder for the time of response, 
    # defaults to end of the 1500ms response window. 
    responded = False # So you only get one response
    trialDone = False # Adding this to do some parallel tracking
    while core.getTime() - startStim < 1.5 and not trialDone: # Now this runs 1500ms OR until trialDone is set to true.
        if core.getTime() - startStim < .5: #For the first 500ms of that time, draw the thing.
            stim.draw() # Draw your stimuli and flip the window so it displays. 
        elif responded: 
            # If the stimuli are done AND the participant has already responded, break this loop.
            trialDone = True 
        win.flip() 
        resp = event.getKeys()
        if len(resp) > 0 and not responded: # IMPORTANT: changes it so we're not calling event.getKeys() twice too quickly
             # Do whatever you need to do for your responses here.
             responded = True
             respRecorded = core.getTime() # Creates a new timestamp at the time of response
    while core.getTime() - respRecorded < .6: # This will start checking whenever the above loop ends.
        pass # Simply waits until 600ms have passed from respRecorded before 
        # going to the next iteration of the loop

I already did precisely what you wrote above in my adaptation of your code, simply because I prefer storing things in variables :wink: . I then typed “print resp” at the end, and “print rt”, where rt = respRecorded - startStim.

The rt is displayed correctly, however, resp is still an empty list, so PsychoPy just prints . So you can get a response time this way, but it isn’t possible yet to identify which key has been pressed.

My Response Box takes care of the latter for me, even though that took quite some trial-and-error, as well. But perhaps we can work out a way here that is going to work for everyone in the future, because most people will probably resort to the event.getKeys() approach, whereas the Response Box is a custom-built thing at our research facility.

Oh good, you’re way ahead of me. Odd that getKeys() is just failing outright like that. I guess it’s something about how the response box is communicating with the computer. You may be able to get PsychoPy to talk to the response box directly. I’d recommend checking the demos for button boxes and the iohub demos and see if there are any useful ideas there. Otherwise, glad I could get you this far!

1 Like

Yeah, I have no clue either why event.getKeys() behaves this way in this special case. Maybe the list is emptied again each time a new getKeys()-command is given, rather than simply appending everything to the list? This shouldn’t be the case, because we’re not using any event.clearEvents(), but who knows…

It would simply be nice if other users could transfer the knowledge from this thread to their own experiments, because those other users likely do not have a Response Box.

Me personally, I can work with this now, so thanks a lot once again! :slight_smile:

Calling getKeys() always clears all keyboard events from the queue. i.e. the purpose of this function is to go the event queue and pull out any and all events in there, leaving it empty. That way, you always know that any responses you collect will only have been collected since the last call to getKey(), so any keypress can only be detected once.

The only reason to use clearEvents() is prior to calling getKeys(). i.e. if you only want to start detecting new events from a certain time point onwards, and you don’t want to collect any stale ones that might be sitting in the queue from a previous period when you weren’t actively checking for responses and clearing the queue.