Advanced: Misc tricks for improving your code
BACK TO Level 0: A New Language
BACK TO Level 1: Manipulating Variables
BACK TO Level 2: Conditionals and Loops
BACK TO Level 3: PsychoPy 101
BACK TO Level 4: PsychoPy - Showing windows and stimuli
BACK TO Level 5: PsychoPy - Clocks and timing
BACK TO Level 6: PsychoPy - Response collection and saving data
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:
#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:
#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:
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:
#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:
#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:
#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:
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
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:
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()
You can further neaten your code by putting the flicker into a function:
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()
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:
#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:
Therefore changing the index every 4 frames. However, once you get to 16 frames, simply using floor(frameN/4) does not work:
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:
# 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:
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:
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:
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:
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.