Coding a full experiment§

In this session we’ll create an experiment from scratch using only Python code.

1

The Posner Cuing task§

Synopsis of the study:

2

How do we even start?§

Don’t be tempted to try and write a script from beginning to end in one go! Break it down into chunks that you can manage. e.g.:

3

Create a window§

First we need to import the necessary libraries. For an experiment we nearly always need to import the visual, event, data and core modules from PsychoPy:

from psychopy import visual, event, core, data

then creating a window is another single line. We’ll use units of pixels for the window for simplicity. Then all our stimulus dimensions will be in pixels:

win = visual.Window([1024,768], fullscr=False, units='pix')

Save your experiment and run it to make sure a window flashes up.

4

Our trial starts with a fixation§

Immediately after creating our window we usually initialise objects like stimuli and clocks:

#initialise some stimuli
fixation = visual.Circle(win, size = 5,
    lineColor = 'white', fillColor = 'lightGrey')

Later (on each trial) we’ll need to draw the fixation point and then flip the screen so that the drawing becomes visible:

#run one trial
fixation.draw()
win.flip()
5

Create your probe stimulus§

Just for variety, let’s create a Gaussian spot for the probe. You need this code where your stimuli are being initialised (doesn’t matter if it’s before or after probe):

probe = visual.GratingStim(win, size = 80, # 'size' is 3xSD for gauss,
    pos = [300, 0], #we'll change this later
    tex = None, mask = 'gauss',
    color = 'green')

After drawing the fixation point and flipping, we need to do the same for the probe:

probe.draw()
win.flip()
6

We also need a cue§

We could use some image of an arrow for this. Or we could create some shape of our own with custom vertices:

cue = visual.ShapeStim(win,
    vertices = [[-30,-20], [-30,20], [30,0]],
    lineColor = 'red', fillColor = 'salmon')

Also add draw() code like the other objects. Again, it doesn’t matter the order we initialise it, but the drawing needs to be between the fixation and the probe.

7

Understanding Window.flip()§

8

Understanding Window.flip()§

This has various knock-on effects:

9

Set some timing parameters§

If you run now the objects will be presented for a single frame each (1/60th of sec). That’s too short for us to see. We need to set times for our objects. we can achieve that with the core.wait() function.

Possible: “hard code” the values by typing them where needed.

Better: store them as variables at the top of the script

Even better: store them in a dictionary that we can save easily in the data files:

info = {} #a dictionary
info['fixTime'] = 0.5 # seconds
info['cueTime'] = 0.2
info['probeTime'] = 0.2
10

Pause after flipping the window for each object§

Add a line to wait after each flip of the window:

# run one trial
fixation.draw()
win.flip()
core.wait(info['fixTime'])

cue.draw()
win.flip()
core.wait(info['cueTime'])

probe.draw()
win.flip()
core.wait(info['probeTime'])

This is not actually a very precise way to control timing, but it’s very easy!

11

Drawing two objects at the same time§

If you draw() two stimuli before a win.flip() then they both appear on the same frame. For the probe presentation let’s have the fixation as well:

fixation.draw()
probe.draw()
win.flip()
core.wait(info['probeTime'])

If the stimuli overlap in space then the later draw() will occlude the earlier one. You can also set the degree of opacity of stimuli so that they are partially visible.

12

Let’s run two trials§

We could copy and paste the trial code to run repeated trials.

Try doing that so that you get two repeats of the trial.

That’s very inefficient though, so undo it.

Exercise: Why not create a loop to run as many trials as you like? That would be more efficient. Add a for… loop and indent your trial code so that it is ‘contained’ in the loop. Set the loop to run for 5 ‘repeats’.

13

Let’s run two trials§

Solution:

for trial in range(5):
    fixation.draw()
    win.flip()
    core.wait(info['fixTime'])

    cue.draw()
    win.flip()
    core.wait(info['cueTime'])

    probe.draw()
    win.flip()
    info['probeTime']
14

TrialHandler§

This allows you to run multiple trials of different conditions in various ways (random or sequential etc.). It lives in the PsychoPy’s data module, which we already imported.

To repeat our trials using the TrialHandler instead of the basic for loop we can do this:

trials = data.TrialHandler(trialList=[], nReps=5)
for thisTrial in trials:
    #code to run one trial here

For now we’ve set the trialList simply to an empty list, but later we’ll change that.

The code above needs to come somewhere after you initialise your stimuli and it needs to include your trial code

15

Controlling conditions§

We need the stimuli to differ on each trial, which TrialHandler can also help us with. It expects to receive conditions (aka trialTypes) as a list of dictionaries, where one dictionary specifies the parameters for one condition. We could write that by code using a for…loop, but it might be easier this time to use a spreadsheet.

You could have achieved exactly the same as this using code to create a list of dictionaries with one dictionary for each type of trial in your conditions.

16

Create a conditions file§

We can import conditions from either .xlsx or .csv files.

Create a file with:

17

Create a conditions file§

For the Posner task we need control of:

For analysis it’s handy also to store:

18

Create a conditions file§

So we might have a sheet like this:

cueOri probeX valid descr
0 300 1 right
180 -300 1 left
0 300 1 right
180 -300 1 left
0 300 1 right
180 -300 1 left
0 300 1 right
180 -300 1 left
180 300 0 conflict
0 -300 0 conflict

Save the file in xlsx or csv format. e.g. “conditions.csv”

19

Import that file and put it to use§

The data module in PsychoPy has a function to import such files. It gives a list of dicts that can be used directly in the TrialHandler:

conditions = data.importConditions('conditions.csv')
trials = data.TrialHandler(trialList=conditions, nReps=5)
for thisTrial in trials:
    #code to run one trial here
    ...

This will run 5 repeats of our 10 trial types randomly. The way we’ve set this up we’ll get 50 trials with 80% valid probes.

20

Updating stimuli§

Each time through the loop the value thisTrial is a dictionary for one trial, with keys that have the column names:

for thisTrial in trials:
    #code to run one trial here
    probe.setPos( [thisTrial['probeX'], 0] )
    cue.setOri( thisTrial['cueOri'] )

You can see the code changes here through looking at the version history on the gitlab project page.

21

Collect responses§

Now let’s get a key-press after each trial and measure the reaction time (RT).

Before starting our trials we could create a clock/timer to measure response times:

respClock = core.Clock()

Then when we present our stimulus we could reset that clock to zero:

fixation.draw()
probe.draw()
win.flip()
respClock.reset()
...
22

Collect responses§

After our stimulus has finished we should flip the screen (without doing any drawing so it will be blank) and then wait for a response to occur:

#clear screen
win.flip()
#wait for response
keys = event.waitKeys(keyList = ['left','right','escape'])
resp = keys[0] #take first response
rt = respClock.getTime()
23

Collect responses§

Check if that response was correct:

if thisTrial['probeX']>0 and resp=='right':
    corr = 1
elif thisTrial['probeX']<0 and resp=='left':
    corr = 1
else:
    corr = 0
24

Collect responses§

And store the responses in the TrialHandler:

trials.addData('resp', resp)
trials.addData('rt', rt)
trials.addData('corr', corr)

(Note that we aren’t saving the data file yet though!)

25

Using the ExperimentHandler§

For today the ExperimentHandler isn’t strictly needed, but it allows some nice things so we’ll use it:

26

Using the ExperimentHandler§

All we need to do is:

27

Create a base filename§

Let’s create a filename using the participant name and the date. OK, so we’ll need to get those!

For the username, we can easily create a dialog box that uses our info dictionary to store information (top of our script):

info = {} #a dictionary
#present dialog to collect info
info['participant'] = ''
dlg = gui.DlgFromDict(info) #(and from psychopy import gui at top of script)
if not dlg.OK:
    core.quit()
#add additional info after the dialog has gone
info['fixTime'] = 0.5 # seconds
info['cueTime'] = 0.2
info['probeTime'] = 0.2
info['dateStr'] = data.getDateStr() #will create str of current date/time
28

Create a base filename§

Now we’ve collected the information there are various ways to create our filename string. All of these achieve the same thing, e.g. data/jwp_2014_Apr_13_1406

filename = "data/" + info['participant'] + "_" + info['dateStr']
filename = "data/%s_%s"%(info['participant'], info['dateStr'])
filename = "data/{0}_{1}".format(info['participant'], info['dateStr'])
filename = "data/{0['participant']}_{0['dateStr']}".format(info)
filename = "data/{participant}_{dateStr}".format(**info)

You can see them looking increasingly obscure, but increasingly brief.

29

Create ExperimentHandler§

After your code to create the TrialHandler loop:

#add trials to the experiment handler to store data
thisExp = data.ExperimentHandler(
        name='Posner', version='1.0', #not needed, just handy
        extraInfo = info, #the info we created earlier
        dataFileName = filename, # using our string with data/name_date
        )
thisExp.addLoop(trials) #there could be other loops (like practice loop)

AND at the end of the response collection we need to inform the experiment handler that it’s time to consider the trial complete:

...
trials.addData('rt', rt)
trials.addData('corr', corr)
thisExp.nextEntry()
30

We can’t quit during a run§

Let’s make it possible to end the experiment during a run using the ‘escape’ key

Where you checked your responses we need to add something to handle that:

elif resp=='escape':
    trials.finished = True

Alternatives to trials.finished=True

break #will end the innermost loop, not necessarily `trials`
core.quit() #from psychopy lib will exit Python

NB: If you hit the red stop button in PsychoPy it issues a very severe abort and no data will be saved!

31

Exercise§

In code:

  1. Add instructions, participants must press a key to start.
  2. Add some feedback text for response time.
  3. Make this feedback red if slow and green if fast.
32

All done!§

If I push these changes to pavlovia, you can see the changes we make to the task throughout task creation…

33

Improvements§

There are a few problems with this version, that we could definitely improve on. Currently:

34

Summary§

Hopefully you’ve learned how to: - create and present stimuli - set timings - receive responses from a keyboard - save data in various formats

35

Summary§

What next? Improvements

36