Python Tutorial: Level 4

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

Updated August 2020

Taught by Reshanne Reeder

The first thing you should do in an experiment, before presenting stimuli and collecting responses, is to collect participant information. This includes at the very least the participant number, as well as various demographic information that will be needed for any report of your experiment (gender, handedness, age, among other things). The best way to collect participant info is with a dialog box.

We first have to create a dictionary of variables, with each variable representing some information we want to collect. Like this:

In [1]:
exp_info = {'subject_nr':0, 'age':0, 'handedness':('right','left','ambi'), 
            'gender':('male','female','other','prefer not to say')}

print(exp_info)
{'subject_nr': 0, 'age': 0, 'handedness': ('right', 'left', 'ambi'), 'gender': ('male', 'female', 'other', 'prefer not to say')}

Then, we have to import the psychopy module "gui" (graphical user interface):

In [2]:
from psychopy import gui

You can take a look at the various functions contained in "gui". The one we are going to use is "DlgFromDict" which createa a dialog box from a dictionary you define. The psychopy help page gives you the following information:

class psychopy.gui.DlgFromDict(dictionary, title='', fixed=None, order=None, tip=None, screen=-1, sortKeys=True, copyDict=False, labels=None, show=True, **kwargs)

It shows you that psychopy.gui.DlgFromDict() is a class you can import. If you don't import "gui" at the top of your script, you will have to create your dialog box by typing "psychopy.gui.DlgFromDict", followed by all the arguments in parentheses. If you import gui at the top of your script, you can create your dialog box with "gui.DlgFromDict".

So what is all that stuff in the parentheses? This tells you all the options you have for customizing your dialog box. There are lots of default settings you do not need to change right now -- the only necessary argument to create a dialog box is the dictionary.

In [7]:
my_dlg = gui.DlgFromDict(dictionary=exp_info)

Running this will open up a dialog box:

dlg_ex2.png

Now you can enter information in the empty boxes and use the drop-down lists. If you press "OK", the information will be updated in the dictionary and saved in the Variable explorer. If you press "Cancel", the dictionary will refresh back to the default empty boxes.

You can customize the dialog box in different ways.

To create a drop-down list by defining options in a tuple, like handedness and gender:

In [8]:
{'handedness':('right','left','ambi'), 'gender':('male','female','other','prefer not to say')}
Out[8]:
{'handedness': ('right', 'left', 'ambi'),
 'gender': ('male', 'female', 'other', 'prefer not to say')}

You can tinker with the different options to customize dialog boxes in the exercises.

Once you enter info in the dialog box, you can flag certain things that might be bad for your experiment later on with another dialog box -- for example, if you forgot to enter the subject_nr, or the subject is not 18 and therefore cannot give consent to take part in an experiment. Flagging errors only requires an error message, so you don't need to make a DlgFromDict for this. You can use PsychoPy's original Dlg function:

In [9]:
from psychopy import core
#make sure subject data is entered correctly
if exp_info['subject_nr'] ==0: #nothing entered
    #create another dialog box (not from a dictionary because we're just showing an error message)
    err_dlg = gui.Dlg(title='error message') #give the dlg a title
    err_dlg.addText('Enter a valid subject number!') #create an error message
    err_dlg.show() #show the dlg
    core.quit() #quit the experiment
    
#make sure subject can consent to taking part in the experiment        
if exp_info['age'] < 18:
    err_dlg = gui.Dlg(title='error message')
    err_dlg.addText('%d year olds cannot give consent!' % (exp_info['age']))
    err_dlg.show()
    core.quit()

The Dlg function requires you to manually addtext, show the box, and then close it. DlgFromDict is more clever and does this all automatically. You can create complex dialog boxes with Dlg as well, but it is less efficient to have to define every field in another line of code. For example, you can create my_dlg like this:

In [10]:
my_dlg = gui.Dlg(title="my experiment")
my_dlg.addText('exp_info')
my_dlg.addField('age:',0)
my_dlg.addField('gender:', choices=['female', 'male', 'other', 'prefer not to say'])
my_dlg.addField('handedness:', choices=['right', 'left'])
my_dlg.addField('subject_nr:',0)
show_dlg = my_dlg.show()  # show dialog and wait for OK or Cancel

Once you've collected your subject info, it's updated in the dictionary exp_info. You can add things to exp_info after the subject is done entering their information - for example, today's date. This is handy to store with the subject info. To automatically store the date of the experiment, you must first import the python function datetime:

In [11]:
from datetime import datetime

date = datetime.now() #what time is it right now?
print(date)
2020-08-09 11:55:02.378253

This gives you an output in the following order: year, month, day, hour, minute, second, microsecond. You can choose how much of this output you want to store with your subject info (I usually only save the day, month, and year). To add a variable to exp_info, simply define it as you would any other variable in the dictionary:

In [12]:
exp_info['date'] = str(date.day) + str(date.month) + str(date.year)
print(exp_info['date'])
982020

You can make this look prettier by separating values with slashes or dashes, etc. But you get the idea.

Finally, we want to create a unique filename so that we can easily save our exp_info data (we'll learn how to save later).

In [16]:
#create a unique filename (don't forget to turn integers into strings for the filename)
#don't forget to add the filetype at the end: csv, txt...
filename = str(exp_info['subject_nr']) + '_' + exp_info['date'] + '.csv'
print(filename)

import os
main_dir = os.getcwd() #define the main directory where experiment info is stored
#create a subject info directory to save subject info
sub_dir = os.path.join(main_dir,'sub_info',filename)
1_982020.csv

And that's how you can collect and store experiment info from a dialog box and datetime in 30 seconds.

Once you have collected subject info, you can go to the next step of starting the experiment: defining the monitor (the parameters of the specific monitor you are working on) and window (the screen on which you will show your experiment). This is important because depending on the size and shape of the monitor, your experiment may look different. For example, if you have stimuli that appear a certain distance from the center, you don't want parts of them to be cut off because your monitor is narrower than you expected. The window is defined separately from the monitor, but it relies on the parameters of the monitor. You can specify whether you want your experiment window to appear in a smaller box on your monitor, or it can be fullscreen.

First, we have to import the respective functions from psychopy (window requires the "visual" function):

In [17]:
from psychopy import visual, monitors
pygame 1.9.6
Hello from the pygame community. https://www.pygame.org/contribute.html

Let's look at the "monitor" information that psychopy provides:

class psychopy.monitors.Monitor(name, width=None, distance=None, gamma=None, notes=None, useBits=None, verbose=True, currentCalib=None, autoLog=True)

Again, this tells you the syntax needed to define a monitor, and the arguments you can include.

We have to know some things about our current monitor: its width in cm, its size in pixels, and your (rough) distance from the screen in cm. Distance is typically set at 57 or 60cm if subjects in your experiment are sitting at a "free viewing distance" from the screen. If you need your subjects to be an exact distance away (and you use a chin rest or stabilize the subject in another way), grab a friend and measure with a tape measure. You also have to measure the diagonal of your monitor to find the width. You should be able to find the pixel resolution of your monitor in the monitor settings on your computer. These are the three arguments you need to define the monitor. The other arguments can stay at default for now.

In [18]:
#you will need to change this for every new monitor on which you run your experiment
#your own monitor width and pixel resolution will probably be different from mine
#give your monitor a name, define width and distance
mon = monitors.Monitor('myMonitor', width=35.56, distance=60) 
#use x,y coordinates to specify the pixel resolution of your monitor
mon.setSizePix([1600,900])

That's all we need to define the monitor. Now on to the window:

class psychopy.visual.Window(size=(800, 600), pos=None, color=(0, 0, 0), colorSpace='rgb', rgb=None, dkl=None, lms=None, fullscr=None, allowGUI=None, monitor=None, bitsMode=None, winType=None, units=None, gamma=None, blendMode='avg', screen=0, viewScale=None, viewPos=None, viewOri=0.0, waitBlanking=True, allowStencil=False, multiSample=False, numSamples=2, stereo=False, name='window1', checkTiming=True, useFBO=False, useRetina=True, autoLog=True, gammaErrorPolicy='raise', *args, **kwargs)

As you can see, the default window that is created has a size of 800x600 pixels, is centered on the monitor, is white, etc etc. The only argument you need to specify, if you want to keep all the other defaults, is the monitor. Window is linked to the monitor parameters you specify, so you can simply write:

In [19]:
win = visual.Window(monitor=mon)

-and you have your window. To add arguments, simply add them inside the parentheses in any order you like:

In [20]:
win = visual.Window(monitor=mon, size=(800,800), color=[-1,-1,-1])

Once the monitor and window have been defined, you can create your stimuli. The stimuli can be created before you show them on-screen. To present a stimulus, you have to tell python to flip the window (win.flip) which we will get to in a moment. It's good to pre-define as much as you can about your stimuli before you start your trial loop. This will give you the most efficient experiment.

Firstly, you can visit the psychopy webpage on the visual module to take a look at all the different stimuli you can create. The ones we are going to use for our experiment are text and images. Let's start with text:

In [21]:
my_text = visual.TextStim(win)

If you look at the psychopy help page on TextStim (click on the highlighted text on the "visual" page), the only argument you need to define is "win". win points to monitor, and all is well. However, if you want to customize your text, you have to define it in your script. For example:

In [22]:
start_msg = 'Welcome to my experiment!'
my_text = visual.TextStim(win, text=start_msg)

So let's present our text in a window. The basic information we need is this:

In [23]:
from psychopy import visual, monitors

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

start_msg = 'Welcome to my experiment!' #write the start message
my_text = visual.TextStim(win, text=start_msg) #create the text stimulus

Then we need to draw the stimulus:

In [27]:
my_text.draw()

This draws the text to a back-buffer, waiting to be flipped to the front (where we can see it). To finally show the text, we need to add one more line:

In [28]:
win.flip()
Out[28]:
432.5368540287018

If you write all of this, it will show the text... but the text will never go away. We have to control how long a stimulus appears and when to close the experiment. To make the stimulus appear until a button has been pressed, then close the experiment, we need two lines:

In [29]:
from psychopy import event #first import the "event" module
event.waitKeys() #wait for a keypress
win.close() #close the window
375.9620 	WARNING 	Monitor specification not found. Creating a temporary one...
407.4410 	WARNING 	Monitor specification not found. Creating a temporary one...

And that's how you pre-define, draw, show, wait, and close a text stimulus. But usually in an experiment you have several stimuli you want to loop through. How do you present multiple text stimuli at different parts of the experiment? You are going to need some for loops.

Let's start the example by creating some text we might want to show at different points in our experiment:

In [30]:
start_msg = "Welcome to my experiment!"
block_msg = "Press any key to continue to the next block."
end_trial_msg = "End of trial"

You can then create the visual text stimuli a few different ways. You can create a different stimulus for each text message:

In [33]:
from psychopy import visual, monitors, event

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

start_text = visual.TextStim(win, text=start_msg) #text=start_msg is the same as writing text="Welcome to my experiment!"
block_text = visual.TextStim(win, text=block_msg)
end_trial_text = visual.TextStim(win, text=end_trial_msg)

start_text.draw()
win.flip() #show the stim
event.waitKeys() #wait for keypress

block_text.draw()
win.flip() #show the next stim
event.waitKeys() #wait for keypress

end_trial_text.draw()
win.flip() #show the next stim
event.waitKeys() #wait for keypress

win.close() #close the window
696.9062 	WARNING 	Monitor specification not found. Creating a temporary one...

Or you can pre-define a single text stimulus and later update what is contained in the "text" argument, like this:

In [35]:
from psychopy import visual, monitors, event

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

my_text = visual.TextStim(win) #create the text stimulus but don't define the text yet

my_text.text = start_msg #define the text
my_text.draw()
win.flip() #show
event.waitKeys() #wait for keypress

my_text.text = block_msg #define the text
my_text.draw()
win.flip() #show
event.waitKeys() #wait for keypress

my_text.text = end_trial_msg #define the text
my_text.draw()
win.flip() #show
event.waitKeys() #wait for keypress

win.close() #close the window
718.5650 	WARNING 	Monitor specification not found. Creating a temporary one...

You can update any arguments for a stimulus by typing the stimulus name, followed by a dot, then the argument you want to update. For example:

In [36]:
my_text.size = 50
my_text.color = 'blue'
my_text.bold = True

You have to make your changes to the arguments before you draw and flip your stimulus. Drawing and flipping always come after you've defined everything you want to show.

So how do you change the text for different parts of the experiment? Here come the for loops!:

In [39]:
from psychopy import visual, monitors, event

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

nBlocks=2
nTrials=3

my_text = visual.TextStim(win) #create the text stimulus but don't define the text yet

my_text.text = start_msg #define the text
my_text.draw()
win.flip()
event.waitKeys() #wait for keypress

for block in range(nBlocks):
    my_text.text = block_msg #define the text
    my_text.draw()
    win.flip()
    event.waitKeys() #wait for keypress
    
    for trial in range(nTrials):
        #present some other stimuli first
        #stim.draw()
        #win.flip()
        #...
        #at the end of the trial...
        my_text.text = end_trial_msg + str(trial) #define the text and trial #
        my_text.draw()
        win.flip()
        event.waitKeys() #wait for keypress
        
win.close()        
861.1009 	WARNING 	Monitor specification not found. Creating a temporary one...
892.6318 	WARNING 	Monitor specification not found. Creating a temporary one...

You can then add multiple other stimuli of different types throughout your experiment. For example, an image:

class psychopy.visual.ImageStim(win, image=None, mask=None, units='', pos=(0.0, 0.0), size=None, ori=0.0, color=(1.0, 1.0, 1.0), colorSpace='rgb', contrast=1.0, opacity=1.0, depth=0, interpolate=False, flipHoriz=False, flipVert=False, texRes=128, name=None, autoLog=None, maskParams=None)

Just like with a text stimulus, the only thing you have to define when you create your image stimulus, is the window:

In [40]:
my_image = visual.ImageStim(win)

You then have to specify the image using the "image" argument. To specify a picture saved on your computer, you simply have to direct psychopy to its location:

In [44]:
from psychopy import visual, monitors, event

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

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

pic_loc = os.path.join(image_dir,'face01.jpg') #point to the specific image

my_image = visual.ImageStim(win, image=pic_loc) #image= the specific image (the entire directory)

my_image.draw()
win.flip()
event.waitKeys() #wait for keypress
win.close()
1044.1093 	WARNING 	Monitor specification not found. Creating a temporary one...

To change the image that is presented in "my_image", you can use the same "argument changing" syntax that you used with text stimuli:

In [47]:
from psychopy import visual, monitors, event

#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')

import numpy as np #for random shuffling
stims = ['face01.jpg','face02.jpg','face03.jpg'] #create a list if images to show
nTrials=3 #create a number of trials for your images
#create the stimulus but don't specify the precise image yet
my_image = visual.ImageStim(win)

np.random.shuffle(stims) #shuffle order of stims

for trial in range(nTrials): #loop through trials
    #point to a different filename for each image
    my_image.image = os.path.join(image_dir,stims[trial])
    
    my_image.draw() #draw
    win.flip() #show
    event.waitKeys() #wait for keypress
    
win.close() #close the window after all trials have looped    
1843.1360 	WARNING 	Monitor specification not found. Creating a temporary one...

To show simultaneous stimuli, simply draw one right after the other before you flip the window. For example:

In [48]:
from psychopy import visual, monitors, event

#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

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

for trial in range(nTrials): #loop through trials
    
    my_image.image = os.path.join(image_dir,stims[trial])
    
    my_image.draw() #draw
    fix_text.draw() #draw fixation at the same time
    win.flip() #show
    event.waitKeys() #wait for keypress
    
win.close() #close the window after all trials have looped    
1875.1706 	WARNING 	Monitor specification not found. Creating a temporary one...

Keep in mind that the order in which you draw the stimuli is important- draw the bigger image first, or else it will be drawn on top of the fixation cross and you won't be able to see the fixation. Draw the fixation on top of the image by putting its "draw" function last.

Now that you know how to create and show text and images in a block or trial sequence, you can put it all together to create a coherent experiment structure! For now do not make the experiment run on its own timing, but rather stick with event.waitKeys() to flip to the next window in the exercises.