Tuesday, August 23, 2011

Interactive PyEPL session

Sometimes, I am interested in exploring some PyEPL feature without writing a whole experiment with setting up subject and all that. I rather like to have an interactive python shell and hack away. Here is the code on how to do that:

from pyepl.locals import *
import os
import shutil

# create an experiment object:
# parse command-line arguments
# & initialize pyepl subsystems
tdir = "/tmp/iPyEPL"
subj = "isubj"
subjdir = os.path.join(tdir,subj)

if not os.path.exists(tdir):
os.makedirs(tdir)


if os.path.exists(subjdir):
shutil.rmtree(subjdir)


exp = Experiment(archive=tdir,subject=subj,fullscreen=False)
exp.setBreak()

# load some tracks
vt = VideoTrack("video")
kt = KeyTrack("key")
#at = AudioTrack("audio")

# reset the display to black
vt.clear("black")
vt.updateScreen()

# create a PresentationClock object
# for timing
clk = PresentationClock()




Just copy and paste into your python session and you are ready to explore. Thanks to Per for that!

Monday, August 8, 2011

How to attach a function to a class

In "Presenting stimulus for fixed duration" we saw how we can easily write a new class to extend existing classes with new functions. While it works, I always had the problem to put this class on every computer and to have it work correctly with different Python Path settings. So, here is a neat trick how to do the same without writing your own class:


from pyepl.locals import *
def present_fixed_dur(clk=None, duration=None, bc=None, minDuration=None):
"""
This is just like the standard Image.present, except
that if you press a button, the image stays on the
screen until the full duration has passed.

I got rid of the jitter argument, because it makes
things complicated.
"""

v = VideoTrack.lastInstance()

# get the clock if needed
if clk is None: clk = PresentationClock()

# show the image
t = v.showCentered(self)
timestamp = v.updateScreen(clk)

if bc:
# wait for button press
button,bc_time = bc.waitWithTime(minDuration,duration,clk)
# figure out how much time is remaining, now
# that they've pressed the button, and delay for
# just that
rt = bc_time[0] - timestamp[0]
clk.delay(duration-rt)
else:
clk.delay(duration)

# unshow that image
v.unshow(t)
upd_ts = v.updateScreen(clk)
# print 'presented for %ims' % (upd_ts[0] - timestamp[0])

if bc: return timestamp,button,bc_time
else: return timestamp

# Use a reference to the function and attach it as a attribute to the Image class
Image.present_fixed_dur = present_fixed_dur


Et voila, we extended the Image class on the fly. :)


Thursday, October 22, 2009

Presenting a stimulus for a fixed duration (despite button presses)

Especially with fMRI, I want my trials to last a particular duration, whether or not the subject responded by pressing a button.

The standard PRESENT function terminates as soon as a button has been pressed. You could immediately re-present the stimulus for the remaining duration, but this creates an annoying flicker.

I sub-classed Text and Image to add a PRESENT_FIXED_DUR function that deals with this problem. This issue was also raised on the PyEPL mailing list.

For images:

#!/usr/bin/python

# from pyepl.hardware.graphics import Image
from pyepl.locals import Image
from pyepl.display import VideoTrack
from pyepl.locals import PresentationClock

class Image2(Image):

    def present_fixed_dur(self, clk=None, duration=None, bc=None, minDuration=None):
        """
        This is just like the standard Image.present, except
        that if you press a button, the image stays on the
        screen until the full duration has passed.

        I got rid of the jitter argument, because it makes
        things complicated.
        """

        v = VideoTrack.lastInstance()
        
        # get the clock if needed
        if clk is None: clk = PresentationClock()

        # show the image
        t = v.showCentered(self)
        timestamp = v.updateScreen(clk)

        if bc:
            # wait for button press
            button,bc_time = bc.waitWithTime(minDuration,duration,clk)
            # figure out how much time is remaining, now
            # that they've pressed the button, and delay for
            # just that
            rt = bc_time[0] - timestamp[0]
            clk.delay(duration-rt)
        else:
            clk.delay(duration)

        # unshow that image
        v.unshow(t)
        upd_ts = v.updateScreen(clk)
        # print 'presented for %ims' % (upd_ts[0] - timestamp[0])

        if bc: return timestamp,button,bc_time
        else: return timestamp

For text:

#!/usr/bin/python

# from pyepl.hardware.graphics import Image
from pyepl.locals import Text
from pyepl.display import VideoTrack

class Text2(Text):

    def present_fixed_dur(self, clk=None, duration=None, bc=None, minDuration=None, xProp=.5, yProp=.5):
        """
        Just as Text2.present, but (like
        Image2.present_fixed_dur): the standard
        Text.present, except that if you press a button, the
        stimulus stays on the screen until the full duration
        has passed.
        """

        # print 'starting present_fixed_dur'
        v = VideoTrack.lastInstance()
        
        # get the clock if needed
        if clk is None: clk = exputils.PresentationClock()

        # show the image
        t = v.showProportional(self,xProp,yProp)
        timestamp = v.updateScreen(clk)

        if bc:
            # wait for button press
            button,bc_time = bc.waitWithTime(minDuration,duration,clk)
            rt = bc_time[0] - timestamp[0]
            # print 'delaying by %i' % (duration-rt)
            # only delay if duration is True
            if duration and (duration-rt): clk.delay(duration-rt)
        else:
            # print 'delaying by %i' % duration
            if duration: clk.delay(duration)

        v.unshow(t)
        v.updateScreen(clk)

        # print 'ending present_fixed_dur'

        if bc: return timestamp,button,bc_time
        else: return timestamp

Friday, August 8, 2008

Getting up to speed with Python

PyEPL is my favorite way to write experiments, and I'd recommend it strongly to anyone who likes programming. However, if you don't like programming, or you really hate the idea of learning to program, you're probably better off with E-Prime or something akin.

If you're new to the idea of programming but enthusiastic about it, then I'd strongly recommend going through the first 8 chapters of Michael Dawson's Python Programming for the Absolute Beginner (2nd edn). He covers a wide range of essential python syntax and functionality, with lots and lots of examples. Even if you're not that excited about video games (which comprise the majority of his examples), it's still an excellent and reasonably comprehensive resource. Going through that before trying to write your first experiment will make things proceed much more smoothly.

If you're an experienced programmer who's new to Python (or just want to brush up further), then I'd strongly recommend Mark Pilgrim's Dive into Python. This is actually completely freely available online, but the book edition is pretty affordable and sends a message of yayness to the author and publisher.

A skeleton Experiment class

I've experimented with lots of different ways of organizing my PyEPL experiment code. I like to place different parts of the experiment in different functions - this makes it easier to figure out what's going on where, and helps greatly when you want to reuse pieces in future experiments. Because most experiments require you to keep track of lots of variables, I'd often end up feeding many, many parameters into each of my functions, e.g.


############################################################
def individStimulus(sessionlog,
video, audio, backgrounds, clips,
clock, bc, wp, state,
config, study_run_no, wordno, taskno, prevTask):
...


Sadly, that is a real example.

In order to avoid having to pass the same horde of variables into each function (VideoTrack, PresentationClock etc.), I tried creating a bunch of global variables that would be shared throughout my experiment code. I'd initialize them all at the very top, but then every function that used any of them would have to declare those variables as global, e.g.


############################################################
def prepare():
"""
Prepare the sequences.
"""

global exp
global config
global video
global audio
global keyboard
global sessionlog
global cueslog
global clock
global study_seqs
global loci_pool
global items_pool


This was really no better. In fact, there's a potential gotcha awaiting the unwary when using global variables inside a function if you forget to declare the variables as global, since Python will simply assume that these are new local variables of the same name (see Example 2).

Now, older and wiser, I have realized that the one true way of organizing your experiment code is to subclass the PyEPL Experiment class, and make use of member variables. Describing classes is beyond the scope of this document, but see here for more info on learning Python if you're unfamiliar with object-oriented programming.

Without further ado, here's a skeleton Experiment subclass that you can build on when first starting a new experiment:

my_exp.py


#!/usr/bin/python

from pyepl.locals import *
import shutil

# only add this if there really is a good reason to exclude
# earlier versions. i like 1.0.29 because it adds
# functionality for resetting accumulated timing error
MIN_PYEPL_VERSION = '1.0.29'


# TODO: change the classname to something more appropriate
# for your experiment
class MyExperiment(Experiment):

############################################################
def __init__(self,**kwargs):
"""
Create the Experiment superclass, set basic
Experiment stuff up, get the config, and prepare all
our main Experiment member variables.
"""

# this is all standard
#
# initialize the Experiment superclass
Experiment.__init__(self,**kwargs)

# allow users to break out of the experiment with
# escape-F1 (the default key combo)
self.setBreak()
# get config
self.cfg = self.getConfig()

# if you have any randomization, you'll probably
# want to make sure it's different very time. i
# think python automatically resets the seed each
# time, but i keep this in just in case
random.seed()

# set up the logs
self.vid = VideoTrack("video")
self.key = KeyTrack("keyboard")
# this is the one i use for storing all my main
# experiment data
self.slog = LogTrack("session")

# set up the presentation clock
try:
# requires 1.0.29, auto-adjusts timing if there's a lag
self.clk = PresentationClock(correctAccumulatedErrors=True)
except:
# if you don't have 1.0.29 loaded, then just
# fall back (since the timing will probably be
# fine anyway)
self.clk = PresentationClock()

self.instr = open(self.cfg.instr_fname,'r').read()

# copy the instructions text into the data directory
# for this session, so that you can refer to it
# later if you need to
shutil.copy(self.cfg.instr_fname, self.session.fullPath())

self._resetVideo()

# TODO call any stimulus generation or other
# preparation functions that you need to get things ready
# for the experiment to run.
self._prepare()

self._sanity_check()


############################################################
def _prepare(self):
"""
Set everything up before we try and run.
"""

# we'll make use of this later
self.greeting_msg = 'Hello'


############################################################
def _resetVideo(self):
"""
And then there was no light.
"""

self.vid.clear("black")
self.vid.updateScreen(self.clk)


############################################################
def _sanity_check(self):

"""
This is where i confirm that everything has been set
up correctly and that everything we'll need for the
rest of the experiment is in place (so that any
problems cause a crash now rather than halfway
through an experiment).

For instance, it's a good place to make sure any
basic constraints are adhered to in your config.
"""

# TODO - add your sanity checks here
assert 2 + 2 == 4


############################################################
def go(self):
"""
Run the entire loci1h experiment
"""

# log start of experiment
self.slog.logMessage('SESS_START')

# tell the subject what's about to happen
instruct(self.instr)

# TODO this is where your experiment code kicks off
flashStimulus(Text(self.greeting_msg),duration=3000)

self.slog.logMessage('SESS_END')

Text("Thank you!\nYou have completed the session.").present(
clk=self.clk,duration=5000)



############################################################
def main_interactive(config='config.py'):

"""
If you want to run things interactively from ipython,
call this. It's basically the same as the main()
function.

from my_exp import *; exp = main_interactive()

OR if you want to load in a special config file:

from my_exp import *; exp = main_interactive('debug_config.py')
"""

exp = MyExperiment(subject='subj00',
fullscreen=False,
resolution=(640,480),
config=config)

exp.go()

return exp



############################################################
if __name__ == "__main__":
# make sure we have the min pyepl version
checkVersion(MIN_PYEPL_VERSION)

# start PyEPL, parse command line options, and do
# subject housekeeping
exp = MyExperiment()

# now run the subject
exp.go()


Notice for instance how the _prepare() function creates a member variable (self.greeting_msg). That gets stored in the MyExperiment object, and it's ready to be deployed in go() by the flashStimulus call. No passing around of arguments, no global variables, and it's easy to subclass this MyExperiment class as MySlightlyImprovedExperiment, just overriding the functions that differ.

I've marked places where you'll almost certainly want to modify and add code for your experiment with a 'TODO' comment. Save the above as my_exp.py.

You'll also need a config.py:

config.py


instr_fname = 'instr.txt'


and some instructions:

instr.txt


These are the instructions. You're about to be greeted by
the program.

Press UP/DOWN or PgUP/PgDOWN to scroll these
instructions. When you get to the bottom of the page, please
discuss what you've read with the experimenter to be sure
that everything is clear, and then press RETURN to move on.

--- please discuss what you have read with the experimenter ---

--- then press RETURN to continue ---


Put all of them in a directory, and you should be able to run them with:


python my_exp.py -s subj00 --resolution 640x480 --no-eeg --no-fs


or fire up python and type:


from my_exp import *; exp = main_interactive()

Sunday, August 3, 2008

Don't initialize the ButtonChooser with lowercase letters

Otherwise, you could get an inscrutable ValueError, 'Key already bound' error message. Always use uppercase letters, and all should be well. I think this has something to do with the error-checking in the KeyboardTrack that won't allow multiple ButtonChoosers to simultaneously be checking the same key.

Timing how long things are taking

When I first started writing experiments in PyEPL, I had a lot of trouble getting the timing right. I wanted a way to time how long things were taking that didn't make use of the PyEPL PresentationClock, to confirm that all was well. The Stopwatch class below is based heavily on timing-example-2.py.


"""
This is my wrapper for the time module. There's probably an
easier way to time the duration of things, but when I looked
into timing stuff, this was the best I could come up with...

To use:

t = Stopwatch()
t.start_time()

# do something

elapsed = t.finish()
"""

import time


class Stopwatch:
"""
Creates stopwatch timer objects.
"""

# stores the time the stopwatch was started
t0 = None

# stores the time the stopwatch was last looked at
t1 = None


def __init__(self):
self.t0 = 0
self.t1 = 0


def start(self):
"""
Stores the current time in t0.
"""

self.t0 = time.time()


def finish(self):
"""
Returns the elapsed duration in milliseconds. This
stores the current time in t1, and calculates the
difference between t0 (the stored start time) and
t1, so if you call this multiple times, you'll get a
larger answer each time.

You have to call this in order to update t1.
"""

self.t1 = time.time()
return self.milli()


def milli(self):
"""
Returns t1 - t0 in milliseconds. Does not update t1.
"""
return int((self.t1 - self.t0) * 1000)

How do I check whether a particular key has been pressed?

# initialize your ButtonChooser
pressed, rt = waitWithChoice(blah,blah)

if pressed==None:
  print 'You did not press a key'
else:
  if pressed==Key('a'):
    print 'You pressed the A key'
  else:
    print 'You pressed something other than the A key'

I can't quit from the experiment!

Make sure you have exp.setBreak() in your code. Then you should be able to use Esc-F1

N.B. on the Mac, the F1 key doubles up as a brightness control, and so you have to hold down the 'Function' key to get it to behave, so you'd hold down Fn, and then press Esc-F1.

None of my keypresses are taking effect!

Make sure you set up a KeyboardTrack.