In an effort to switch to open-source programs, and also as an excuse for me to learn Python, I started programming experiments in PsychoPy in 2018. Initially I only used PsychoPy Coder (hence Examples 1 - 3 are all code), but now I add custom scripts into Builder instead.
I'll update this list as I add more entries.
This simple example experiment, made in PsychoPy Builder, implements online mouse tracking (with Pavlovia) using a bit of Python and JS code. This is the basis of my experiments that involve visual world paradigm mouse tracking.
I programmed FrootStroop for a public engagement event while at UCD. It runs on PsychoPy and uses the functions listed below (random and array functions). I've updated it periodically for subsequent public events, such as university open days, ESRC festival of social science, and the British Science Festival.
The full experience requires fruits hooked up to a Makey Makey kit, but it works with keyboard input as well.
Download it here.
*UPDATE* - This tutorial is for building an experiment entirely from scratch with PsychoPy coder. I've since moved to using PsychoPy Builder, although I do still use some of the code (e.g., using random and array functions) using the code component.
This current experiment is a bit more complex than a standard example as it has instructions, practice, feedback, and blocks. Also has an additional stimuli-response 'contingency' manipulation. But it's the simplest actual experiment that I have at the moment. If anyone really wants a simpler one, let me know, and I'll write a better demo.
# ################# Initialising Project Settings #####################
from __future__ import division
from psychopy import visual, core, data, event, gui
import numpy as np
import random
win = visual.Window([1024,768], fullscr=True, units="pix", color=u'slategray')
respClock = core.Clock()
# ################# This part needs to be defined by experimenter #########################
# #########################################################################################
# #########################################################################################
# Defining number of stimuli in the experiment # ####
# ####
blocksPerContingency=2 # i.e. how many block each for contingency controlled/uncontrolled # ####
trialsPerBlock=4 # ####
practiceTrials=5 # ####
# ####
# ################# Defining colours and words used in experiment ################# # ####
# #### Half of each list will be randomly assigned to each contingency condition # ####
colourLibrary = ['red','green','yellow','blue','pink','white'] # ####
wordLibrary = ['wall','due','marvel','ship','knife','lot'] # ####
practiceWords = ['###','####','#####'] # ####
# #########################################################################################
# #########################################################################################
#randomises colours and word lists
random.shuffle(colourLibrary)
random.shuffle(wordLibrary)
#selects half of each list for first part
colours = colourLibrary[0:3]
neutralWords = wordLibrary[0:3]
# ################# Defining the experimental conditions #################
condition = ['congruent','neutral']
contingency = ['controlled','notControlled']
random.shuffle(contingency)
totalBlocks=(blocksPerContingency*len(contingency))
totalTrials=(totalBlocks*trialsPerBlock)
trialsPerContingency=blocksPerContingency*trialsPerBlock
# ################# Setting up data file name #################
info = {}
info['dateStr'] = data.getDateStr()
filename = "data/" + "_" + info ['dateStr']
# ################# Defining instructions screen #################
# This screen will appear at the start. The two TextStim
# makes up the text that will appear
instructionText = visual.TextStim(win=win, name='instructionText',
text=u'This is a Stroop task\nYour goal is to respond to the colour of the word,\nwhile ignoring what it spells out\n\nDo this as quickly and accurately as you can',
pos=(0, 150), height=40, wrapWidth=1000, ori=0, alignHoriz='center',
color='white', colorSpace='rgb', opacity=1, font = 'Arial',
bold = True, depth=0.0);
instructionText2 = visual.TextStim(win=win, name='instructionText2',
text='Press the following number keys to respond:\n1 - '+str(colours[0])+' 2 - '+str(colours[1])+' 3 - '+str(colours[2])+'\n\nPress SPACE to start',
pos=(0, -150), height=50, wrapWidth=2000, ori=0, alignHoriz='center',
color='white', colorSpace='rgb', opacity=1, font = 'Arial',
bold = True, depth=0.0);
instructionText.draw()
instructionText2.draw()
win.flip()
keys = event.waitKeys(keyList = ['space']) #waits for spacebar to start
# ################# Initialise stimuli for fixation, target, and feedback #################
# 'background' is the colour of the background during the feedback. Since it is
# different from the default white a large coloured square is drawn (technically
# a grating mask with no grating).
fixation = visual.TextStim(win,text = '+',
bold = True,
height = 80,
color = u'black')
target = visual.TextStim(win,text = 'text',
bold = False,
height = 150)
feedback = visual.TextStim(win=win, name='feedback',
text='incorrect',
pos=(0, 50), height=80, wrapWidth=None, ori=0,
colorSpace='rgb', opacity=1,depth=0.0);
background = visual.GratingStim(
win=win, name='grating',
tex=None, mask=None,
ori=0, pos=(0, 0), size=(1500), sf=None, phase=0.0,
color= u'black', colorSpace='rgb', opacity=1,
texRes=128, interpolate=True, depth=0.0)
# Start of Practice block
for x in range(practiceTrials):
#randomly samples a colour and text
target.text = random.choice (practiceWords)
target.color = random.choice(colours)
target.pos = (0,0)
target.height = 60
#displays practice fixation cross. Duration is jittered
fixation.draw()
win.flip()
core.wait(random.uniform(0.4,0.6))
#displays practice stroop stimuli
target.draw()
win.flip()
respClock.reset()
#waits for a valid response for a maximum of 2.5 seconds
#records responses and calculates RT and accuracy
#exits program if 'escape' key is pressed
keys = event.waitKeys(keyList = ['1', '2', '3','escape'],maxWait=2.5)
if keys:
resp = keys[0]
rt=respClock.getTime()
if resp=='escape':
trials.finished = True
elif target.color== colours[0] and resp=='1':
corr = 1
elif target.color== colours[1] and resp=='2':
corr = 1
elif target.color== colours[2] and resp=='3':
corr = 1
#if incorrect response is given, feedback is displayed
else:
corr = 0
background.draw()
feedback.draw()
win.flip()
core.wait(1.5)
background.draw()
win.flip()
core.wait(0.1)
# Start of Experiment trials
instructionsClock = core.Clock()
initialText = visual.TextStim(win=win, name='initialText',
text=u'That was the end of the practice\nReady for the experiment?\nPress Space to start block 1 of '+str(totalBlocks),
pos=(0, 50), height=40, wrapWidth=2000, ori=0, alignHoriz='center',
color='white', colorSpace='rgb', opacity=1, font = 'Arial',
bold = True, depth=0.0);
initialText2 = visual.TextStim(win=win, name='initialText2',
text='Remember:\n 1 - '+str(colours[0])+' 2 - '+str(colours[1])+' 3 - '+str(colours[2])+'\n\nRespond as quickly and accurately as you can',
pos=(0, -250), height=50, wrapWidth=2000, ori=0, alignHoriz='center',
color='white', colorSpace='rgb', opacity=1, font = 'Arial',
bold = True, depth=0.0);
initialText.draw()
initialText2.draw()
win.flip()
keys = event.waitKeys(keyList = ['space']) #waits for spacebar to start
# ################# Setting up trial handler #################
# Set number of total trials with nReps
trials = data.TrialHandler(trialList=[], nReps=totalTrials)
# ################# Creates a new data file each time program is run #################
# Names the data file with the information we defined earlier, namely the date
# and time etc. and loops to add a new row for each trial repetition
thisExp = data.ExperimentHandler(
extraInfo = info,
dataFileName = filename,
)
thisExp.addLoop(trials)
# ################# Defining some variables #################
blockNumber = 1
trialIndex=0
congruentRT=[]
neutralRT=[]
# ################# Sequence at each trial #################
# ##########################################################
# ##########################################################
for currentTrial in range(trialsPerContingency):
currentCond = np.random.choice(condition,p=[.5,.5])
target.color = random.choice(colours)
target.pos = (0,0)
#If Congruent condition, target colour and word is the same
if currentCond == condition[0]:
target.text = target.color
#If Neutral condition, sample word from neutralWords at random
else:
if contingency[0] == 'notControlled':
target.text= np.random.choice(neutralWords)
else:
targetWordIndex = colours.index(target.color) - 1
target.text=neutralWords[targetWordIndex]
# ################# Trial counter #################
# Counts number of trials and resets it when it's a new block.
trialIndex=trialIndex + 1
if trialIndex > trialsPerBlock :
trialIndex = 1
#displays fixation cross. Duration is jittered
fixation.draw()
win.flip()
core.wait(random.uniform(0.4,0.6))
#displays stroop stimuli
target.draw()
win.flip()
respClock.reset()
#waits for a valid response for a maximum of 2.5 seconds
#records responses and calculates RT and accuracy
#exits program if 'escape' key is pressed
keys = event.waitKeys(keyList = ['1', '2', '3','escape'],maxWait=2.5)
if keys:
resp = keys[0]
rt=respClock.getTime()
if resp=='escape':
trials.finished = True
elif target.color== colours[0] and resp=='1':
corr = 1
elif target.color== colours[1] and resp=='2':
corr = 1
elif target.color== colours[2] and resp=='3':
corr = 1
#if incorrect response is given, feedback is displayed
else:
corr = 0
background.draw()
feedback.draw()
win.flip()
core.wait(1.5)
background.draw()
win.flip()
core.wait(0.1)
#if no response is given
else:
resp,rt,corr = 'n/a',-1,0
#performs a break after a block and counts the block number
if trialIndex > trialsPerBlock-1:
blockNumber = blockNumber + 1
# Initialize final screen
text_1 = visual.TextStim(win=win, name='text_1',
text=u'take a short break',
font=u'Arial',
pos=(0,150), height=50, wrapWidth=None, ori=0,
color=u'white', colorSpace='rgb', opacity=1,
depth=0.0);
initialText3 = visual.TextStim(win=win, name='initialText3',
text=u'Press Space to start block '+str(blockNumber)+' of '+str(totalBlocks),
pos=(0, 50), height=40, wrapWidth=2000, ori=0,
color='white', colorSpace='rgb', opacity=1, font = 'Arial',
bold = True, depth=0.0);
endScreen = visual.TextStim(win=win, name='endScreen',
text=u"That's the end of the experiment\nThank you for participating",
pos=(0, 50), height=40, wrapWidth=2000, ori=0,
color='white', colorSpace='rgb', opacity=1, font = 'Arial',
bold = True, depth=0.0);
instructionText20 = visual.TextStim(win=win, name='instructionText20',
text=u'The colours have now changed',
pos=(0, 150), height=40, wrapWidth=1000, ori=0, alignHoriz='center',
color='white', colorSpace='rgb', opacity=1, font = 'Arial',
bold = True, depth=0.0);
if blockNumber > blocksPerContingency:
instructionText20.draw()
else:
text_1.draw()
win.flip()
core.wait(2)
text_1.draw()
initialText3.draw()
initialText2.draw()
win.flip()
keys = event.waitKeys(keyList = ['space'])
# #### writes data to results file
trials.addData('contingency',contingency[0])
trials.addData('trialIndex', trialIndex)
trials.addData('word',target.text)
trials.addData('colour',target.color)
trials.addData('resp', resp)
trials.addData('rt', rt)
trials.addData('corr', corr)
trials.addData('condition',currentCond)
thisExp.nextEntry()
colours = colourLibrary[3:6]
neutralWords = wordLibrary[3:6]
instructionText20 = visual.TextStim(win=win, name='instructionText20',
text=u'The colours have now changed',
pos=(0, 150), height=40, wrapWidth=1000, ori=0, alignHoriz='center',
color='white', colorSpace='rgb', opacity=1, font = 'Arial',
bold = True, depth=0.0);
instructionText21 = visual.TextStim(win=win, name='instructionText21',
text='Press the following number keys to respond:\n1 - '+str(colours[0])+' 2 - '+str(colours[1])+' 3 - '+str(colours[2])+'\n\nPress SPACE to start',
pos=(0, -150), height=50, wrapWidth=2000, ori=0, alignHoriz='center',
color='white', colorSpace='rgb', opacity=1, font = 'Arial',
bold = True, depth=0.0);
instructionText20.draw()
instructionText21.draw()
win.flip()
keys = event.waitKeys(keyList = ['space']) #waits for spacebar to start
# Start of Practice block2
for x in range(practiceTrials):
#randomly samples a colour and text
target.text = random.choice (practiceWords)
target.color = random.choice(colours)
target.pos = (0,0)
target.height = 60
#displays practice fixation cross. Duration is jittered
fixation.draw()
win.flip()
core.wait(random.uniform(0.4,0.6))
#displays practice stroop stimuli
target.draw()
win.flip()
respClock.reset()
#waits for a valid response for a maximum of 2.5 seconds
#records responses and calculates RT and accuracy
#exits program if 'escape' key is pressed
keys = event.waitKeys(keyList = ['1', '2', '3','escape'],maxWait=2.5)
if keys:
resp = keys[0]
rt=respClock.getTime()
if resp=='escape':
trials.finished = True
elif target.color== colours[0] and resp=='1':
corr = 1
elif target.color== colours[1] and resp=='2':
corr = 1
elif target.color== colours[2] and resp=='3':
corr = 1
#if incorrect response is given, feedback is displayed
else:
corr = 0
background.draw()
feedback.draw()
win.flip()
core.wait(1.5)
background.draw()
win.flip()
core.wait(0.1)
# Start of Experiment trials 2
instructionsClock = core.Clock()
initialText20 = visual.TextStim(win=win, name='initialText20',
text=u'That was the end of the practice\nReady for the experiment?\nPress Space to start',
pos=(0, 50), height=40, wrapWidth=2000, ori=0, alignHoriz='center',
color='white', colorSpace='rgb', opacity=1, font = 'Arial',
bold = True, depth=0.0);
initialText21 = visual.TextStim(win=win, name='initialText2',
text='Remember:\n 1 - '+str(colours[0])+' 2 - '+str(colours[1])+' 3 - '+str(colours[2])+'\n\nRespond as quickly and accurately as you can',
pos=(0, -250), height=50, wrapWidth=2000, ori=0, alignHoriz='center',
color='white', colorSpace='rgb', opacity=1, font = 'Arial',
bold = True, depth=0.0);
initialText20.draw()
initialText21.draw()
win.flip()
keys = event.waitKeys(keyList = ['space']) #waits for spacebar to start
# ################# Sequence at each trial #################
# ##########################################################
# ##########################################################
for currentTrial in range(trialsPerContingency):
currentCond = np.random.choice(condition,p=[.5,.5])
target.color = random.choice(colours)
target.pos = (0,0)
#If Congruent condition, target colour and word is the same
if currentCond == condition[0]:
target.text = target.color
#If Neutral condition, sample word from neutralWords at random
else:
if contingency[1] == 'notControlled':
target.text= np.random.choice(neutralWords)
else:
targetWordIndex = colours.index(target.color) - 1
target.text=neutralWords[targetWordIndex]
# ################# Trial counter #################
# Counts number of trials and resets it when it's a new block.
trialIndex=trialIndex + 1
if trialIndex > trialsPerBlock :
trialIndex = 1
#displays fixation cross. Duration is jittered
fixation.draw()
win.flip()
core.wait(random.uniform(0.4,0.6))
#displays stroop stimuli
target.draw()
win.flip()
respClock.reset()
#waits for a valid response for a maximum of 2.5 seconds
#records responses and calculates RT and accuracy
#exits program if 'escape' key is pressed
keys = event.waitKeys(keyList = ['1', '2', '3','escape'],maxWait=2.5)
if keys:
resp = keys[0]
rt=respClock.getTime()
if resp=='escape':
trials.finished = True
elif target.color== colours[0] and resp=='1':
corr = 1
elif target.color== colours[1] and resp=='2':
corr = 1
elif target.color== colours[2] and resp=='3':
corr = 1
#if incorrect response is given, feedback is displayed
else:
corr = 0
background.draw()
feedback.draw()
win.flip()
core.wait(1.5)
background.draw()
win.flip()
core.wait(0.1)
#if no response is given
else:
resp,rt,corr = 'n/a',-1,0
#performs a break after a block and counts the block number
if trialIndex > trialsPerBlock-1:
blockNumber = blockNumber + 1
# Initialize final screen
text_1 = visual.TextStim(win=win, name='text_1',
text=u'take a short break',
font=u'Arial',
pos=(0,150), height=50, wrapWidth=None, ori=0,
color=u'white', colorSpace='rgb', opacity=1,
depth=0.0);
initialText3 = visual.TextStim(win=win, name='initialText3',
text=u'Press Space to start block '+str(blockNumber)+' of '+str(totalBlocks),
pos=(0, 50), height=40, wrapWidth=2000, ori=0,
color='white', colorSpace='rgb', opacity=1, font = 'Arial',
bold = True, depth=0.0);
endScreen = visual.TextStim(win=win, name='endScreen',
text=u"That's the end of the experiment\nThank you for participating",
pos=(0, 50), height=40, wrapWidth=2000, ori=0,
color='white', colorSpace='rgb', opacity=1, font = 'Arial',
bold = True, depth=0.0);
if blockNumber > totalBlocks:
endScreen.draw()
else:
text_1.draw()
win.flip()
core.wait(2)
text_1.draw()
initialText3.draw()
initialText21.draw()
win.flip()
keys = event.waitKeys(keyList = ['space'])
# #### writes data to results file
trials.addData('contingency',contingency[1])
trials.addData('trialIndex', trialIndex)
trials.addData('word',target.text)
trials.addData('colour',target.color)
trials.addData('resp', resp)
trials.addData('rt', rt)
trials.addData('corr', corr)
trials.addData('condition',currentCond)
thisExp.nextEntry()
I usually make multiple versions of my Stroop experiments with different colours of stimuli to control for the possibility of the set of colours used having some unexpected effect on performance (e.g., maybe the colours are mostly warm/cool). This also means I have to personally test run every single version, which isn't fun for me. So instead, I now get the program to randomly sample the colours used in each session from a larger pool of colours, and also randomly assign the response buttons that each colour gets assigned to.
This example is for an experiment with 3 response buttons, where the three colours used is taken from a pool of 6 colours.
To do this I first defined a colourLibrary list which contains all the possible colours.
colourLibrary = ['red','green','yellow','blue','pink','white']
The position of the items on the list is then randomised using the random.shuffle function.
random.shuffle(colourLibrary)
A second list (which I named colours)is then created, which is made up of the first three items from the shuffled colourLibrary list. The items in colours will then be used in the same way as the previous example where each response option references an item from the colours list
colours = colourLibrary[0:3]
Technically you can just randomly select any 3 items from colourLibrary instead of shuffling and selecting the first three. The reason I did this instead is because my actual experiment has a second part that uses the remaining three colours, and it would be a simple case of selecting the last three items (i.e. colourLibrary [3:6]).
Remember to reflect the colours and the buttons they are assigned to in the instructions before the trials start! The experimenter won't know what they are.
This can be useful if you don't want to mess around creating a spreadsheet of trials. The stimuli will be completely random, instead of having the same exact trials for all participants.
This is a simple experiment that only contains the essential elements to run. It does not have instructions or feedback screens. Coded in PsychoPy 2, but tested and works in 3.0.6.
Experiment details:
Three colours are defined in a list called colour
At every trial, one of the items in colour is randomly selected as the colour of the stimuli. This is done again to determine the text of the stimuli.
An additional feature is that when picking the stimuli colour, the probability of sampling the items in colour is not equal
Target is then displayed and responses recorded, RT calculated, and accuracy coded, before writing information to output file.
#initialising functions used in most experiments
from __future__ import division
from psychopy import visual, core, data, event, gui
import numpy as np
import random
win = visual.Window([1024,768], fullscr=True, units="pix")
respClock = core.Clock()
#defining the colours used in this Stroop task
#colours double up as word stimuli as they are also strings
colours = ['red','green','blue']
#setting up the session information (date and time) to be recorded in the data file
info = {}
info['dateStr'] = data.getDateStr()
filename = "data/" + "_" + info ['dateStr']
#setting up how the fixation cross and target stimuli will look
fixation = visual.TextStim(win,text = '+',
bold = True,
height = 80)
target = visual.TextStim(win,text = 'text',
bold = False,
height = 50)
#setting up TrialHandler and ExperimentHandler which is what makes running experiments in
#psychopy work. It creates an empty array (trialList), which will be populated by the details
#of each trial as experiment goes along. Number of trials in the session (nReps) is also defined.
trials = data.TrialHandler(trialList=[], nReps=10)
thisExp = data.ExperimentHandler(
extraInfo = info, #the info we created earlier
dataFileName = filename, # using our string with data/name_date
)
thisExp.addLoop(trials)
#defining what happens during each trial
#Stroop stimuli colour and word are sampled from the 'colours' list defined earlier
#each item has a different probability of being sampled as the stimuli colour,
#but an equal probability of being sampled as the stimuli word
for currentTrial in trials:
target.color = np.random.choice(colours,p=[0.5,.25,.25])
target.text = random.choice(colours)
#draws fixation cross in the background before 'flipping' it to be displayed for 0.5 seconds
fixation.draw()
win.flip()
core.wait(0.5)
#draws target stimuli before 'flipping' it. No time limit
target.draw()
win.flip()
#resets the clock to be 0 at the start of each trial
respClock.reset()
keys = event.waitKeys(keyList = ['1', '2', '3'],maxWait=2)
if keys:
resp = keys[0]
rt=respClock.getTime()
if resp=='escape':
trials.finished = True #if escape key is pressed, terminate experiment
elif target.color=='red' and resp=='1':
corr = 1
elif target.color=='green' and resp=='2':
corr = 1
elif target.color=='blue' and resp=='3':
corr = 1
elif resp=='escape':
trials.finished = True
else:
corr = 0
else:
resp,rt,corr = 'n/a',-1,0 #this can occur if no response detected within 2 seconds
#at the end of the trial, the response, RT and accuracy are recorded
trials.addData('resp', resp)
trials.addData('rt', rt)
trials.addData('corr', corr)
thisExp.nextEntry()