Temporal dynamics

Objectives

  • Be able to use clocks to track time during program execution.
  • Understand the concept of the monitor refresh rate and its implications for drawing dynamic stimuli.
  • Be able to structure code to draw moving stimuli and collect responses.

In the previous lessons, we have only considered situations in which the stimuli are static and there is a simple timecourse in waiting for a response from the observer. However, in vision science experiments we often want to show moving or otherwise dynamic stimuli and with a complex presentation and response schedule. First, we need to investigate how we handle time in Python and psychopy.

Time and clocks

One important way in which we can manipulate the temporal dynamics of our programs is to be able to track time. We can do this in psychopy by creating a psychopy.core.Clock(), which has a getTime() function that tells us how many seconds have elapsed since the clock was created.

import psychopy.core

clock = psychopy.core.Clock()

for iteration in range(2):

    psychopy.core.wait(1.0)

    print clock.getTime()

We can use such a clock as a simple way of presenting a stimulus for a desired duration. For example, we could show a grating for 500ms by:

import psychopy.visual
import psychopy.event
import psychopy.core

win = psychopy.visual.Window(
    size=[400, 400],
    units="pix",
    fullscr=False
)

clock = psychopy.core.Clock()

text = psychopy.visual.TextStim(win=win)

grating = psychopy.visual.GratingStim(
    win=win,
    size=[200, 200],
    mask="circle",
    units="pix",
    sf=5.0 / 200.0
)

text.text = "Press any key to show the grating"

text.draw()

win.flip()

psychopy.event.waitKeys()

clock.reset()

while clock.getTime() < 0.5:
    grating.draw()
    win.flip()

text.text = "Press any key to finish"

text.draw()

win.flip()

psychopy.event.waitKeys()

win.close()

The key lines are highlighted in the above code. First, we reset the clock to zero. Then, we use a while loop to draw the grating and update the window for as long as the time that has elapsed (which we find out by calling clock.getTime()) is less than how long we want to show the grating for (500ms, or 0.5 seconds).

The above method is fine for experiments in which precise timing is not required. Uncertainties in timing that arise using this approach come about, in part, due to the way that the window is updated. Have a think about the while loop above—how frequently do you think the grating will be drawn and the window updated? Typically, this is limited by the monitor’s “refresh rate”, which specifies how often it updates. This is measured in hertz (Hz), and 60Hz is a common rate for LCD monitors.

If we are confident that the precise refresh rate is known and that it is stable (neither of which can happen without specialised testing), we can instead present our stimuli for a given number of “flips” (known as a given number of “frames”). For example, if we know that our monitor updates at 60 frames per second and we want to draw a grating for 500ms, we can draw 30 frames:

import psychopy.visual
import psychopy.event

win = psychopy.visual.Window(
    size=[400, 400],
    units="pix",
    fullscr=False
)

text = psychopy.visual.TextStim(win=win)

grating = psychopy.visual.GratingStim(
    win=win,
    size=[200, 200],
    mask="circle",
    units="pix",
    sf=5.0 / 200.0
)

text.text = "Press any key to show the grating"

text.draw()

win.flip()

psychopy.event.waitKeys()

for frame in range(30):
    grating.draw()
    win.flip()

text.text = "Press any key to finish"

text.draw()

win.flip()

psychopy.event.waitKeys()

win.close()

Drawing dynamic stimuli

Now that we know about timing and clocks, let’s use them to draw a moving stimulus. What we’d like to do is draw a grating that changes phase such that it moves through two complete cycles in one second. We can do that via code such as (note that there are a few new elements to this code—we will go through it below):

import numpy as np

import psychopy.visual
import psychopy.event
import psychopy.core

win = psychopy.visual.Window(
    size=[400, 400],
    units="pix",
    fullscr=False
)

grating = psychopy.visual.GratingStim(
    win=win,
    size=[200, 200],
    mask="circle",
    units="pix",
    sf=5.0 / 200.0
)

clock = psychopy.core.Clock()

keep_going = True

while keep_going:

    grating.phase = np.mod(clock.getTime() / 0.5, 1)

    grating.draw()

    win.flip()

    keys = psychopy.event.getKeys()

    if len(keys) > 0:
        keep_going = False

win.close()

The first new thing is the structure of our loop. We first set a boolean variable keep_going to True. We will use this to indicate whether the user has pressed a key to indicate that they’d like the program to finish. Because we don’t know when this will be, we use a while loop to keep repeating until keep_going becomes False.

The second new thing is the way we are setting the phase of the grating. Recall that the phase is specified by a value that is between 0 and 1. Let’s start by thinking about what would happen if we wanted the grating to move through one complete cycle in one second (rather than the two cycles per second we want it to have eventually). We can think of that as wanting the grating’s phase to change from 0 to 1 across the course of 1 second of time. Hence, we could simply divide the time, in seconds, by 1 in order to determine the phase. However, remember that the phase needs to be between 0 and 1; as soon as the time is greater than one second, the phase value will be greater than 1. We can fix this by using the mod command, which effectively “wraps” the value back around on itself (e.g. 1.2 mod 1 is 0.2, 2.7 mod 1 is 0.7, etc.). The way it does this is by dividing the first number by the second and returning the remainder.

Having established the necessary phase at a given point in time, we use it to update the phase of the grating stimulus. Because this statement is operating within the while loop, it is being frequently evaluated and so the grating is frequently changing phase. This change in phase over time gives the visual appearance of a smoothly moving grating.

The final new thing is how we handle the keyboard. In the past, we have used the psychopy.event.waitKeys() function, which halts execution until a key is pressed. We can’t use that here because we wouldn’t be able to update our stimulus if we’re waiting for a keypress. Instead, we use the psychopy.event.getKeys() function, which probes the state of the keyboard at that precise moment and returns immediately a list of the keys that are being pressed. If a key has been pressed, this list will have a length (len) that is greater than zero—in this case, we set our keep_going variable to False so that we can exit out of the while loop.

Controlling the trial schedule

We can using timing and clocks to control the temporal schedule of a given trial in an experiment. For example, we may want to start the trial with 500ms of no stimulus, then show a stimulus for 500ms, then wait for a participant to respond. Furthermore, we wish to require a minimum of 2 seconds between trials. We can do that via:

import numpy as np

import psychopy.visual
import psychopy.event
import psychopy.core

win = psychopy.visual.Window(
    size=[400, 400],
    units="pix",
    fullscr=False
)

grating = psychopy.visual.GratingStim(
    win=win,
    size=[200, 200],
    mask="circle",
    units="pix",
    sf=5.0 / 200.0
)

clock = psychopy.core.Clock()

n_trials = 2
pre_duration_s = 0.5
stim_duration_s = 0.5
min_iti = 2.0

for trial in range(n_trials):

    clock.reset()

    # wait until the 'pre' time has passed
    while clock.getTime() < pre_duration_s:
        win.flip()

    while clock.getTime() < pre_duration_s + stim_duration_s:
        grating.draw()
        win.flip()

    # clear the window
    win.flip()

    keys = psychopy.event.waitKeys()

    while clock.getTime() < min_iti:
        win.flip()

win.close()