Advanced: Miscellanious tricks for improving your code

Beyond the Python Tutorial

By Reshanne Reeder

Here you will find some bits of code and tips for improving/enhancing your experiments. I will make updates as I find out new things, or get some new problems. If you would like to see something here, please feel free to contact me!

Something I didn't have time to cover in the tutorial proper, was how to present multiple stimuli at the same time. The only thing you have to do is draw more objects before flipping the window. So instead of this:

In [ ]:
#this will draw stim1 for 1 second, then stim2 for 1 second
stim1.draw()
win.flip()
core.wait(1)

stim2.draw()
win.flip()
core.wait(1)

You write this:

In [ ]:
#this will draw stim2 on top of stim1 for 1 second
stim1.draw()
stim2.draw()
win.flip()
core.wait(1)

That's it! It's important to keep in mind the order in which you draw the stimuli. If you want to present a word of text on top of a larger image, draw the image first and the text second. Otherwise, the image will be drawn on top of the text and you will only be able to see the image. If you don't want your stimuli to appear overlapping in the center of the screen, predefine the locations of each stimulus:

In [ ]:
stim1.pos=(20,0) #present stim1 20 pixels to the right of center
stim2.pos=(-20,0) #present stim2 20 pixes from the left of center

stim1.draw()
stim2.draw()
win.flip()
core.wait(1)

It can get cumbersome to write out multiple stimuli and locations as your experiment becomes more complex, especially if you want to show lots of different stimuli at many different locations on a single trial. Here is where it can be useful to define small functions to neaten up your code. A function is basically a template (for a display layout, a calculation, anything that you might use repeatedly in your task), which you can create before running your experiment, and invoke at any time during the experiment. The syntax is like this:

In [ ]:
#defining a function
def name_of_function(arg1, arg2, ...):
    statement1
    statement2
    ...

"def" tells python that you are defining a new function. Name your function -- it can be anything (no spaces), but keep it practical. The arguments are placeholder names, which you will replace with the actual stimulus names when you execute your function in the experiment. The statements specify what it is you want the function to do with those stimuli. For example, say you want to present stimuli both to the left and right of center, and you want your function to have an argument for the left-side stimuli and an argument for the right-side stimuli. You could start with something like this:

In [ ]:
#for example
def left_right_text(left,right):

Then, your statements can be various experiment parameters you don't want to write out every time you flip your window:

In [ ]:
#for example
def left_right_text(left,right):
    left.text=text_left[trial] #assign text to the left side
    right.text=text_right[trial] #assign text to the right side
    left.pos=(-100,0) #assign left position in pixels
    right.pos=(100,0) #assign right position in pixels
    left.color='white' #assign color to left
    right.color='white' #assign color to right
    left.draw() #draw left
    right.draw() #draw right

You would define this before you start your trial loop. In action, it might look something like this:

In [11]:
from psychopy import core, visual, monitors

#monitor specs
mon = monitors.Monitor('myMonitor', width=35.56, distance=60)
mon.setSizePix([1920, 1080])
win = visual.Window(monitor=mon, size=(800,800), color='black', units='pix')

#for example
def left_right_text(left,right):
    left.text=text_left[trial] #assign text to the left side
    right.text=text_right[trial] #assign text to the right side
    left.color='white' #assign color to left
    right.color='white' #assign color to right
    left.pos=(-100,0) #assign left position
    right.pos=(100,0) #assign right position
    left.height=50 #assign left text height in pixels
    right.height=50 #assign right text height in pixels
    left.draw() #draw left
    right.draw() #draw right

#in action
stim1 = visual.TextStim(win, units='pix') #random text stim
stim2 = visual.TextStim(win, units='pix') #random text stim

text_right=['This', 'is', 'the', 'right']
text_left =['Here','on','the','left']

nTrials=4 #4 trials for the 4 words

#cycle through different stimuli on each side
for trial in range(nTrials):
    left_right_text(stim1,stim2)
    win.flip() #flip window and show all of it
    core.wait(1) #wait 1 second
    
win.close() #close window
777.4983 	WARNING 	Monitor specification not found. Creating a temporary one...

Oftentimes when you have multiple stimuli, you not only want different information to be presented at the same time, but also at different frequencies. For example, you might want to present a static stimulus against a changing background. For this, it is a good idea to switch to a timer for stimulus presentation (using a while loop), so that you can add frame counting within the loop. Then you simply flip each stimulus for a different number of frames:

In [15]:
from psychopy import core, visual, monitors

#monitor specs
mon = monitors.Monitor('myMonitor', width=35.56, distance=60)
mon.setSizePix([1920, 1080])
win = visual.Window(monitor=mon, size=(800,800), color='black', units='pix')

timer = core.CountdownTimer() #create countdown timer
#your background that changes at its own frequency:
flicker_box = visual.Rect(win, size=(100,100), units='pix')
    
def left_right_text(left,right):
    left.text=text_left[trial] #assign text to the left side
    right.text=text_right[trial] #assign text to the right side
    left.pos=(-100,0) #assign left position
    right.pos=(100,0) #assign right position
    left.draw() #draw left
    right.draw() #draw right

#cycle through different stimuli on each side
for trial in range(nTrials):   
    
    frameN=-1 #set frame for beginning of trial
    timer.reset() #reset timer
    timer.add(2) #2-second trial time
    
    while timer.getTime() > 0: #until the end of trial
        frameN+=1#count frames 
        
        #create the flickering background and draw
        if frameN%(2*4) < 4: #every 4 frames, or whatever you want it to be
            flicker_box.color='purple' 
        else: flicker_box.color='blue'
        flicker_box.draw()
        
        #present the text
        left_right_text(stim1,stim2)
        
        win.flip() #flip every frame
        
win.close()
982.6053 	WARNING 	Monitor specification not found. Creating a temporary one...

You can further neaten your code by putting the flicker into a function:

In [19]:
from psychopy import core, visual, monitors

#monitor specs
mon = monitors.Monitor('myMonitor', width=35.56, distance=60)
mon.setSizePix([1920, 1080])
win = visual.Window(monitor=mon, size=(800,800), color='black', units='pix')

def flicker(stim,freq): #what is the stimulus and flicker frequency?
    if frameN%(2*freq) < freq:
            stim.color='purple' 
    else: stim.color='blue'
    stim.draw()
        
#cycle through different stimuli on each side
for trial in range(nTrials):   
    
    frameN=-1 #set frame for beginning of trial
    timer.reset() #reset timer
    timer.add(2) #2-second trial time
    
    while timer.getTime() > 0: #until the end of trial
        frameN+=1#count frames 
        
        #create the flickering background and draw
        flicker(flicker_box,4) #present the box we created at the desired frequency
        #remember to present the text on top of the box so it's not hidden
        left_right_text(stim1,stim2)
        
        win.flip() #flip every frame
        
win.close()      
1111.2367 	WARNING 	Monitor specification not found. Creating a temporary one...

This is just one example of how you can set your different stimuli with independent frequencies. Remember, the window is being redefined on every frame, so static stimuli only appear static -- they are really refreshing with your monitor. If you don't tell the stimuli to change with every frame, but instead on every trial (like the text stimuli in the above example), the text will stay consistent throughout the trial.

If you would rather have your text stimulus appear with a certain number of frames, change the part of your function that sets its identity:

In [1]:
#So this...
def left_right_text(left,right):
    left.text=text_left[trial] #assign text to the left side
    right.text=text_right[trial] #assign text to the right side
    
#...becomes... 
from math import floor #import function to round down
def left_right_text(left,right,freq):
    left.text = text_left[floor(frameN/freq)%4]
    right.text = text_right[floor(frameN/freq)%4]

OK maybe I made this a little complicated, but I wanted to come up with a method of cycling through the list of words based on frame number rather than trial number and this was the most efficient way I could think of.

So I imported the handy "floor" function, then defined a function that takes a given frequency, and performs indexing based on frame number frequency. To present the text for the given number of frames (rather than flipping the text every frame), I used indexing based on floor(frameN/freq). For example, if you input frequency as 4 frames, then frames 0-3 will give an index of 0 (the floor of 0/4=0, same with 1/4=0, 2/4=0, 3/4=0). Then frame 4 will change the index to 1. Like this:

pyexample_1.png

Therefore changing the index every 4 frames. However, once you get to 16 frames, simply using floor(frameN/4) does not work:

pyexample_2.png

In our example, we only have 4 words to cycle through, so we need to keep indexing between 0-3. Therefore, we need to add a final component to the calculation - the modulus (division with remainder) to keep the output values under 4:

floor(frameN/freq)%4

You can change "%4" to "%nWords" or another value to make it more flexible for your own purposes.

Suppose you want to present stimuli that move around the screen. PsychoPy has some stimuli you can choose from already:

RadialStim to present rotating checkerboard patterns (e.g., for retinotopic mapping)

DotStim to present patterns of moving dots (e.g., for dot kinematograms)

But say you want to do something else. For example, you want to present a single stimulus that drifts away from the center of the screen. How do you control the direction and speed? To set the direction of motion, you simply have to specify x/y coordinates. To set the speed, you have to specify how often you want the coordinates to change. The key again lies in making use of frame-based timing. If you are already cycling over a number of frames (counting up from 0), you can use them to define the new position of your stimulus on each trial:

In [ ]:
# drift at the refresh rate of your screen
stim.pos=(0,frameN) #make your stimulus drift UP
stim.pos=(0,-frameN) #make your stimulus drift DOWN
stim.pos=(frameN,0) #make your stimulus drift RIGHT
stim.pos=(-frameN,0) #make your stimulus drift LEFT
stim.pos=(frameN,frameN) #make your stimulus drift DIAGONALLY

You get the idea. You can use the frame number to specify the number of pixels to move on each flip. You can also speed up or slow down the drift rate by using multiplication or division:

In [ ]:
stim.pos=(0,frameN/2) #make your stimulus drift UP, SLOWER
stim.pos=(0,frameN*2) #make your stimulus drift UP, FASTER

To use it in a piece of code, it looks like this:

In [ ]:
from psychopy import visual, monitors, core
#-define the monitor settings using psychopy functions
mon = monitors.Monitor('myMonitor', width=35.56, distance=60)
mon.setSizePix([1920, 1080])
win = visual.Window(fullscr=False, monitor=mon, size=(800,800), color='black', units='pix')

timer = core.CountdownTimer() #timer for trial duration
stim = visual.Rect(win, size=(100,100), color='blue') #random example stimulus

nTrials=5 #number of trials to demonstrate effect

#start trial sequence
for trial in range(nTrials):   
    frameN=-1 #set frame for beginning of trial
    timer.reset() #reset timer
    timer.add(2) #2-second trial time
    while timer.getTime() > 0: #until the end of trial
           
        frameN+=1#count frames 
        stim.draw() #draw stim
        stim.pos=(0,frameN/4) #make stim move slowly upward from center
        win.flip()

win.close()

Keep in mind the position that you specify has to be in the units you assigned to your window (pixels, degrees of visual angle...). This particular bit of code tells the stimulus to move up a quarter of a pixel every frame.

You can change a lot about your stimulus dynamically this way - making the stimulus grow or shrink:

In [ ]:
flicker_box.size=(frameN) #this will make it grow 1 pixel every frame

And you can change the speed of growth or shrinkage in the same way you changed speed of motion. Pay attention to how things like position and size are defined, because you have to write the proper syntax. If you specify the size of the Rect with "size", it will automatically change both the width and height dimensions. If you want to change height and width independently, you have to define those dimensions:

In [ ]:
flicker_box.height=(frameN) #this will make it taller by 1 pixel every frame
flicker_box.width=(frameN*2) #this will make it taller by 2xframeN every frame

But in that case, make sure you specify both height and width instead of size (that is, remove the "size=" argument from when you defined your stimulus), or else the values will interact weirdly.