CML Seminar, 2018

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: 

    Download (PDF, 107KB)

 

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:

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