Tips and Tricks

Tips and Tricks for writing PEBL Programs


Abstraction of Trial Function

Tip: For any moderately complex experiment, make a function Trial() that handles the events of a single trial. This function should only perform display and response collection, and should be provided the stimulus from outside, performing no important randomization or decisions about stimulus properties. This allows you to generate the randomization at a higher (and usually simpler) level, allows you to use the Trial() function in other experiments more easily, and allows you to trade in another Trial() function without as much hassle.


Saving Data

Data is usually saved to a file with the FilePrint() and FilePrint_() commands. To save trouble later, attempt to save each piece of data tagged with all of the relevant independent variables (condition/trial/subject/etc.) that might be used during analysis, even if it is in some sense redundant. If a subject identifier is included on each line, then files from multiple subjects can be concatenated easily without worrying about losing identifiability. Even if you can determine whether a trial is correct by looking at the saved stimulus and the saved response, record accuracy on each trial. To better allow this information to be stored, write standard subject/block/trial/condition outside of the Trial() display function using FilePrint_(), which will not add a carriage return to the end of the line.

It can be useful to save each subject in a different file; see tip below on how to specify subject number from the command-line or launcher.


Using Subject Number Identifiers

Pass the subject number in to the experiment with the -v command-line option (also available through the windows launcher). At the beginning of the program, include the following code to ensure that subnum is set to something if no command-line options are provided

 if(IsList(par))
   {
      subnum <- First(par)
   } else {

      subnum <- "0"
   }

Multi-session Experiments

If you have a multi-session experiment that you want to counterbalance factors over, use the following idiom: at the beginning of the script, seed the random number generator with the subject number and select the counterbalance condition based on the subject number: SeedRandom(ToInteger(subnum)), create the complete stimulus sequence with counterbalancing or randomization that you want, and then select out the subset of trials relevant for the current block. This will ensure the same randomization or counterbalance for each subject, without having to read in any data files.


List-Based Processing

PEBL uses a list as its primary sequence type. This makes it a fairly direct descendent of LISP and encourages a functional programming style. But, LISP typically iterates using recursion, which is complex and difficult for beginners to understand. Instead, PEBL provides facilities for generating, combining, sorting, sampling from, and shuffling lists and for looping over the elements of a list.

In contrast, languages with a vector or matrix as the primary sequence encourage you to create a fixed-size sequence, and then directly access elements of the sequence.

As an example, consider the goal of designing an experiment with 50 A trials and 50 B trials, randomly ordered. In PEBL, the canonical way to do this is: sequence <-  Shuffle(Merge(Repeat("A",50), Repeat("B",50)))

In PEBL, experimental design problems should be thought about first in terms of the numbers of each trial condition. Later, the order of these trials can be shuffled.


Randomization Idioms

When generating a randomized design, Shuffle() is your friend. For complex designs, you may need to Sort() a list by the values produced by another list. Suppose you want to create a sequence that has mostly "A" trials at the beginning, and mostly "B" trials at the end. To do this, you might generate 50 standard normal random values with a mean of 0 associated with A, and another 50 with a mean of 2 associated with B. Then, sort combined lists based on these random values:

  aRand <- []   ##initialize empty lists
  bRand <- []
  i <- 0
  while(i < 50)
  {
    ##Add a random number to each list on each iteration
    aRand <- Append(aRand,RandomNormal(0,1))
    bRand <- Append(bRand,RandomNormal(1,1))
    i <- i + 1  ##don't forget to increment
  }
   ##Create a list of 50 As and 50 Bs, then sort by the merged random numbers above
   stim <- Sort(Merge(Repeat("A",50), Repeat("B",50)), Merge(aRand,bRand))
   

stim will be mostly As at the beginning, and mostly Bs at the end.


Looping over multiple lists

When you have two (or more) lists you want to loop over simultaneously (e.g., levels of two counterbalanced conditions), form a list of these condition lists and Transpose() this outer list, then loop over the elements of that list. Inside the loop, you can extract the individual elements:

   condA <- [1, 2, 1, 2, 1, 2, 1, 2]
   condB <- [1, 1, 1, 1, 2, 2, 2, 2]
   sequence <- Transpose([condA, condB])
   loop(i, sequence)
   {
      a <- First(i)
      b <- Nth(i,2)
      Print(a + " " + b)
    }

Use of Global Variables

PEBL allows global variables to be specified easily, simply by starting the variable name with a lowercase g. When used appropriately, this can be quite handy. Consider making the following objects global in appropriate situations: fonts, the main window, output files, commonly-used text controls. Avoid making other variables global, like color variables, stimulus sequences, any normal variable. Why? See table below for rationale.

Object

Make Global?

Rationale

color

Avoid Making global

consider defining implicitly within object definition that uses it

Window

Make Global

Main window is probably used throughout, clumsy to require passing it to each function that needs it. Call it gWin consistently and forget about it

Fonts

Make Global

Creating a font is relatively costly; it is better to create just once globally and use throughout

Input Files

Local Only

Read in data and close the file

Output Files

Make Global

Define globally so that trial-level functions can have access to them

Text Control

Make Global

It can be more efficient to re-use a single textbox or label, rather than repeatedly creating and destroying one within a trial function

Stimulus Sequence

Keep Local

Abstract experimental design so that just the information related to a specific trial or specific trial is sent to lower-level functions


Syncing to Vertical Retrace

Depending upon your computer, video card, and driver, it may be possible for PEBL to perform double-buffered drawing, which will synchronize drawing to the retrace of the monitor. One generally needs to be in fullscreen mode for this to work. By examining the output that reports what The first Draw() command will be executed immediately, but if another Draw() is issued immediately, execution will block until it is completed. So, to be safe, if you want to synchronize, issue three Draw() commands in a row while a stimulus is invisible (but already created and added to the window), then make the object visible and issue Draw() commands equal to the number of refresh cycles you want the stimulus to appear; finally, Hide() the object and Draw() again. It is possible to get very consistent presentation times, if your video card and driver allow.

last edited 2005-10-15 00:42:04 by 12-210-213-212


PEBL: The Psychology Experiment Building LanguageSourceForge.net Logo