Hi everyone! This is the ultra-condensed highlights from my three-day PsychoPy course. You can see the full one here. Here are some materials for the workshop. As a preparation for the whole course:
- Make sure to take a look at the “Getting Started” page in the “PsychoPy Course” menu above. I have been using PsychoPy 3.0.11b and Spyder4 (beta) on my computer.
- I may be using the ppc module which I wrote some years ago. It’s useful helpers to complement psychopy. Right click to download.
- In general, skim over the The PsychoPy API to get a sense of where you can turn to to get more information and help. It documents almost all the methods and classes available to you.
First webinar: stimuli and precision
Here are some useful things to look into for the first webinar:
- Slides from the first seminar.
- For this session, we’ll use just two images: a smooth and a pointy one. Right-click these links to download them. They are white images, so don’t worry if you can’t see them on a white background! If you’re interested, here are a larger stimulus set, controlled for area, width, and height:
Here’s the code from the first seminar:
# -*- coding: utf-8 -*-
"""
TODAY's PLAN:
* Present some stimuli: Window, ShapeStim, ImageStim
* Present sounds
* Manipulate stimuli
* Control appearance: DKL color, size in degrees
* Synchronize stimuli and collect a response
To change audiolib, change the order of the available libraries:
Files --> Preferences
audioLib: 'pysoundcard', 'pyo', 'portaudio'
Change e.g. to
audioLib: 'pysoundcard', 'pyo', 'portaudio'
"""
# Import the ppc module
import ppc # Looks for the file or folder "ppc"
print(ppc.deg2cm(2, 90)) # Measure this on your screen to confirm
# Set up some stimuli
from psychopy import visual, sound, event
win = visual.Window(monitor='testMonitor', units='deg') # Makes a Window
instruction = visual.TextStim(win, text=u'Hello World æø Å', color='red', height=1)
circle = visual.Circle(win, pos=(0, -4), radius=2)
bouba = visual.ImageStim(win, image='smooth_simple.png')
beep = sound.Sound(1200, secs=0.120)
# Show it
instruction.draw()
circle.draw()
win.flip()
event.waitKeys(keyList=['escape', 'return']) # Waits for a keyresponse
# Change attributes
circle.pos = (0, 4)
circle.fillColor = 'blue'
instruction.text = u'Hi again.'
# Show updated stimuli
circle.draw()
instruction.draw()
win.flip()
event.waitKeys()
# Show coloured bouba image
bouba.colorSpace = 'rgb255'
bouba.color = (255, 128, 50) # RGB (red, green, blue)
bouba.draw()
win.flip()
beep.play() # Notice: immediately after the beep!
event.waitKeys()
# Animate kiki
bouba.image = 'pointy_simple.png' # Load new image
for frame in range(120):
bouba.ori += 1
bouba.draw()
win.flip() # This is synchronized to the monitor update
Second webinar: Experiment flow #1
If you have a bit of time to review some materials before the seminar, here are two suggestions:
- You can see my suggestion for a general code layout here: https://github.com/lindeloev/p
sychopy-course/blob/master/pre sentations/ppc3_code_layout. pdf - And here’s a quite minimal experiment (but heavily commented!) which includes most of the features you’d want to use: https://github.com/lindeloev/p
sychopy-course/blob/master/ppc _template.py - It’s always be helpful to look at the demos under PsychoPy Coder –> demos. There you can see worked examples of pretty much everything PsychoPy can do. For example, if you are interested in interfacing with hardware:

This is the code we wrote in the second webinar. Notice that it follows the code layout above.
# VARIABLES
# Stimulus attriutes
FIX_DURATION = 30 # Number of frames
FIX_HEIGHT = 0.2 # Degrees visual angle
STIM_SIZE = 2 # Degrees visual angle
STIM_FREQUENCY = 4 # Gabor patch spatial frequency
INSTRUCTION_HEIGHT = 0.3 # Degrees visual angle
BEEP_DURATION = 0.2 # Seconds
# Conditions
STIM_POSITIONS = [-3, 3] # Degrees visual angle on the y-axis
BEEP_FREQUENCIES = [1000, 1500] # Hz
REPETITIONS = 2 # Number of repetitions of each condition
# Flow
BREAK_INTERVAL = 5 # Number of trials between breaks
KEYS_CONTINUE = ['return', 'space']
KEYS_QUIT = ['escape']
TEXT_BREAK = u'Take a short break if you need to.\nPress RETURN or SPACE to continue...'
TEXT_INTRO = u'Press UP if the visual stimulus is up.\nPress DOWN if it is down'
# Set up some stimuli
import random
from psychopy import visual, sound, event, core
win = visual.Window(monitor='testMonitor', units='deg', fullscr=False)
stim = visual.GratingStim(win, size=STIM_SIZE, sf=STIM_FREQUENCY, mask='gauss')
fix = visual.TextStim(win, text='+', height=FIX_HEIGHT*1.6) # roughly makes the text the correct size
beep = sound.Sound(1000, secs=BEEP_DURATION)
instruction = visual.TextStim(win, height=INSTRUCTION_HEIGHT*1.6)
def make_block():
"""Return a list of trials"""
trials = []
# Run through all combinations of stimulus feature
for pos in STIM_POSITIONS:
# run this code
for frequency in BEEP_FREQUENCIES:
for repetition in range(REPETITIONS):
trial = {
'pos': pos,
'sound': frequency
}
trials.append(trial)
# Randomize order
random.shuffle(trials)
for i, trial in enumerate(trials):
trial['no'] = i
return(trials)
def show_instruction(text):
"""Show an instruction and wait for a response"""
instruction.text = text
instruction.draw()
win.flip()
key = event.waitKeys(keyList=KEYS_CONTINUE + KEYS_QUIT)[0]
if key in KEYS_QUIT:
win.close()
core.quit()
return(key)
def run_block():
# RUN THE TRIAL
# Make a list of trials
trials = make_block()
# Loop through trials
for trial in trials:
# Prepare trial
stim.pos = (0, trial['pos']) # Stimulus up or down
beep.setSound(trial['sound'], secs=BEEP_DURATION)
# Intermittent break
if trial['no'] % BREAK_INTERVAL == 0:
show_instruction(TEXT_BREAK)
# Show fixation cross
for frame in range(FIX_DURATION):
fix.draw()
win.flip()
# Show stimuli
stim.draw()
win.flip()
beep.play()
event.waitKeys()
# EXECUTE EXPERIMENT
show_instruction(TEXT_INTRO)
run_block()
# Quit gracefully
win.close()
Third webinar: Experiment flow #2
We will continue (and hopefully finish!) coding our experiment by adding:
- Collect response and score it
- Save data. Also take a look at the psychopy.data module.
- Dialogue box.
- Fancy code-only stuff: non-random order. Breaks in certain conditions, etc.
- Interface with external hardware – sending/receiving triggers.
As mentioned in the intro to the previous webinar, it’s very helpful to look at the worked code examples for all PsychoPy functionality under PsychoPy Coder –> demos.
We updated the code during the webinar and here it is:
# -*- coding: utf-8 -*-
"""
PLAN (X = finished, * = planned but not finished):
X Show a gabor patch and play a sound
X Add fixation cross
X Make a list of trials: location and pitch
X Add a loop
X Add breaks (code only!)
X Collect response
X Save data
X psychopy.data module and reading from Excel script
X Dialogue box
* Fancy code-only stuff: non-random order. Break in certain conditions, etc.
* UseVersion?
X interface with external hardware - sending/receiving triggers.
* score trial?
We style our code using PEP8
"""
# VARIABLES
# Stimulus attriutes
FIX_DURATION = 30 # Number of frames
FIX_HEIGHT = 0.2 # Degrees visual angle
STIM_SIZE = 2 # Degrees visual angle
STIM_FREQUENCY = 4 # Gabor patch spatial frequency
STIM_DURATION = 9 # 150 ms
INSTRUCTION_HEIGHT = 0.3 # Degrees visual angle
BEEP_DURATION = 0.2 # Seconds
# Conditions
STIM_POSITIONS = [-3, 3] # Degrees visual angle on the y-axis
BEEP_FREQUENCIES = [1000, 1500] # Hz
REPETITIONS = 2 # Number of repetitions of each condition
# Define triggers
TRIGGERS_STIM = {-3: 1, 3: 2}
TRIGGER_OFFSET = 200
# Flow
BREAK_INTERVAL = 5 # Number of trials between breaks
KEYS_CONTINUE = ['return', 'space']
KEYS_QUIT = ['escape']
KEYS_RESPONSE = ['up', 'down']
TEXT_BREAK = u'Take a short break if you need to.\nPress RETURN or SPACE to continue...'
TEXT_INTRO = u'Press UP if the visual stimulus is up.\nPress DOWN if it is down'
# Set up some stimuli
import random
import ppc
from psychopy import visual, sound, event, core, gui, parallel
def send_trigger(trigger):
""" Send a particular trigger on the parallel port"""
parallel.setData(trigger)
core.wait(0.020) # keep the signal on for 20 ms
parallel.setData(0)
# Show dialogue
DIALOGUE = {
'id': '',
'age': '',
'gender': ['male', 'female', 'other'],
'show_fixation': ['yes', 'no']
}
# Updates DIALOGUE with dialogue response
if not gui.DlgFromDict(DIALOGUE).OK:
core.quit()
win = visual.Window(monitor='testMonitor', units='deg', fullscr=False)
stim = visual.GratingStim(win, size=STIM_SIZE, sf=STIM_FREQUENCY, mask='gauss')
fix = visual.TextStim(win, text='+', height=FIX_HEIGHT*1.6) # roughly makes the text the correct size
beep = sound.Sound(1000, secs=BEEP_DURATION)
instruction = visual.TextStim(win, height=INSTRUCTION_HEIGHT*1.6)
clock = core.Clock()
# Make a CSV writer
writer = ppc.csvWriter('subject_data', 'data')
def make_block():
"""Return a list of trials"""
trials = []
# Run through all combinations of stimulus feature
for pos in STIM_POSITIONS:
# run this code
for frequency in BEEP_FREQUENCIES:
for repetition in range(REPETITIONS):
trial = {
'pos': pos,
'sound': frequency,
'trigger': TRIGGERS_STIM[pos]
}
trial.update(DIALOGUE) # Add dialogue data
trials.append(trial)
# Randomize order
random.shuffle(trials)
# Add trial number
for i, trial in enumerate(trials):
trial['no'] = i
return(trials)
def show_instruction(text):
"""Show an instruction and wait for a response"""
instruction.text = text
instruction.draw()
win.flip()
key = event.waitKeys(keyList=KEYS_CONTINUE + KEYS_QUIT)[0]
if key in KEYS_QUIT:
win.close()
core.quit()
return(key)
def run_block():
# RUN THE TRIAL
# Make a list of trials
trials = make_block()
# Loop through trials
for trial in trials:
# Prepare trial
response_given = False
stim.pos = (0, trial['pos']) # Stimulus up or down
beep.setSound(trial['sound'], secs=BEEP_DURATION)
# Intermittent break
if trial['no'] % BREAK_INTERVAL == 0:
show_instruction(TEXT_BREAK)
# Show fixation cross
if DIALOGUE['show_fixation'] == 'yes':
for frame in range(FIX_DURATION):
fix.draw()
win.flip()
# Show stimuli
#win.callOnFlip(beep.play)
#win.callOnFlip(clock.reset)
for frame in range(STIM_DURATION - 1): # Skip one frame due to trigger
stim.draw()
win.flip()
# Play beep on stimulus onset
if frame == 0:
beep.play()
clock.reset() # The t=0 for reaction time
#send_trigger(trial['trigger']) # Send trigger at stim onset. Will fail if no parallel port
# Get responses while the stimulus is updating using the event module
response = event.getKeys(keyList=KEYS_RESPONSE, timeStamped=clock)
if response and not response_given:
trial['key'], trial['rt'] = response[0]
response_given = True
# Using the iohub module
# Stimulus offset
win.flip()
send_trigger(TRIGGER_OFFSET) # send trigger
# Collect response
trial['key'], trial['rt'] = event.waitKeys(keyList=KEYS_RESPONSE, timeStamped=clock)[0]
# Save the trial
writer.write(trial)
# EXECUTE EXPERIMENT
show_instruction(TEXT_INTRO)
run_block()
# Quit gracefully
win.close()