Python Tutorial: Level 5

Created for the OVGU Cognitive Neuroscience Master's course H.3: Projektseminar Fall 2019

Updated August 2020

Taught by Reshanne Reeder

So far we have been controlling window flips using keypresses, but in a real experiment you will usually want window flips to occur automatically following some kind of timer (draw stimulus, flip window, wait 2 seconds. draw stimulus, flip window, etc.). To do this in psychopy, we need the "core" module:

In [2]:
from psychopy import core

Then if you type "core." followed by the "tab" key, this will bring up a list of functions you can use with this module. The simplest way to make a window wait until the next flip is to use core.wait. This is the easiest but least flexible / least accurate timing function.

All you have to do is replace "event.waitKeys()" in the trial loop with "core.wait()", and the wait time in seconds within the parentheses:

In [ ]:
#So this:
        #-draw stimulus
        my_image.draw() #draw
        win.flip() #show
        event.waitKeys() #wait for keypress

#Becomes:
        #-draw stimulus
        my_image.draw() #draw
        win.flip() #show
        core.wait(.5) #wait for half a second

And you can see it in action like this:

In [20]:
from psychopy import visual, monitors, event, core

#define the monitor parameters
mon = monitors.Monitor('myMonitor', width=35.56, distance=60)
mon.setSizePix([1600,900])
win = visual.Window(monitor=mon) #define a window

import os
#stuff you only have to define once at the top of your script
os.chdir('/home/shoarly/Documents/pytutorial/exp')
main_dir = os.getcwd() 
image_dir = os.path.join(main_dir,'images')

my_image = visual.ImageStim(win)

stims = ['face01.jpg','face02.jpg','face03.jpg'] #create a list if images to show
nTrials=3 #create a number of trials for your images

for trial in range(nTrials): #loop through trials
    
    my_image.image = os.path.join(image_dir,stims[trial])
    
    my_image.draw() #draw
    win.flip() #show
    core.wait(.5) #wait .5 seconds, then:
    
win.close() #close the window after trials have looped    
1663.3460 	WARNING 	Monitor specification not found. Creating a temporary one...

You need to define the wait time for every stimulus that will be shown on a trial. This includes an initial fixation, the image, and then the "end of trial" message. You can add these to your experiment script in the exercises.

"wait" is easy to implement, but you it is imprecise and will also run into problems if you want to present dynamic or changing stimuli. "wait" will essentially freeze the window, so more complex stimuli are not possible with this function. A more precise and flexible option is to use a Clock. Clock is psychopy's internal experiment timer. As soon as you define a clock, it starts counting up until the experiment ends or the clock is refreshed. For example:

In [9]:
my_timer = core.Clock() #define a clock
my_timer.getTime() #get time on the clock
Out[9]:
0.00013494491577148438

It is important to know that re-defining or resetting my_timer inside or outside of a loop will change the timing that is counted.

If you define my_timer at the beginning of the trial loop, and print "getTime" at the end of the trial loop, you will get the total trial time. This will reset for every trial because you refresh my_timer by redefining it within the loop.

If you define my_timer within the block loop, and print "getTime" at the end of the block loop, you will get the total block time.

If you want to know both the total trial time and the total block time, you can define several clocks outside of either loop, and simply refresh each clock within each loop, respectively:

In [13]:
nBlocks=2
nTrials=3

block_timer = core.Clock()
trial_timer = core.Clock()

for block in range(nBlocks):
    block_timer.reset()
    
    for trial in range(nTrials):
        trial_timer.reset()
        #run the experiment...
        
        trial_timer.getTime() #get time at the end of the trial
        
    block_timer.getTime() #get block time (remember to indent properly)
    
win.close()       

And implemented in an easily readable output:

In [19]:
nBlocks=2
nTrials=3

block_timer = core.Clock()
trial_timer = core.Clock()

for block in range(nBlocks):
    block_timer.reset()
    
    for trial in range(nTrials):
        trial_timer.reset()
        #run the experiment...
        core.wait(.5) #wait for the trial to run its course
        
        print('Trial'+str(trial)+' time =', trial_timer.getTime()) #proper indent
        
    print('Block'+str(block)+' time =', block_timer.getTime()) #proper indent
        
win.close()       
Trial0 time = 0.5009090900421143
Trial1 time = 0.5005450248718262
Trial2 time = 0.5006711483001709
Block0 time = 1.502417802810669
Trial0 time = 0.5009770393371582
Trial1 time = 0.5006511211395264
Trial2 time = 0.5009708404541016
Block1 time = 1.5028131008148193

Clocks are also very handy in defining stimulus timing and duration, and are more accurate than core.wait. To do this, you need to wrap your stimulus in a while loop, and tell your experiment "while the clock is between these 2 times, present the stimulus". This loop is nested inside your trial loop, so instead of this:

In [ ]:
for block in range(nBlocks):    
    #=====================
    #TRIAL SEQUENCE
    #=====================    
    for trial in range(nTrials):
        #-set stimuli and stimulus properties for the current trial
        #...
        #-draw stimulus
        my_image.draw() #draw
        win.flip() #show
        event.waitKeys() #wait for keypress
        
win.close()           

You have this:

In [ ]:
pres_timer = core.Clock() #define the clock at the beginning of the experiment

for block in range(nBlocks):
    #=====================
    #TRIAL SEQUENCE
    #=====================    
    for trial in range(nTrials):
        #-set stimuli and stimulus properties for the current trial
        #...  
        #reset stimulus presentation timer right before the first stimulus should appear
        pres_timer.reset()
        #-draw stimulus
        while pres_timer.getTime() <=2: #2 seconds
            my_image.draw() #draw
            win.flip() #show
            
win.close()               

And you can add multiple while loops to present sequential stimuli:

In [ ]:
        #reset stimulus presentation timer right before the first stimulus should appear
        pres_timer.reset()
        #-draw stimulus
        while pres_timer.getTime() <=1: #1 second
            fix_text.draw() #draw
            win.flip() #show  
        while 1 < pres_timer.getTime() <=3: #2 seconds
            my_image.draw() #draw
            win.flip() #show  

To present simultaneous stimuli (for example, a constant fixation cross), you have to draw it for every while loop:

In [ ]:
        #reset stimulus presentation timer right before the first stimulus should appear 
        pres_timer.reset()
        #-draw stimulus
        while pres_timer.getTime() <=1: #1 second
            fix_text.draw() #draw
            win.flip() #show  
        while 1 < pres_timer.getTime() <=3: #2 seconds
            my_image.draw() #draw
            fix_text.draw() #draw fixation on top of the image
            win.flip() #show  

You can also use a CountdownTimer, which is similar to Clock, except instead of counting up, it counts - you guessed it - down. In that case you simply have to add time to your CountdownTimer after resetting the clock, and invert the times defined in your while loops:

In [ ]:
#define the countdown clock at the beginning of the experiment
pres_timer = core.CountdownTimer()

for block in range(nBlocks):
    #=====================
    #TRIAL SEQUENCE
    #=====================    
    for trial in range(nTrials):
        #-set stimuli and stimulus properties for the current trial
        #point to a different filename for each image
        my_image.image = os.path.join(image_dir,stims[trial])
        #-empty keypresses
        
        #=====================
        #START TRIAL
        #=====================   
        #reset stimulus presentation timer right before the first stimulus should appear
        pres_timer.reset()
        pres_timer.add(2) #add 2 seconds because your trial is 2 seconds
        #-draw stimulus
        while pres_timer.getTime() >0: #2 seconds
            my_image.draw() #draw
            win.flip() #show
            
win.close()               

And you can add stimuli (and trial time) as necessary. The complete code snippet you can run looks like this:

In [22]:
from psychopy import visual, monitors, event, core

#define the monitor parameters
mon = monitors.Monitor('myMonitor', width=35.56, distance=60)
mon.setSizePix([1600,900])
win = visual.Window(monitor=mon) #define a window

import os
#stuff you only have to define once at the top of your script
os.chdir('/home/shoarly/Documents/pytutorial/exp')
main_dir = os.getcwd() 
image_dir = os.path.join(main_dir,'images')

fix_text = visual.TextStim(win, text='+')
my_image = visual.ImageStim(win)

stims = ['face01.jpg','face02.jpg','face03.jpg'] #create a list if images to show    

nBlocks=2
nTrials=3

#define the countdown clock at the beginning of the experiment
pres_timer = core.CountdownTimer()

for block in range(nBlocks):
    #=====================
    #TRIAL SEQUENCE
    #=====================    
    for trial in range(nTrials):
        #-set stimuli and stimulus properties for the current trial
        #point to a different filename for each image
        my_image.image = os.path.join(image_dir,stims[trial])
    
        #reset stimulus presentation timer right before the first stimulus should appear
        pres_timer.reset()
        pres_timer.add(3) #add 3 seconds because your trial is 3 seconds
        #-draw stimulus
        while pres_timer.getTime() >= 2: #1 second
            fix_text.draw() #draw
            win.flip() #show  
        while 0 <= pres_timer.getTime() < 2: #2 seconds
            my_image.draw() #draw
            fix_text.draw() #draw fixation on top of the image
            win.flip() #show  
            
win.close()            
1676.4303 	WARNING 	Monitor specification not found. Creating a temporary one...
1731.8815 	WARNING 	Monitor specification not found. Creating a temporary one...

You can use clocks for most experiment timing needs, but what if you want millisecond precision? As precise as clocks are, they are not perfectly accurate because the duration of your stimuli are limited by your monitor's refresh rate. If your monitor has a refresh frequency of 60 Hz (pretty common), that means your computer screen refreshes 60 times in 1 second = 1/60 = 16.666..ms frames. Therefore, a stimulus can be presented for 16.666 ms, 33.333 ms, 50 ms, 66.666 ms...etc, but nothing in between. For millisecond precision based on your hardware limitations, you should therefore use frame-based timing.

The first thing you need to find out is your monitor's refresh rate. Typically you would need to search for this manually in your computer monitor settings. Whatever it is (x), define the refresh rate as 1.0/x:

In [23]:
refresh=1.0/60.0 #single frame duration in seconds
print(1/(1/60))
60.0

Then you can define your stimulus durations based on this number:

In [24]:
#So this:
fix_dur = 1.0 #1 sec
image_dur = 2.0 #2 sec
text_dur = 1.5 #1.5 sec

#Becomes:
fix_frames = fix_dur / refresh
image_frames = image_dur / refresh
text_frames = text_dur / refresh

print("Seconds:", fix_dur, image_dur, text_dur)
print("Frames:", fix_frames, image_frames, text_frames)
Seconds: 1.0 2.0 1.5
Frames: 60.0 120.0 90.0

When you are deciding upon stimulus presentation times, it is good if your durations in seconds equate to whole numbers of frames as shown sbove. This will give you the most accurate timing whether you use frame-based or clock-based timing. Here is how you can implement frame-based timing into your trial loop:

In [37]:
from psychopy import visual, monitors, event, core

#define the monitor parameters
mon = monitors.Monitor('myMonitor', width=35.56, distance=60)
mon.setSizePix([1600,900])
win = visual.Window(monitor=mon) #define a window

#set durations
fix_dur = 0.2 #200 ms
image_dur = 0.1 #100 ms
text_dur = 0.2 #200 ms

#set frame counts
fix_frames = int(fix_dur / refresh) #whole number
image_frames = int(image_dur / refresh) #whole number
text_frames = int(text_dur / refresh) #whole number
#the total number of frames to be presented on a trial
total_frames = int(fix_frames + image_frames + text_frames)

fix = visual.TextStim(win, text='+')

nBlocks=1
nTrials=1

for block in range(nBlocks):
    #=====================
    #TRIAL SEQUENCE
    #=====================    
    for trial in range(nTrials):
        #-set stimuli and stimulus properties for the current trial
        
        
        #=====================
        #START TRIAL
        #=====================   
        for frameN in range(total_frames): #for the whole trial...
            #-draw stimulus
            if 0 <= frameN <= fix_frames: #number of frames for fixation      
                fix.draw() #draw
                win.flip() #show
                
                if frameN == fix_frames: #last frame for the fixation
                    print("End fix frame =", frameN) #print frame number
                    
            #number of frames for image after fixation
            if fix_frames < frameN <= (fix_frames+image_frames):      
                fix.draw() #draw
                win.flip() #show 
                
                if frameN == (fix_frames+image_frames): #last frame for the image
                    print("End image frame =", frameN) #print frame number  
                    
            #number of frames for the final text stimulus    
            if (fix_frames+image_frames) < frameN < total_frames:  
                fix.draw() #draw
                win.flip() #show  
                
                if frameN == (total_frames-1): #last frame for the text
                    print("End text frame =", frameN) #print frame number    
                
win.close()                
End fix frame = 12
End image frame = 18
End text frame = 29
3089.0793 	WARNING 	Monitor specification not found. Creating a temporary one...

This is the most accurate way to measure timing... IF you are not dropping frames. Your experiment may drop frames for varius reasons - if you are running simultaneous programs, if you are running your experiment from a virtual environment, etc. This can lead to lags on the order of several milliseconds. If there is a lag, the experiment will wait until another frame is completed, thus "dropping" the last frame. You can check if your experiment is dropping frames with the following code:

In [ ]:
#this can output various information about your experiment
from psychopy import logging

win.recordFrameIntervals = True #record frames
#give the monitor refresh rate plus a few ms tolerance (usually 4ms)
win.refreshThreshold = 1.0/60.0 + 0.004

# Set the log module to report warnings to the standard output window 
#(default is errors only).
logging.console.setLevel(logging.WARNING)

#-------------------  
#EXPERIMENT CODE HERE
#for block in range(nBlocks):
    #=====================
    #TRIAL SEQUENCE
    #=====================    
    #define stimulus params
    
    #=====================
    #START TRIAL
    #=====================   
    #for trial in range(nTrials):
#-------------------           

        #this will print total number of frames dropped following every trial
        print('Overall, %i frames were dropped.' % win.nDroppedFrames)

win.close()

Implemented with runnable code, it would look something like this:

In [40]:
#this can output various information about your experiment
from psychopy import visual, monitors, event, core, logging

#define the monitor parameters
mon = monitors.Monitor('myMonitor', width=35.56, distance=60)
mon.setSizePix([1600,900])
win = visual.Window(monitor=mon) #define a window

#set durations
fix_dur = 0.2 #200 ms
image_dur = 0.1 #100 ms
text_dur = 0.2 #200 ms

#set frame counts
fix_frames = int(fix_dur / refresh) #whole number
image_frames = int(image_dur / refresh) #whole number
text_frames = int(text_dur / refresh) #whole number
#the total number of frames to be presented on a trial
total_frames = int(fix_frames + image_frames + text_frames)

fix = visual.TextStim(win, text='+')

nBlocks=1
nTrials=1

#add information to record dropped frames
win.recordFrameIntervals = True #record frames
#give the monitor refresh rate plus a few ms tolerance (usually 4ms)
win.refreshThreshold = 1.0/60.0 + 0.004

# Set the log module to report warnings to the standard output window 
#(default is errors only).
logging.console.setLevel(logging.WARNING)

for block in range(nBlocks):
    #=====================
    #TRIAL SEQUENCE
    #=====================    
    for trial in range(nTrials):
        #-set stimuli and stimulus properties for the current trial
        #=====================
        #START TRIAL
        #=====================   
        for frameN in range(total_frames): #for the whole trial...
            #-draw stimulus
            if 0 <= frameN <= fix_frames: #number of frames for fixation      
                fix.draw() #draw
                win.flip() #show
                    
            #number of frames for image after fixation
            if fix_frames < frameN <= (fix_frames+image_frames):      
                fix.draw() #draw
                win.flip() #show  
                    
            #number of frames for the final text stimulus    
            if (fix_frames+image_frames) < frameN < total_frames:  
                fix.draw() #draw
                win.flip() #show          

        #this will print total number of frames dropped following every trial
        print('Overall, %i frames were dropped.' % win.nDroppedFrames)

win.close()
Overall, 0 frames were dropped.
3380.9673 	WARNING 	Monitor specification not found. Creating a temporary one...

If your experiment keeps dropping frames, you may actually notice a lag in your stimulus presentation when using frame-based timing. In this case, you can try to make your experiment run more efficiently (closing multiple programs, running the experiment outside of a virtual environment), but if this is not possible, it is better to use clock-based timing.