Sample Psychopy Experiment

From Lupyan Lab
Jump to: navigation, search


Here is a sample study in the format that we normally use in the Lupyan Lab. Most experiments depend on two scripts. The first generates the trial list. The second actually runs the study. You can download both scripts and the stimuli to run the experiment here.

Contents

Demo

Below is a demo of the experiment we are making. The video can be downloaded here.

Generate Trial List

Helper Functions

For starters, we define some functions that will help us make the trial list.

#!/usr/bin/env python
"""
Makes a trial list given a subject code and a seed for the random generator.
"""
 
import random
 
def randomButNot(arr, index):
    """
    Function to select a random item of the array not equal to arr[index]
    """
	randIndex=index
	while randIndex == index:
		randIndex = random.randint(0, len(arr)-1)
	return arr[randIndex]
 
def circularList(lst,seed):
    """
    Makes an endless list.
    """
	if not isinstance(lst,list):
		lst = range(lst)
	i = 0
	random.seed(seed)
	while True:
		yield lst[i]
		if (i+1) % len(lst) ==0:
			random.shuffle(lst)
		i = (i + 1)%len(lst)

Define Experiment-Specific Variables

Each generateTrial script is specific for each experiment. This script generates a trial list for a simple sound picture verification task in which participants hear a sound file through the headphones and then see a picture on the screen. Their job is to answer yes or no: does the sound match the picture.

# define categories of pictures
pics = ['baby','bee','bird','cat', 'chainsaw', 'dog','keyboard','scissors'] 
 
# identify categories as animate v. inanimate
isAnimate = ['1','1','1','1','0','1','0','0'] # note len(isAnimate) == len(pics)
 
# sound cues are the same categories as pics
cues = list(pics)
 
# Possible locations for the sound and the pictures
soundPicLocations = [
					('left','left'),
					('right','right'),
					('left','right'),
					('right','left')
					]
 
#that's pretty tiresome to type. We don't need to permute by hand. Computers! Python! The list above can be generated using the itertools function product (import itertools first)
#soundPicLocations = list(itertools.product(['left','right'],['left','right']))
 
primeTypes = ["label","sound"] # types of sounds
picTypes = ["Silent","Sound"]  # types of pictures
picNum = ['1','2']             # two pictures of each categories
numIter = 2                    # how many times to repeat each unique trial

Generate Unique Trial Types

The main function in the generateTrials script creates all the unique trial types. Any ratios (e.g., proportion of valid to invalid trials) need to be defined here.

separator = ","
def	main(subjCode,seed):
    """
    Makes, shuffles, and writes a trial list to file.
 
    Subject code is used to write the file.
    Seed is used for pseudorandomization.
    """
 
    testFile = open('trialList_test_'+subjCode+ '.csv','w')
 
    # print column headers to the file
    print >> testFile, separator.join(("picType","primeType","picCategory", \
        "picFile","soundCategory","soundFile","soundLocation","picLocation",\
        "isLocationCongruent","isMatch"))
 
    random.seed(seed)
    seed = int(seed)	
    isMatch = circularList([1,1,1,0],seed) # ratio of match to mismatch is 3:1
    trialList =[]
    for curIter in range(numIter):
        for curSoundPicLocation in soundPicLocations:
	    for curPicType in picTypes:
	        for curPrimeType in primeTypes:
		    for curPic in pics:
                        for curPicNum in picNum:
			    curIsMatch = isMatch.next()
                            # if it's a match trial, the sound and the picture
                            # are from the same category
			    if curIsMatch==1:
			        curSound = curPic
				if curSoundPicLocation[0]=='center':
				    curSoundLocation = ''
				else:
				    curSoundLocation = '_'+curSoundPicLocation[0] 
				    curSoundFile = curPic+"_"+str(curPrimeType)+curSoundLocation
				    curPicFile = curPicType+"_"+curPic+curPicNum
 
                            # if it's a mismatch trial, the sound and the pic
                            # are from different categories
			    elif curIsMatch==0:
			        curSound = randomButNot(pics, pics.index(curPic))
				curSoundFile = curSound+"_"+str(curPrimeType)+curSoundLocation
				curPicFile = curPicType+"_"+curPic+curPicNum
 
                            # make sure to append the variables to the trial list in the same order as the trial list header
                            trialList.append(separator.join((curPicType, curPrimeType, curPic, \
                                curPicFile, curSound,curSoundFile, curSoundPicLocation[0], curSoundPicLocation[1],\
                                str(int(curSoundPicLocation[0]==curSoundPicLocation[1])),str(curIsMatch))))
 
    # shuffle all the trials and print
    # seeding the random number generator ensures we can get the same randomization again if we want
    random.shuffle(trialList)
	for curTrialList in trialList:
		print >>testFile, curTrialList
 
if __name__ == "__main__":
    """
    Running the file as a script (instead of importing it) creates a sample file
    """
    trialList = main('testTrials-15',15)

Main experiment script

The main experiment uses Psychopy for stimulus presentation and response recording. The experiment setup happens in two parts: getting the session variables, and loading all the materials for the experiment. The bulk of the experiment is simply looping through the trial list one row/trial at a time.

Get Session Variables

#!/usr/bin/env python
import time
from baseDefsPsychoPy import * # custom LupyanLab functions 
from stimPresPsychoPy import *  # custom LupyanLab functions
import generateTrials
 
class Exp:
    def __init__(self):
 
        #this is where the subject variables go.  'any' means any value is
        # allowed as long as it's the correct type (str, int, etc.) the numbers
        # 1 and 2 control the order in which the prompts are displayed (dicts
        # have no natural order)
 
        self.optionList = { '1':  {   'name' : 'subjCode', 
                                    'prompt' : 'Subject Code: ', 
                                    'options': 'any', 
                                   'default' : 'systemA_101',
                                      'type' : str}, 
                            '2' : {   'name' : 'gender', 
                                    'prompt' : 'Subject Gender m/f: ', 
                                   'options' : ("m","f"),
                                    'default':'',
                                      'type' : str},
                            '3' : {   'name' : 'responseDevice', 
                                    'prompt' : 'Response device: keyboard/gamepad: ', 
                                   'options' : ("keyboard","gamepad"),
                                   'default' : 'gamepad',
                                      'type' : str},
                            '4' : {   'name' : 'whichValid', 
                                    'prompt' : 'whichKeyValid "up" or "down": ', 
                                   'options' : ('up','down'),
                                    'default':'',
                                      'type' : str},
                            '5' : {   'name' : 'useFeedback', 
                                    'prompt' : 'Use Feedback? y/n ', 
                                   'options' : ("y","n"), 
                                   'default' : 'y',
                                      'type' : str},
                            '6' : {   'name' : 'seed', 
                                    'prompt' : 'Enter seed: ', 
                                   'options' : 'any', 
                                   'default' : 100,
                                      'type' : int},
                            '8' : {   'name' : 'expInitials', 
                                    'prompt' : 'Experiment Initials: ', 
                                   'options' : 'any', 
                                   'default' : '', 
                                      'type' : str}
                          }
 
        optionsReceived=False
        fileOpened=False
        while not optionsReceived or not fileOpened:
            # enterSubjInfo is a Lupyan-Lab specific wrapper for a psychopy.gui
            [optionsReceived,self.subjVariables] = enterSubjInfo('picWordVerify-systematicity',self.optionList)
            if not optionsReceived:
                popupError(self.subjVariables)
            try:
                if  os.path.isfile(self.subjVariables['subjCode']+'_test.txt'):
                    fileOpened=False
                    popupError('Error: That subject code already exists')
                else:
                    self.outputFileTest     = file(self.subjVariables['subjCode']+'_test.txt','w')
                    fileOpened=True
            except:
                pass
 
        # generate the trials, which writes out to a .csv
        generateTrials.main(self.subjVariables['subjCode'],self.subjVariables['seed'])
 
        if self.subjVariables['responseDevice']=='gamepad':
            try:
                self.stick=initGamepad()
                pygame.init()
                self.inputDevice = "gamepad"
                responseInfo = " Press the Green button for 'Yes' and the Red button for 'No'."
                self.validResponses = {'0':3,'1':0}
            except SystemExit:
                self.subjVariables['responseDevice']='keyboard'
                print "No joystick; using keyboard"
                self.inputDevice = "keyboard"
                if self.subjVariables['whichValid']=='up':
                    self.validResponses = {'1':'up','0':'down'} #change n/o to whatever keys you want to use
                    responseInfo = " Press the 'up arrow' for 'Yes' and the 'down arrow' for 'No'."
                else:
                    self.validResponses = {'0':'up','1':'down'} #change n/o to whatever keys you want to use
                    responseInfo = " Press the 'down arrow' for 'Yes' and the 'up arrow' for 'No'."
        else:
            print "Using keyboard"
            self.inputDevice = "keyboard"
            if self.subjVariables['whichValid']=='up':
                self.validResponses = {'1':'up','0':'down'} #change n/o to whatever keys you want to use
                responseInfo = " Press the 'up arrow' for 'Yes' and the 'down arrow' for 'No'."
            else:
                self.validResponses = {'0':'up','1':'down'} #change n/o to whatever keys you want to use
                responseInfo = " Press the 'down arrow' for 'Yes' and the 'up arrow' for 'No'."
 
        try:
            self.win = visual.Window(fullscr=True, color=[.6,.6,.6], allowGUI=False, monitor='testMonitor',units='pix',winType='pyglet')
        except:
            self.win = visual.Window([1024,768], color=[.6,.6,.6], allowGUI=False, monitor='testMonitor',units='pix',winType='pyglet')
 
        self.preFixationDelay  =     0.750
        self.postFixationDelay  =     0.500
 
        self.stimPositions = {'center':(0,0), 'left':(-570,0),'right':(570,0)}
        self.numPracticeTrials = 4
        self.takeBreakEveryXTrials = 100; 
        self.finalText              = "You've come to the end of the experiment.  Thank you for participating."
        self.instructions        = \
        """Thank you for participating \nIn this experiment you will hear various words or sounds (e.g., the word "cat" or a sound of a cat meowing) and pictures of various animals/objects. \n\nMost of the time the word/sound will match the picture, for example you'll hear 
        a meowing sound and see a cat. Other times, the picture will not match the sound/word. Your task is to decide as quickly as possible if the word/sound matches the picture or not. For example, if you see a cat and then hear "cat" you should press the button for "yes". \nIf you make a mistake, you will hear a buzzing sound. If you are making too many mistakes, you are probably rushing. 
        During the experiment there will be some breaks during which you can rest your eyes. \nWe will begin with some practice trials.\n\nPlease let the experimenter know if you have any questions. 
        """
        self.instructions+=responseInfo
 
        self.takeBreak = "Please take a short break.  Press 'Enter' when you are ready to continue"
        self.practiceTrials = "The next part is practice"
        self.realTrials = "Now for the real trials"

Load Stimuli

After we get the subject info and make the trials file, we can initialize the experiment by loading all the stimuli into Psychopy objects.

class trial(Exp):
	def __init__(self):
		firstStim=''
 
class ExpPresentation(trial):
	def __init__(self,experiment):
		self.experiment = experiment
 
	def initializeExperiment(self):	
		# Experiment Clocks
		self.expTimer = core.Clock()
		"""This loads all the stimili and initializes the trial sequence"""
		self.fixSpot = visual.TextStim(self.experiment.win,text="+",height = 20,color="black")
		self.centerRectOuter = newRect(self.experiment.win,size=(260,260),pos=(0,0),color=(.5,.5,.5))
		self.centerRectInner = newRect(self.experiment.win,size=(254,254),pos=(0,0),color=(1,1,1))
		showText(self.experiment.win, "Loading Images...",color="black",waitForKey=False)
		self.pictureMatrix = loadFiles('stimuli\Pictures','jpg','image',self.experiment.win)
		self.soundMatrix = loadFiles('stimuli\Sounds','wav','winSound')
		(self.trialListMatrix,self.fieldNames) = importTrials('trialList_test_'+self.experiment.subjVariables["subjCode"]+'.csv',method="sequential")
		self.stim = visual.PatchStim(self.experiment.win,mask="none",tex="none")
	def checkExit(self): #I don't think this works if gamepad is in use
		if event.getKeys()==['equal','equal']:
			sys.exit("Exiting experiment")

Trial Procedure

Behavior on each trial is defined by a single function called "presentTestTrial". It's main argument is a dict-like object that corresponds to a row from the trial list.

def presentTestTrial(self,whichPart,curTrial,curTrialIndex):
 
    self.checkExit() #check for exit press the equals key twice.
    self.experiment.win.flip()
    core.wait(self.experiment.preFixationDelay) 
    #setAndpresentStimulus(self.experiment.win,[self.fixSpot]) #show fixation cross
    core.wait(self.experiment.postFixationDelay)
 
    playAndWait(self.soundMatrix[curTrial['soundFile']],soundPath=self.soundMatrix[curTrial['soundFile']],winSound=True)
    core.wait(.8)
    print 'soundLocation is',curTrial['soundLocation'],'picLocation is',curTrial['picLocation']
 
    self.pictureMatrix[curTrial['picFile']][0].setPos(self.experiment.stimPositions[curTrial['picLocation']])
    setAndPresentStimulus(self.experiment.win,[self.pictureMatrix[curTrial['picFile']][0]])
 
    correctResp = self.experiment.validResponses[str(curTrial['isMatch'])]
    if self.experiment.inputDevice=='keyboard':
        (response,rt) = getKeyboardResponse(self.experiment.validResponses.values())
    elif self.experiment.inputDevice=='gamepad':
        (response,rt) = getGamepadResponse(self.experiment.stick,self.experiment.validResponses.values())
 
    #self.soundMatrix[curTrial['soundFile']].stop()
    if response==correctResp:
        isRight=1
        if self.experiment.subjVariables['useFeedback']=='y':
            playAndWait(self.soundMatrix['bleep'],winSound=True)
    else:
        isRight=0
        if self.experiment.subjVariables['useFeedback']=='y':
            playAndWait(self.soundMatrix['buzz'],winSound=True)
 
    self.experiment.win.flip()
    fieldVars=[]
    for curField in self.fieldNames:
        fieldVars.append(curTrial[curField])
    curLine = createResp(self.experiment.optionList,self.experiment.subjVariables,fieldVars,
    a_whichPart = whichPart,
    b_curTrialIndex = curTrialIndex,
    c_expTimer = self.expTimer.getTime(),
    d_isRight = isRight,
    e_rt = rt*1000)
    writeToFile(self.experiment.outputFileTest,curLine)

Cycle Through Experiment

The rest of the experiment is simply looping through the rows of the trial list and presenting each trial. Here we cycle through the experiment and behave differently if it is a practice trial or a test trial.

 
def cycleThroughExperimentTrials(self,whichPart):
    if whichPart == "practice":
        trialIndices = random.sample(range(1,50),self.experiment.numPracticeTrials)
        curTrialIndex=0
        for curPracticeTrial in trialIndices:
            self.presentTestTrial(whichPart,self.trialListMatrix.getFutureTrial(curPracticeTrial),curTrialIndex)
    else:
        curTrialIndex=0
        for curTrial in self.trialListMatrix:
            self.checkExit()
            if curTrialIndex>0 and curTrialIndex % self.experiment.takeBreakEveryXTrials == 0:
                showText(self.experiment.win,self.experiment.takeBreak,color=(0,0,0),inputDevice=self.experiment.inputDevice) #take a break
            self.presentTestTrial(whichPart,curTrial,curTrialIndex)
            curTrialIndex+=1
        #close test file
        self.experiment.outputFileTest.close()

Run Experiment

The bottom of the script outlines the order of events for the experiment. When the experiment object is created, a GUI pops up to get the session info. Then the trials are made, stimuli are loaded, and the devices are created. The instructions are shown using a Lupyan Lab specific function called "showText" which is a wrapper for making an on-the-fly psychopy.visual.TextStim object and collecting input from either the keyboard, mouse, or gamepad. Then the practice trials are run, followed by the test trials.

currentExp = Exp()
currentPresentation = ExpPresentation(currentExp)
currentPresentation.initializeExperiment()
showText(currentExp.win,currentExp.instructions,color=(-1,-1,-1),inputDevice=currentExp.inputDevice) #show the instructions for test
showText(currentExp.win,currentExp.practiceTrials,color=(-1,-1,-1),inputDevice=currentExp.inputDevice)
currentPresentation.cycleThroughExperimentTrials("practice")
showText(currentExp.win,currentExp.realTrials,color=(0,0,0),inputDevice=currentExp.inputDevice)
currentPresentation.cycleThroughExperimentTrials("test")
showText(currentExp.win,currentExp.finalText,color=(-1,-1,-1),inputDevice=currentExp.inputDevice) #thank the subject
Personal tools
Namespaces
Variants
Actions
Navigation
HowTos
Troubleshooting
Scripts
Toolbox