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()