Tuesday, March 15, 2011

Inelegant Hacks

PEBL often has some limits that people run into, and these can sometimes be overcome with other programs.  The SystemCall() function gives some powerful means to extend PEBL functionality.  All of these examples work for windows, although they could mostly be adapted to OSX or Linux fairly easily.  Some of these will make it into the new version of PEBL, either as shown or with a compiled implementation.  One word of caution--on windows, this function brings up a black terminal window when it runs the function, so it is best to not use this while you are collecting trial data; but for setup or later analysis it works fine. 





Determining if a file exists
One problem is that there currently (in Version 0.11) is no good way to know if a file you are about to open already exists.  Version 0.12, I've added functionality to do this, but it would be nice to check whether a data file exists before you potentially overwrite it or put two subjects in the same data file. 
 But you can call a the old MS-DOS command dir to find out whether a file exists.  This can be done with PEBL's SystemCall() function, but SystemCall() does not return the actual text that a command produces--if it were run on the command line, it would spit it out to see, but that display is otherwise lost.  But you can redirect the output to another file.  So, a basic strategy is to call a command an direct its output into a file, then read that file into PEBL  To do this, I'll pick an obscure filename (dir12345.txt) to put the output into, then delete it when done:



define FileExists(filename)
{
    SystemCall("dir /B "+filename + " > dir12345.txt")
    tmp <- FileOpenRead("dir12345.txt")
    dir <- FileReadLine(tmp)
    SystemCall("del dir12345.txt")
    return filename==dir   
}



Getting a directory listing
Sometimes, you have a set of stimili that are just images in a directory.  Right now, PEBL needs to know all of their names.  What if you had them in a list so you could read them in?  Adapting the above function is pretty easy.  Again, I'll have  a function in 0.12 that reads in files in a directory, but for now, this should suffice:

define GetJPEGS(directory)
{
    SystemCall("dir /B "+directory+ "\*.jpg > dir12345.txt")
    tmp <- FileOpenRead("dir12345.txt")
    dir <- FileReadList(tmp)
    SystemCall("del dir12345.txt")

    return dir
}

Now, you can just loop on the returned list and load all of the images lickety-split:

 files <- GetJPEGS("images")
 images <- []
 loop(i,files)
   {
        tmp <- MakeImage(i)
       images <- Append(images,tmp)
   }


Run an analysis script and display the results

Suppose you have collected data and you want to immediately analyze the data and give a report or display a figure in your script.  I'll use R to do the analysis.

  Just as an example, look at the following PEBL script, which just generates random data according to a linear model.

define Start(p)

{
     out <- FileOpenWrite("data.txt")
     i <- 1
    while (i < 1000)
    {
         x <- Random()*1000
         y <- 33 + .2 * x - .0009*x^2 + Random()*100
         FilePrint(out, i + " " + x + " " + y)
         i <- i + 1
    }

    ##Run an analysis script
     SystemCall("R CMD BATCH process3.R")
     gWin <- MakeWindow()

    img <- MakeImage("results.png")

    AddObject(img,gwin)
    Move(img,gVideoWidth/2,gVideoHeight/2)
    Draw()
    WaitForAnyKeyPress()
}

With the file process3.R:


dat <- read.table("data.txt")
  colnames(dat) <- c("i","x","y")
  glm <- lm(dat$y~dat$x+I(dat$x^2)+I(dat$x^3))
  png("results.png",width=800,height=600)
  plot(dat$x,dat$y,xlab="x",ylab="y",main="Results", xlim=c(0,1000))
  coef<-glm$coeff
  range <- 0:1000
  y <-  coef[1]+ coef[2]*range + coef[3]*range^2+coef[4]*range^3
  points(range,y,type="l",lwd=3,ly=3)
  dev.off()

Here, you'd need to alter R CMD text to point to where R is located on your computer.


Coordinating multi-session experiments
Suppose you have a multi-session experiment that you want to coordinate across sessions, either for counterbalancing or other experimental design issues, but you don't want to worry about entering which session is being run.  The first thing you should do is collect an participant code, and make a session log for that participant.  Explanation as comments in the code:



define Start(p)
{
   win <- MakeWindow()
   gSubNum <- GetSubNum(win)

  #make a name for the logfile that depends on subject code
  logname <- "explog-"+gSubNum+".csv"

  #Check if the file already exists
  if(FileExists(logname))
   {
       #if it does, find out how long it was.  FileReadList reads a file into a list
       ## we could also use FileReadTable(), and get the actual number of the session.
       tmp <- FileReadList(logname)

       ## Increment the session number
       session <- Length(tmp)+1

   } else {
       #This is the first session
       session <- 1
   }

   ##Now,know which session we are in.  Open the log file and write out a line
   ##including a timestamp, so you know when it took place.

   outlog <- FileOpenAppend(logname)
   FilePrint(outlog,gSubNum + "," + session +","+ TimeStamp())
   FileClose(outlog)
 
}

By the end of this code block, you have the session code.  But maybe you have a pool of stimuli you want to spread evenly across multiple sessions, but you want everyone to split them differently.  This isn't too hard when you understand how random numbers work in PEBL. PEBL uses a pseudo-random number generator, which means its random numbers are actually a deterministic sequence, and if you start at the same seed number, you will get the same sequence.  By default, PEBL starts the random sequence anew every time its run, using the current time as a seed.  But you can reset the seed anytime you want using the SeedRNG() function.   See the demo in demo\random.pbl for examples.



So, just seed the Random Number Generator (RNG) using the subject code, and you will be guaranteed to get the same sequence every time (make sure the subject code is a number).  The trick is to create the entire stimulus during each session, then pick out the ones you want for the current session.  Suppose you want to use the numbers 1 to 1000 as stimuli, spread across five sessions. Add the following code before the final closed-brace of the above code:

  #Make sure gSubNum is a number
  subnum <- ToNumber (gSubNum)
 SeedRNG(subnum)
  #create a random sequence of the stimuli, guaranteed to be the same every time the subject is run
  stim <- Shuffle(Sequence(1,100,1))

  sessionStim <- SubList(stim, 1 + (session-1)*20, session*20)
  Print(sessionStim)


Running this 5 times produces the following output.  You can see that each number 1 through 100 is represented exactly once across the five sessions.



Session 1:
[5, 25, 6, 35, 28, 48, 18, 12, 40, 72, 7, 82, 87, 20, 43, 80, 83, 86, 9, 4]
Session 2:
[17, 76, 77, 64, 31, 26, 74, 27, 88, 66, 34, 94, 92, 10, 45, 36, 44, 3, 60, 19]
Session 3:
[73, 65, 41, 21, 67, 14, 47, 58, 61, 53, 70, 22, 54, 63, 33, 2, 84, 96, 57, 100]
Session 4:
[81, 32, 51, 42, 55, 78, 69, 79, 23, 1, 52, 16, 97, 93, 24, 29, 49, 8, 30, 95]
Session 5:
[68, 91, 38, 50, 11, 99, 59, 75, 13, 39, 90, 46, 37, 98, 85, 56, 62, 15, 71, 89]

And I have a session log that tells me when each session was run:
99,1,Sun Feb 27 12:04:34 2011
99,2,Sun Feb 27 12:04:37 2011
99,3,Sun Feb 27 12:04:39 2011
99,4,Sun Feb 27 12:04:42 2011
99,5,Sun Feb 27 12:04:57 2011

No comments: