Table of Contents

MGL

Mgl is a suite of mex/m files for displaying visual psychophysics stimuli and writing experimental programs in Matlab. Runs on Mac OS X (G4/5 and Intel 32 and Intel 64 bit OS Versions 10.5-10.7) Version 2.0.

A quick overview

mgl is a set of matlab functions for displaying full screen visual stimuli from matlab. It is based on OpenGL functions, but abstracts these into more simple functions that can be used to code various kinds of visual stimuli. It can be used on Mac OS X systems (a Windows beta exits which can open/close screen and display but is not ready for serious use and an older but usable version (1.5) exists for Linux. Neither windows nor linux versions are recommended for anyone who is not comfortable programming in C and building out existing functionality into a fully compatible version).

The best way to see whether it will be useful to you is to try out the mglTest programs and also the sample experiment testExperiment. A basic “hello world” program can be written in four lines:

  % Open the screen, 0 specifies to draw in a window. 
  % 1 would be full screen in the main display
  % 2 would be full screen in a secondary display, etc... 
  >> mglOpen(0);
 
  % Select the coordinate frame for drawing
  % (e.g. for a monitor 57 cm away, which has width and height of 16 and 12 cm). 
  >> mglVisualAngleCoordinates(57,[16 12]);
 
  % Draw the text in the center (i.e. at 0,0)
  >> mglTextDraw('Hello World!',[0 0]);
 
  % The above is drawn on the back-buffer of the double-buffered display
  % To make it show up you flush the display.
  % This function will wait till the end of the screen refresh
  >> mglFlush;

When finished, with displaying the stimuli, you simply close the screen:

>> mglClose;

Design philosophy

A couple of things that distinguish MGL from other packages for doing psychophysics experiments using Matlab.

1) Simple standalone functions. We were aiming to make the code as simple as possible, so that we and others could extend functionality as easily as possible. So a major design goal was to keep each function as atomic and simple as possible - every function is one C/MEX file that has OS specific code clearly separated out, typically as a separate function. Something like the philosophy behind UNIX where kitchen sink type functions are frowned upon. This was so that it would be easy to hack in the future and for others. New functionality typically gets added as a separate function so we don't risk breaking existing code. Functions are simple so that they are easier to maintain against OS upgrades (the back end of MGL was rewritten a few years ago using objective-C so that we could be 64-bit compliant).

2) Task library. We have a low-level library for displaying stimuli and accessing hardware as well as a task library which sits on top of that. The task library simplifies writing experiments since it takes care of timing, trials, synching with digital pulses, interfacing with eye-trackers etc. Each new experiment simply implements callback functions that handle how to draw the screen, what to do when a subject responds etc. This makes it quick to write new experiments and makes sure that problems fixed for one experiment are solved for all experiments.

Download

The latest version of mgl (2.0) can be retrieved using subversion:

svn checkout http://gru.brain.riken.jp/svn/mgl/trunk mgl

We recommend using subversion (see here for a quick primer) if at all possible. Mac OS 10.5-7 comes with subversion preinstalled. The latest versions of Mac OS 10.8 (Mountain Lion) does not have svn preinstalled. You can install svn by installing the latest Xcode (make sure that you have checked the option to install the command line tools to get svn - if you already have Xcode installed, but svn is not installed, go to Xcode > Preferences > Downloads > Command Line Tools > Install). You will also need to have Xcode installed if you need to recompile mgl. If these options fail, you can install svn directly from the subversion website. If you are really unable to get subversion, you can download mgl.tar.gz which contains a current version of the code. It should just open by clicking on it. If not, from a command line, you should be able to do:

gunzip mgl.tar.gz
tar xfv mgl.tar

Once you have downloaded, see Initial setup for installation instructions.

Problems downloading with SVN

If you have problems downloading with svn because your institution has a proxy server, you might have to let subversion know about it. Have a look at download_with_proxy-server for details on how to set this up.

Older versions of mgl

You can access a stable read-only version of mgl (version 1.5) by using subversion:

svn checkout http://gru.brain.riken.jp/svn/mgl/branches/v1_5 mgl

We are no longer making updates to version 1.5

Initial setup

  1. Add the mgl directory to your Matlab path. In Matlab:
    >> addpath(genpath('MYPATH/mgl'));

    where MYPATH should be replaced by the path to your version of mgl.

  2. Make sure to enable access for assistive devices so that you can use the function mglGetKeyEvent, mglGetMouseEvent, mglPostEvent, mglSimulateRun and mglEatKeys.
  3. There are a few functions (mglEditScreenParams and mglDoRetinotopy) that require the mrTools GUI functions to be installed. If you are not already using mrTools, you can download the following:
      svn checkout http://cbi.nyu.edu/svn/mrTools/trunk/mrUtilities/MatlabUtilities mrToolsUtilities

    and add that to your MATLAB path (with MYPATH replaced with the path to your version of the mrToolsUtilities):

    >> addpath(genpath('MYPATH/mrToolsUtilities'));

Test your setup

You can see what functions are available by doing (in matlab):

>> help mgl

After downloading, you may wish to try the mglTest programs (e.g. mglTestDots, mglTestTex, etc….).

If these functions don't work or you are running on Linux (version 1.5 only), then you may need to recompile. This may especially be necessary if you are running an older version of matlab (we run Matlab version >= 7.3 on Mac OS >= 10.4.8). We have found that mex files created on Matlab 7.3 do not run on matlab 14.1 for instance (if you run -nojvm you will see that it complains that it cannot find a dynamic link library for the mex functions – if you run with the matlab desktop it will just crash the system). If this happens to you simply recompile and you should be good to go.

What is in the mgl distribution

GNU General Public License

These programs are free to distribute under the GNU General Public License. See the file mgl/COPYING for details.

Recompiling mgl

If you are running into an obvious error like a segmentation fault, then you may want to recompile. The command to recompile is:

» mglMake(1);

If you run into any problems with mglMake, you may want to restart matlab and try again.

The mglMake command simply runs mex on all the .c files in the mgl/mgllib directory – you can do this by hand (e.g. mex mglPrivateOpen.c), if you prefer.

Note that this requires mex to be setup properly on your machine. On a Mac OS X machine, at a minimum you will need to have the apple developer tools installed (XCode) http://developer.apple.com/tools/. On linux, you will need a compatible version of gcc (older versions of gcc can be found here).

See here for more help on compiling.

If all else fails, how to get back control over the display?

If you can't do mglClose, you can always press:

option-command-esc

this will quit your matlab session as well.

Also, some of our test programs will run until you hit the <ESC> key (e.g. testExperiment), so it is worth trying that as well.

Can I get access to all OpenGL functions?

We have only exposed parts of the OpenGL functionality. If you need to dig deeper to code your stimulus, consider writing your own mex file. This will allow you to use the full functionality of the OpenGL library. To do this, you could start by modifying one of our mex functions (e.g. mglClearScreen.c) and add your own GL code to do what you want and compile.

Printing the wiki help pages

You can print out all the wiki help pages at once, by using this link.

Compatibility with latest MAC OS and Matlab versions

We stay reasonably up-to-date with OS and Matlab versions. As of this writing (11/25/2012) we are using MGL on Mac OS 10.7.5 and Matlab 7.13 on 64 bit. If you are using a newer or very much older version of MGL, you may need to recompile. Note that the Eyelink and NI Digital I/O code may particularly have compatibility issues as they rely on libraries made by their manufacturers. As of this writing Eyelink has a 64 bit library which we run that works. NI Digital I/O is still basically 32 bit.

Resolved issues

Check here for issues that have been resolved. Some issues may also be covered in our issue tracking software redmine.

Recompiling MGL

We (Apple developers) run the latest Mac OS (10.8.4 as of 6/19/2013) with the latest version of Matlab (8.1) or (Linux developers) Ubuntu 64-bit (Gutsy) and 32-bit (Feisty) with Matlab 7.4 and the binaries are created to run on these systems. As noted above, some older versions (notably Matlab 14.1) are not able to use these mex files and crash when you try to run mglOpen. If this happens, then all you need to do is recompile MGL using mglMake(1). You may also have to make sure that you link to the correct version of the Mac OS X system frameworks.

License manager timing glitch

The Matlab license manager checks every 30 seconds for the license. This can cause there to be an apparent frame glitch in your stimulus code, especially if you are using a network license (on our machines it can take ~200 ms to check for the license). The only known workaround to this is to run on a machine that has a local copy of the license. You can check this for yourself by seeing how long it takes to do screen refreshes:

 mglOpen;
 global MGL;
 checkTime = 30*MGL.frameRate;
 timeTaken = zeros(1,checkTime);
 mglFlush;
 for i = 1:checkTime
   flushStart = mglGetSecs;
   mglFlush;
   timeTaken(i) = mglGetSecs(flushStart);
 end 
 mglClose;
 plot((1:checkTime)/MGL.frameRate,timeTaken);
 zoom on;xlabel('seconds');ylabel('Frame refresh time (seconds)');

If you have the same problem, you should see one large spike in the time course like this:

This one shows it taking about 65 ms. Note that you may see small deviations in which one frame takes longer and then the following frame takes shorter than the mean. These are normal flucations persumably due to multi-tasking and other events that are intermittently taking up time. As long as these are shorter than a frame refresh interval minus the time it takes you to process the stimuli for your display, you will not drop any frames. Note that in the above code, if you change mglFlush to any other command, such as WaitSecs(1/frameRate);, you will still see the big spike for the license manager check–confirming that this has nothing to do with drawing to the screen.

Problems compiling

If you are trying to compile on Mac OS X and are running into problems, you may not be linking against the proper systems libraries. We typically (as of 6/19/2013) are compiling against Mac OS X 10.6 SDK, but you may need an earlier or later version of the Mac OS X SDK's. We have updated mglMake so that it does some basic checks and uses different mexopts.sh files for different operating systems (i.e. can either compile with 10.6 SDK or 10.8 SDK, etc), but if this fails for your system you may want to try to go in by hand and change the mexopts.sh file yourself to link against the appropriate SDK. To do so, for example, for 10.5:

  1. Make sure that you have Xcode properly installed.
  2. Next, check that you have the proper Mac OS X SDK installed. You generally want the 10.6 SDK (assuming your operating system is up-to-date though more recent versions of the SDK (10.7 and 10.8) should work as well. If you need to compile for an older systems like 10.5, check that it is installed properly (this should be installed when you install Xcode)
      /Developer/SDKs/MacOSX10.5.sdk/System/Library/Frameworks/
  3. You may no have to go into the “mexopts.sh” file in the mgl/mgllib directory to link to 10.5 instead of 10.6. Change the following lines appropriately to link to 10.5:
      SDKROOT='/Developer/SDKs/MacOSX10.5.sdk'
      MACOSX_DEPLOYMENT_TARGET='10.5'

SVN problems

If you have problems updating with SVN because you have changed files, you might be running into file or tree conflicts. See here for more info.

I have run into some unresolvable conflicts with the following error:

svn: GET of '/svn/mgl/!svn/bc/946/trunk/mgllib/mglEyelink/mglPrivateEyelinkReadEDF.c': Could not read response body: connection was closed by server (http://gru.brain.riken.jp)

You may try to svn checkout the whole repository again (remember to make a copy of your current repository and then copy back the files you have changed). It seems that this can be caused by Apache timing out for long updates, but that doesn't seem to be the reason why we are getting that (we have set out timeout very long and this happens for single files and always at the same place in certain revisions). I suspect that it is some problem with the Mac Apache svn/PHP module, but have not been able to figure it out. A workaround is to use svn+ssh for accessing the repository, but this is only for developers who have an account on our server gru.brain.riken.jp (email to justin if you think you need this). Also, if someone knows what the problem is and can provide a fix, let us know.

Function not supported on Linux

Note that only version 1.5 supports linux, version 2.0 and beyond do not have linux support at the moment (actually could be supported with a little bit of work, if someone is interested in putting in the effort).

The list of funcitons not supported yet on linux for version 1.5 are:

If you want to use text under the linux operating system, you can use mglStrokeText.

Here is a more recent update from Jonas about the Linux version:

I am in the process of upgrading the Linux version of MGL to run under Ubuntu (64-bit and 32-bit) with NVIDIA and ATI graphics cards. Although the upgrade is still incomplete, most functions work equally well under Linux at this stage. Some differences that will remain between the platforms are listed below.

For the time being, only NVIDIA and ATI cards will be supported (because I only have access to those two machines).

Main screen functions

mglOpen: Opens the screen

usage: mglOpen(whichScreen, <screenWidth>, <screenHeight>, <frameRate>, <bitDepth>)
purpose: Opens an openGL window

argument value
whichScreen 0=Window, 1=primary screen, 2=secondary screen, etc
<screenWidth> [width] in pixels
<screenHeight> [height] in pixels
<frameRate> [frameRate] in hertz
<bitDepth> [bitDepth] this is usually 32 bits

Open last monitor in list with current window settings

mglOpen

Open with resolution 800×600 60Hz 32bit fullscreen

mglOpen(1,800,600,60,32);

Open in a window

mglOpen(0);


Note that mglOpen hides the mouse cursor as a default. If you want to show the mouse cursor, you should call mglDisplayCursor after opening the screen.

mglFlush: Flips front and back buffer

purpose: swap front and back buffer (waits for one frame tick)
usage: mglFlush
N.B. mglFlush has to be called after each operation that involves drawing or erasing from the currently open window

mglClose: Closes the screen

purpose: close OpenGL screen
usage: mglClose

Other screen functions

mglResolution: Get and set display resolution

availability: mgl 2.0
usage: mglResolution(<whichScreen>, <screenWidth>, <screenHeight>, <frameRate>, <bitDepth>)
purpose: Gets or sets the resolution of a display usage: To get the resolution of the default display:

mglResolution

To get the resolution of display 1:

mglResolution(1)

To set the resolution of display 2 to 1440×900:

mglResolution(2,1440,900)

mglSwitchDisplay: Switch between multiple monitors

usage: mglSwitchDisplay(<displayID>)
purpose: If you are using multiple monitors to display stimuli (like for a dichoptic presentation), you can open up multiple displays using this function.

argument value
<displayID> which display to switch to

You can use this to open up two separate screens and control them independently. For example, say you have two monitors 1 and 2. You open the first in the usual way:

mglOpen(1)

Then you switch monitors so that you can open up the other one

mglSwitchDisplay
mglOpen(2)

Now if you want draw to the first display, you can do

mglSwitchDisplay(1)
mglClearScreen(0.5);
mglFlush;

Similarly, to draw to the second display

mglSwitchDisplay(2);
mglClearScreen(1);
mglFlush;

You can check the status of all open displays with:

mglSwitchDisplay(-2);

If you want to close all displays at once, you can do:

mglSwitchDisplay(-1);

mglMoveWindow: Moves windows created by mglOpen(0)

usage: mglMoveWindow(leftPos,topPos)
purpose: Moves a window created by mglOpen(0)

argument value
leftPos Left position of where the window will be moved to
topPos Top position of where the window will be moved to
mglOpen(0);
mglMoveWindow(100,100);


mglDescribeDisplays: Get information about your monitor and computer system

usage: [displayInfo computerInfo] = mglDescribeDisplays()
purpose: Gets information about your displays (as an array of structs with values for each monitor). As well as a single struct with info about your computer.

mglFrameGrab: Frame grab to a matlab matrix

usage: mglFrameGrab(<frameRect>)
purpose: Does a frame grab of the current mgl screen and returns it as a matrix of dimensions widthxheightx3

argument value
frameRect Optional argument that is a 1×4 array specifying a rectangular part of the frame to grab in the format [x y width height]
mglOpen();
mglScreenCoordinates;
mglClearScreen([0 0 0]);
mglPoints2(mglGetParam('screenWidth')*rand(5000,1),mglGetParam('screenHeight')*rand(5000,1));
mglPolygon([0 0 mglGetParam('screenWidth') mglGetParam('screenWidth')],[mglGetParam('screenHeight')/3 mglGetParam('screenHeight')*2/3 mglGetParam('screenHeight')*2/3 mglGetParam('screenHeight')/3],0);
mglTextSet('Helvetica',32,[1 1 1]);
mglTextDraw('Frame Grab',[mglGetParam('screenWidth')/2 mglGetParam('screenHeight')/2]);
frame = mglFrameGrab;
imagesc(mean(frame,3)');colormap('gray')
mglFlush

Functions to adjust the coordinate frame

mglVisualAngleCoordinates: Visual angle coordinates

purpose: Sets view transformation to correspond to visual angles (in degrees) given size and distance of display. Display must be open and have valid width and height (defined in MGL variable)
usage: mglVisualAngleCoordinates(physicalDistance,physicalSize);

argument value
physicalDistance [distance] in cm
physicalSize [width height] in cm
mglOpen
mglVisualAngleCoordinates(57,[16 12]);

mglScreenCoordinates: Pixel coordinate frame

purpose: Set coordinate frame so that it is in pixels with 0,0 in the top left hand corrner
usage: mglScreenCoordinates()

mglTransform: Low-level function to adjust transforms

purpose: applies view transformations
usage: mglTransform(whichMatrix, whichTransform, [whichParameters])

argument value
whichMatrix 'GL_MODELVIEW', 'GL_PROJECTION', or 'GL_TEXTURE'
whichTransform 'glRotate', 'glTranslate', 'glScale','glMultMatrix', 'glFrustum', 'glOrtho','glLoadMatrix', 'glLoadIdentity', 'glPushMatrix','glPopMatrix', 'glDepthRange', or 'glViewport'
whichParameters function-specific; see OpenGL documentation

You can also specifiy one of GL_MODELVIEW, GL_PROJECTION, or GL_TEXTURE and a return variable current matrix values. If two outputs are specified, the result of the computation will be returned.

This function is usually not called directly, but called by mglVisualAngleCoordinates or mglScreenCoordinates to set the transforms

mglHFlip: Horizontally flip coordinates

purpose: flips coordinate frame horizontally, useful for when the display is viewed through a mirror
usage: mglHFlip()

mglOpen
mglVisualAngleCoordinates(57,[16 12]);
mglHFlip
mglTextSet('Helvetica',32,1,0,0,0,0,0,0,0);
mglTextDraw('Mirror reversed',[0 0]);
mglFlush;

mglVFlip: Vertically flip coordinates

purpose: flips coordinate frame vertically
usage: mglVFlip

mglOpen
mglVisualAngleCoordinates(57,[16 12]);
mglVFlip
mglTextSet('Helvetica',32,[1 1 1],0,0,0,0,0,0,0);
mglTextDraw('Vertically flipped',[0 0]);
mglFlush;

Texture functions used for displaying images

mglCreateTexture: Create a texture from a matrix

purpose: Create a texture for display on the screen with mglBltTexture image can either be grayscale nxm, color nxmx3 or color+alpha nxmx4
usage: texture = mglCreateTexture(image,axis)

argument value
image nxm matrix of grayscale values from 0 to 255, or nxmx3 matrix of RGB values or nxmx4 of RGBA values
axis 'xy' rows are x and columns are y dimension (default–matlab oriented matrix, i.e. will give you the same results as using imagesc), 'yx' rows are y and columns are x dimension
OUTPUT: texture a texture structure that can be drawn to the screen using mglBltTexture
mglOpen;
mglClearScreen
mglScreenCoordinates
texture = mglCreateTexture(round(rand(100,100)*255));
mglBltTexture(texture,[0 0]);
mglFlush;

When you are done using a texture, you may want to free its memory using mglDeleteTexture.

mglBltTexture: Draw the texture to the screen

purpose: Draw a texture to the screen in desired position.
usage: mglBltTexture(texture,position,<hAlignment>,<vAlignment>,<rotation>)

argument value
texture A texture structure created by mglCreateTexture or mglText.
position Either a 2-vector [xpos ypos] or 4-vector [xpos ypos width height]. units are degrees.
hAlignment -1 = left, 0 = center, 1 = right (defaults to center)
vAlignment -1 = top, 0 = center, 1 = bottom (defaults to center)
rotation rotation in degrees, defaults to 0

To display several textures at once, texture can be an array of n textures, position is nx2, or nx4 and hAlignment, vAlignment and rotation are either a single value or an array of n.

multiple textures:

 mglOpen;
 mglVisualAngleCoordinates(57,[16 12]);
 image = rand(200,200)*255;
 imageTex = mglCreateTexture(image);
 mglBltTexture([imageTex imageTex],[-3 0;3 0],0,0,[-15 15]);
 mglFlush;

single textures

 mglOpen;
 mglVisualAngleCoordinates(57,[16 12]);
 image = rand(200,200)*255;
 imageTex = mglCreateTexture(image);
 mglBltTexture(imageTex,[0 0]);
 mglFlush;

mglDeleteTexture: Delete a texture

purpose: Deletes a texture. This will free up memory for textures that will not be drawn again. Note that when you call mglClose texture memory is freed up. You only need to call this if you are running out of memory and have textures that you do not need to use anymore.
usage: mglDeleteTexture(tex)

argument value
tex The texture created by mglCreateTexture that you want to delete
 mglOpen;
 mglClearScreen
 mglScreenCoordinates
 texture = mglCreateTexture(round(rand(100,100)*255));
 mglBltTexture(texture,[0 0]);
 mglFlush;
 mglDeleteTexture(texture);

Drawing text

mglTextSet: Set parameters for drawing text

purpose: Set text properties for mglText
usage: mglTextSet(fontName,<fontSize>,<fontColor>,<fontVFlip>,<fontHFlip>,<fontRotation>,<fontBold>,<fontItalic>,<fontUnderline>,<fontStrikeThrough>)

argument value
fontName 'fontName' (defaults to 'Times Roman')
fontSize font point size (defaults to 36)
fontColor [r g b] where r, g and b are values between 0 and 1 (defaults to [1 1 1])
fontVFlip 0 = no vertical flip, 1 = vertical flip (defaults to 0)
fontHFlip 0 = no horizontal flip, 1 = horizontal flip (defaults to 0)
fontRotation rotation in degrees (defaults to 0)
fontBold 0 for normal, 1 for bold (defaults to 0)
fontItalic 0 for normal, 1 for italic (defaults to 0)
fontUnderline 0 for normal, 1 for underline (defaults to 0)
fontStrikethrough 0 for normal, 1 for strikethrough (defaults to 0)
mglOpen;
mglVisualAngleCoordinates(57,[16 12]);
mglTextSet('Helvetica',32,[0 0.5 1 1],0,0,0,0,0,0,0);
mglTextDraw('Hello There',[0 0]);
mglFlush;

mglText: Create a texture from a string

purpose: Creates a texture from a string.
usage: tex = mglText('string')

argument value
string The string you want to draw
mglOpen;
mglVisualAngleCoordinates(57,[16 12]);
mglTextSet('Helvetica',32,[0 0.5 1 1],0,0,0,0,0,0,0);
thisText = mglText('hello')
mglBltTexture(thisText,[0 0],'left','top');
mglFlush;

Normally you will only set one output argument which is a texture usable by mglBltTexture. But if you have two output arguments

[tex texMatrix] = mglText('hello');

texMatrix will contain a 2D matlab array that has a rendering of the text (i.e. it will have values from 0-255 that represent the string). You can modify this matrix as you want and then use mglCreateTexture to create it into a texture that can be displayed by mglBltTexture

mglTextDraw: Draws text to screen (simple but slow)

purpose: wrapper around mglText and mglBltTexture to draw some text on the screen. If you need to draw text more quickly, you will have to pre-make the text textures with mglText and then use mglBltTexture when you want it. Otherwise, for non time-critical things this functions should be used.
usage: mglTextDraw(str,pos,<hAlignment>,<vAlignment>)

argument value
str desired string
pos [x y] position on screen
hAlignment -1 = left, 0 = center, 1 = right (defaults to center)
vAlignment -1 = top, 0 = center, 1 = bottom (defaults to center)
mglOpen;
mglVisualAngleCoordinates(57,[16 12]);
mglTextSet('Helvetica',32,[0 0.5 1 1],0,0,0,0,0,0,0);
mglTextDraw('Hello There',[0 0]);
mglFlush;

mglStrokeText: Fast no-frills line-based text drawing (does not use texture memory)

purpose: Draws a stroked fixed-width character or string on MGL display. Default width is 1, default height 1.8 (in current screen coordinates)
usage: [x,y]=mglStrokeText( string, x, y, scalex, scaley, linewidth, color, rotation );

argument value
string text string. Unsupported characters are printed as #
x,y center coordinates of first character (current screen coordinates)
scalex scale factor in x dimension (relative to character) (current screen coordinates). Note that text can be mirrored by setting this factor to a negative value.
scaley scale factor in y dimension (relative to character) (current screen coordinates). Optional, defaults to scalex.
linewidth width of line used to draw text. Default 1.
color text color. Default [1 1 1]
rotation in radians. Default 0.
OUTPUT x,y position after last letter (for subsequent calls) [optional]
mglOpen;
mglVisualAngleCoordinates(57,[16 12]);
mglStrokeText('Hello',0,0);
mglFlush;

Drawing functions

mglClearScreen

purpose: sets the background color
usage: mglClearScreen(<color>)

argument value
color color to set background to, can be a grayscale value or an [r g b] value

set to the level of gray (0-1)

mglClearScreen(gray)

set to the given [r g b]

mglClearScreen([r g b])

full example

mglOpen;
mglClearScreen([0.7 0.2 0.5]);
mglFlush();

mglPoints2: 2D points

purpose: plot 2D points on an OpenGL screen opened with mglOpen. For round dots, use mglGluDisk.
usage: mglPoints2(x,y,size,color)

argument value
x,y position of dots on screen
size size of dots (height of square in pixels)
color color of dots
mglOpen;
mglVisualAngleCoordinates(57,[16 12]);
mglPoints2(16*rand(500,1)-8,12*rand(500,1)-6,2,1);
mglFlush

mglPoints3: 3D points

purpose: plot 3D points on an OpenGL screen opened with mglOpen
usage: mglPoints2(x,y,z,size,color)

argument value
x,y,z position of dots on screen
size size of dots (in pixels)
color color of dots
mglOpen;
mglVisualAngleCoordinates(57,[16 12]);
mglPoints3(16*rand(500,1)-8,12*rand(500,1)-6,zeros(500,1),2,1);
mglFlush


mglLines2: 2D lines

purpose: mex function to plot lines on an OpenGL screen opened with glopen
usage: mglLines(x0, y0, x1, y1,size,color)

argument value
x0,y0 initial position of line
x1,y1 end position of line
size size of line (in pixels)
color color of line
mglOpen
mglVisualAngleCoordinates(57,[16 12]);
mglLines2(-4, -4, 4, 4, 2, [1 0.6 1]);
mglFlush

mglFillOval: Ovals

purpose: draw filled oval(s) centered at x,y with size [xsize ysize] and color [rgb]. the function is vectorized, so if you provide many x/y coordinates (identical) ovals will be plotted at all those locations.
usage: mglFillOval(x,y, size, color)

argument value
x,y center position of oval
size [width height] of oval
color color of oval
mglOpen;
mglVisualAngleCoordinates(57,[16 12]);
x = [-1 -4 -3 0 3 4 1];
y = [-1 -4 -3 0 3 4 1];
sz = [1 1];
mglFillOval(x, y, sz,  [1 0 0]);
mglFlush();

mglFillRect: Rectangles

purpose: draw filled rectangles(s) centered at x,y with size [xsize ysize] and color [rgb]. the function is vectorized, so if you provide many x/y coordinates (identical) ovals will be plotted at all those locations.
usage: [ ] = mglFillRect(x,y, size, color)

argument value
x,y center position of rectangle
size [width height] of rectangle
color color of rectangle
mglOpen;
mglVisualAngleCoordinates(57,[16 12]);
x = [-1 -4 -3 0 3 4 1];
y = [-1 -4 -3 0 3 4 1];
sz = [1 1];
mglFillRect(x, y, sz,  [1 1 0]);
mglFlush();


mglFixationCross: Cross

purpose: draws a fixation cross. With no arguments, draws a fixation cross at origin (default width 0.2 with linewidth 1 in white at [0,0])
usage: mglFixationCross([width], [linewidth], [color], [origin]);
alternate usage: mglFixationCross( params )

argument value
params [width linewidth r g b x y]
width width in degrees of fixation cross
linewidth width in pixels on line
color color of fixation cross
origin center position of fixation (defaults to [0 0])
mglOpen;
mglVisualAngleCoordinates(57,[16 12]);
mglFixationCross;
mglFlush;

mglGluAnnulus: Annuli, rings

purpose: for annuli and rings, e.g. for retinotopic stimuli. The function is vectorized, such that multiple annuli can be rendered in one call. In this case, x,y, isize, and osize need to have the same number of elements. Color is also vectorized.
usage: [ ] = mglGluAnnulus( x, y, isize, osize, color, [nslices], [nloops] )

argument value
x,y position of circle from which annulus/annuli is/are derived
isize inner radius/radii
osize outer radius/radii
color color of annuli, either [], 3-vector, or 3-row by n-column matrix
nslices number of wedges used in polygon→circle approximation [default 8]
nloops number of annuli used in polygon→circle approximation [default 1]
 mglOpen(0);
 mglVisualAngleCoordinates(57,[16 12]);
 x = zeros(4, 1);
 y = zeros(4, 1);
 isize = linspace(1, 8, 4);
 osize = isize+linspace(0.1, 2, 4);
 colors = jet(4)';
 mglGluAnnulus(x, y, isize, osize,colors , 60, 2);
 mglFlush();


mglGluDisk: Circular dots

purpose: for plotting circular (rather than square dots), use this function. on slower machines, large number of dots may lead to dropped frames. there may be a way to speed this up a bit in future.
usage: [ ] = mglGluDisk( x, y, size, color, [nslices], [nloops] )

argument value
x,y position of dots
size size of dots (radius)
color color of dots
nslices number of wedges used in polygon→circle approximation [default 8]
nloops number of annuli used in polygon→circle approximation [default 1]
mglOpen;
mglVisualAngleCoordinates(57,[16 12]);
x = 16*rand(100,1)-8;
y = 12*rand(100,1)-6;
mglGluDisk(x, y, 0.1,  [0.1 0.6 1], 24, 2);
mglFlush();


mglGluPartialDisk: Segments, wedges

purpose: for segments and wedges, e.g. for retinotopic stimuli. The function is vectorized, such that multiple segments can be rendered in one call. In this case, x,y, isize, osize, startAngles, and sweepAngles need to have the same number of elements. Color is also vectorized (see mglGluAnnulus and the example below).
usage: [ ] = mglGluPartialDisk( x, y, isize, osize, startAngles, sweepAngles, color, [nslices], [nloops] )

argument value
x,y center position of circle from which segment is derived
isize innter radius of segment
osize outer radius of segment
startAngles angle at which segment(s) start
sweepAngles angle each segment(s) sweep(s) out
color color of segment
nslices number of wedges used in polygon→circle approximation [default 8]
nloops number of annuli used in polygon→circle approximation [default 2]
mglOpen(0);
mglVisualAngleCoordinates(57,[16 12]);
x = zeros(10, 1);
y = zeros(10, 1);
isize = linspace(1, 5, 10);
osize = 3+isize;
startAngles = linspace(0,180, 10)
sweepAngles = ones(1,10).*10;
colors = jet(10)';
mglGluPartialDisk(x, y, isize, osize, startAngles, sweepAngles, colors, 60, 2);
mglFlush();

mglPolygon: Polygons

purpose: mex function to draw a polygon in an OpenGL screen opened with mglOpen. x and y can be vectors (the polygon will be closed)
usage: mglPolygon(x, y, [color])

argument value
x,y position of vertices
color color of polygon
mglOpen;
mglVisualAngleCoordinates(57,[16 12]);
x = [-5 -6 -3  4 5];
y = [ 5  1 -4 -2 3];
mglPolygon(x, y, [1 0 0]);
mglFlush();

mglQuads: Quads

usage: mglQuad( vX, vY, rgbColor, [antiAliasFlag] );
purpose: mex function to draw a quad in an OpenGL screen opened with mglOpen

argument value
vX 4 row by N column matrix of 'X' coordinates
vY 4 row by N column matrix of 'Y' coordinates
rgbColors 3 row by N column of r-g-b specifing the color of each quad
antiAliasFlag turns on antialiasing to smooth the edges


mglOpen;
mglScreenCoordinates
mglQuad([100; 600; 600; 100], [100; 200; 600; 100], [1; 1; 1], 1);
mglFlush();

Gamma tables

mglSetGammaTable: Sets the display card gamma table

purpose: Set the gamma table
usage: There are a number of ways of calling this function explained below.

Setting a redMin, redMax, redGamma, greenMin, etc.

mglSetGammaTable(0,1,0.8,0,1,0.9,0,1,0.75);

or with a vector of length 9:

mglSetGammaTable([0 1 0.8 0 1 0.9 0 1 0.75]);

or set with a single table for all there colors. Note that the table values go from 0 to 1 (i.e. 0 is the darkest value and 1 is the brightest value). If you have a 10 bit gamma table (most cards do–see section on monitor calibration for a list), then the intermediate values will be interpreted with 10 bits of resolution.

gammaTable = ((0:1/255:1).^0.8)';
mglSetGammaTable(gammaTable);

or set all three colors with differnet tables

redGammaTable = (0:1/255:1).^0.8;
greenGammaTable = (0:1/255:1).^0.9;
blueGammaTable = (0:1/255:1).^0.75;
mglSetGammaTable(redGammaTable,greenGammaTable,blueGammaTable);

can also be called with an nx3 table

gammaTable(:,1) = (0:1/255:1).^0.8;
gammaTable(:,2) = (0:1/255:1).^0.9;
gammaTable(:,3) = (0:1/255:1).^0.75;
mglSetGammaTable(gammaTable);

can also be called with the structure returned by mglGetGammaTable

mglSetGammaTable(mglGetGammaTable);

Note that the gamma table will be restored to the original after mglClose.

Timing. The setting of the gamma table is done by the OS in a way that seems to be asynchronous with mglFlush. For instance, the following code gives unexpected results:

  mglOpen(1);
  mglClearScreen(1); % set back buffer to white
  mglWaitSecs(2);
 
  % now set the gamma table to all black, this should insure that nothing will be displayed
  mglSetGammaTable(zeros(1,256)); 
  mglFlush; 
  % now the flush will bring the value 255, set by the mglClearScreen above,
  % to the front buffer, but because the gamma table is set to black,
  % nothing should be displayed
 
  mglWaitSecs(2);
  mglClose;

This should keep the screen black, but on my machine, the screen temporarily flashes white. Presumably this is because the mglSetGammaTable happens after the mglFlush. It is recommended that you change the gamma while there is nothing displayed on the screen and wait for at least one screen refresh before assuming that the gamma table has actually changed.

mglGetGammaTable: Gets the current gamma table

purpose: returns what the gamma table is set to usage: table = mglGetGammaTable()

mglOpen;
gammaTable = mglGetGammaTable

Stencils to control drawing only to specific parts of screen

Here is a demonstration of how to use stencils using these these functions:

mglOpen;
mglScreenCoordinates;
%Draw an oval stencil
mglStencilCreateBegin(1);
mglFillOval(300,400,[100 100]);
mglStencilCreateEnd;
mglClearScreen;
% now draw some dots, masked by the oval stencil
mglStencilSelect(1);
mglPoints2(rand(1,5000)*500,rand(1,5000)*500);
mglFlush;
mglStencilSelect(0);

mglStencilCreateBegin: Start drawing a stencil

purpose: Begin drawing to stencil. Until mglStencilCreateEnd is called, all drawing operations will also draw to the stencil. Check MGL.stencilBits to see how many stencil planes there are. If invert is set to one, then the inverse stencil is made
usage: mglStencilCreateBegin(stencilNumber,invert)

argument value
stencilNumber stencil number, usualy 1-8 but look at the global variable MGL.stencilBits to see how many stencil planes there are.
invert 1 or 0 to invert the stencil that is made

see example above.

mglStencilCreateEnd: End drawing a stencil

purpose: Ends drawing to stencil usage: mglStencilCreateEnd

see example above.

mglStencilSelect: Select a stencil

purpose: Sets which stencil to use, 0 for no stencil usage: mglStencilSelect(stencilNumber)

argument value
stencilNumber number of stencil to use

See example above.

Keyboard and mouse functions

mglDisplayCursor: Hide or display the mouse cursor

purpose: Hide or display the mouse cursor
usage: mglDisplayCursor(<display>)

argument value
display 1 or 0 to display or hide the cursor

When you call mglOpen the mouse cursor is hidden by default. You can get it to come back by doing:

mglOpen
mglDisplayCursor

mglGetKeys: Get keyboard state

purpose: returns the status of the keyboard (regardless of whether the focus is on the mgl window)
usage: mglGetKeys(<keys>)

argument value
keys array of keycodes. In this case mglGetKeys will only return thestatus of those keys, for example: mglGetKeys([61 46]) will return the values of key 61 and 46.

mglGetMouse: Get mouse state

usage: mglGetMouse()
purpose: returns the status of the mouse buttons (regardless of whether the focuse is on the mgl window

mglGetKeyEvent: Get a key down event off of queue

purpose: returns a key down event waitTicks specifies how long to wait for a key press event in seconds. Note that the timing precision is system-dependent:

The default wait time is 0, which will return immediately and if no keypress event is found, will return an empty array []. The return structure contains the character (ASCII) code of the pressed key, the system-specific keycode, a keyboard identifier (on Linux, this is the keyboard state, or modifier field), and and the time (in secs) of the key press event. NOTE that to get a key event the focus *MUST* be on the mgl window. For faster timing, try mglGetKeys
usage: mglGetKeyEvent(waitTicks)

argument value
waitTicks Ticks to wait for before giving up and returning empty event
mglOpen
mglGetKeyEvent(0.5)

mglGetMouseEvent: Get a mouse button down event off of queue

usage: mglGetMouseEvent(waitTicks)
purpose: returns a mouse down event waitTicks specifies how long to wait for a mouse event in seconds. Note that the timing precision is system-dependent:

The default wait time is 0, which will return immediately with the mouse position regardless of button state. The return structure contains the x,y coordinates of the mouse, the button identifier if pressed (on the button-challenged Mac this is always 1) and 0 otherwise, and the time (in secs) of the mouse event. NOTE that the mouse down event has to be *ON* the mgl window for this to work with waitTicks not equal to 0

argument value
waitTicks Ticks to wait for before giving up and returning empty event
mglOpen
mglGetMouseEvent(0.5)

mglCharToKeycode: Returns keycode of char

Purpose: Returns the keycodes of a (list of) keynames
Usage: keycode=mglCharToKeycode(keyname)

Note on special keys: On Linux (X), special keys and function keys have unique names, e.g., 'Escape', 'F1', etc., so obtaining the keycodes for these is done by mglCharToKeycode({'Escape','F1'}) etc. On Macs, this is not possible; instead, test for the keycode and name of a key using the mglShowKey function.

The keycodes match those used by mglGetKeys and mglGetKeyEvent

argument value
keyname cell array where each entry is a key name string e.g. keyname = {'h','g' '1'}
OUTPUT:keycode vector of integer keycodes for each keyname entry e.g. for the above example, keyname=[44 43 11] (on Linux)

Example: testing for specific keypresses:

keycodes=mglCharToKeycode({'1','2' '3'}) % keys 1-3 on main keyboard
while (1); k=mglGetKeys(keycodes); if (any(k)),break;end;end

Technical note: the returned keycodes are identical to system keycodes+1

mglKeycodeToChar: Returns char of keycode

Purpose: Returns the keynames of a (list of) keycodes
Usage: keyname=mglKeycodeToChar(keycode)

Note on special keys: This repeats the above entry, deleted.

argument value
keycode vector of integer keycodes for each keyname entry for example, keyname=[44 43 11]
OUTPUT:keyname cell array where each entry is a key name string for the above example on linux keyname = {'h','g' '1'}

Example: testing which keys were pressed:

while (1); k=mglGetKeys; if (any(k)),break;end;end
keycodes=find(k);
keynames=mglKeycodeToChar(keycodes)

Technical note: keycodes are identical to system keycodes+1

mglSimulateRun: Generates simulated key presses

availability: mgl 2.0 Mac OS X only
usage: mglSimulateRun(framePeriod,numFrames,<startDelay>,<char>)
purpose: This function simulates a run (currently only available on Mac) it does this by posting periodic backtick keypresses using mglPostEvent. For example if you want to have 375 backticks occur spaced 1.5 seconds apart:

mglSimulateRun(1.5,375);

Also, you can delay the start of the backticks, by doing

mglSimulateRun(1.5,375,10);

Instead of sending the backtick character, send the character 'a'

           
mglSimulateRun(1.5,375,0,'a');

Note that if you want to stop the keyboard events in the middle, you need to do:

mglPostEvent('quit');

mglEatKeys: Keep key presses from entering buffer

availability: mgl 2.0 Mac OS X only
usage: mglEatKeys(keyCodes)
purpose: Starts eating keypresses (i.e. the sent in keycodes will no longer be sent to the terminal window. This can be useful if you don't want to fill your text buffer with extraneous keyboard backticks and subject responses). keyCodes can also be a char array or the myscreen variable (if the myscreen variable it will eat all keys that initScreen is using for backticks and response keys). Note that if you press any key that is not being eaten, then key eating will stop. Usually, you do not need to call this function directly, but you have initScreen call it, by setting eatKeys in mglEditScreenParams.

mglListener: Background process that reads mouse and keyboard events

availability: mgl 2.0 Mac OS X only
usage: mglListener('init')
purpose: Starts a background thread that monitors keyboard and mouse events. What this does is it uses event taps to record keyboard and mouse events. Event taps are a fairly low-level systems call in Mac OS X which is used by assistive devices (which is why you have to enable access for assistive devices. The time stamps are set by the operating system which gives nominally nanosecond precision. The precision ultimately depends on how the operating system polls devices. But, this is likely to be fairly accurate and our experience with the time stamps has not shown any problems. Could in principle event time stamps get slowed down by other process - yes, but this is unlikely to be a major problem on todays mult-core systems in which the thread running by the kernel to pick up key and mouse events is likely run at a very low level with high priority and probably in a dedicated processor unless you have too many things running. Normally, you don't have to call this function yourself. It is called by the other functions that get key events (or run by the task code). To run yourself, you call it with different commands:

command value
init starts the process running
getKeyEvent Returns a keyboard down event
getMouseEvent Returns a mouse down event
getKeys Returns an array like mglGetKeys except that the time each key that is currently down was initially pressed is returned.
getAllKeyEvents Returns a structure with when/keyCode for all pending keyEvents
getAllMouseEvents Same as getAllKeyEvents but for mouse events
eatKeys specify a string or an array of keyCodes (as arg1) for keyboard events that will not pass through to the application window. The events will still be available from 'getKeyEvent'. This is useful, if you don't want a lot of backtick key presses going into your command window for instance
quit Quits the listener, after this you won't be able to run other commands

mglGetSecs: Get time in seconds

purpose: Get current or elapsed time
usage: mglGetSecs(<t0>)

argument value
t0 start time from which to compute elapsed time

To get current time

t=mglGetSecs

Get elapsed time since t0

t0 = mglGetSecs;
elapsedTime=mglGetSecs(t0)

mglWaitSecs: Wait for a time in seconds

purpose: Wait for some time
usage: mglWaitSecs(waitTime)

argument value
waitTime time to wait for in seconds

Wait 3.3 seconds:

mglWaitSecs(3.3);

Sound functions

mglInstallSound: Install an .aiff file for playing with mglPlaySound

purpose: Install an .aiff file for playing with mglPlaySound
usage: mglInstallSound(soundName)

argument value
soundName aiff filename

This will install sounds to be played using mglPlaySound. Note that if you just want to use systems sounds then you do not need to call this function directly, it will be called by mglOpen to install all your system sounds. Once the sound is installed you can play it with mglPlaySound

soundNum = mglInstallSound('/System/Library/Sounds/Submarine.aiff');
mglPlaySound(soundNum);

With no arguments, mglInstallSound uninstalls all sounds

mglInstallSound

Version 2.0

You can also specify a directory of sound files to install:

mgInstallSound('soundDir');

which will install all sounds named *.aif or *.aiff. You can play the sounds by specifying the name of the file (without the file extension):

mglPlaySound('soundFileName');

mglPlaySound: Play a system sound

purpose: Play a sound
usage: mglPlaySound(soundNum)

argument value
soundNum number of sound

Plays a system sound. After calling mglOpen, all of the system sounds will be installed and you can play a specific one as follows:

mglOpen;
global MGL;
mglPlaySound(find(strcmp(MGL.soundNames,'Submarine')));

With no arguments mglPlaySound plays the system alert sound

mglPlaySound

Note that this function returns right after it starts playing the sound (it does not wait until the sound finishes playing).

mglMovie

availability: mgl 2.0 Mac OS X 64-bit Matlab only
usage: movieStruct = mglMovie(filename,position) or mglMovie(movieStruct,command,<argument>);
purpose: Used to display quicktime movies. (This *only* works on 64 bit Mac. There is some issue with the quicktime library QTKit and threads which does not seem to be a problem on 64 bit). You also need to be using a cocoa window, so make sure to set mglSetParam('movieMode',1) before running mglOpen. Check the sample experiment, taskTemplateMovie for a working example.

Also, note that the movies will play in front of the openGL buffer. Thus you can't draw on top of the movie and mglFrameGrab won't grab the movie frame – you can grab movie frames with mglMovie(m,'getFrame');

To init the movie, you open with a filename, and an optional position array [x y width height] and save the returned structure.

mglSetParam('movieMode',1);

mglOpen;
m = mglMovie('movie.mov');

Then you can run commands on the movie:

mglMovie(m,'play');

If no position is specified, the movie will be made to fill the display. Then pass in the structure with any of the following commands:

command purpose
0:'close“ Close the movie. After you run this, you will no longer be able to play the movieStruct again since the memory will have been released.
1:'play' Play the movie
2:'pause' Pause the movie
3:'gotoBeginning' Goto the beginning of the movie
4:'gotoEnd' Goto the end of the movie
5:'stepForward' Step one frame forward in the movie
6:'stepBackward' Step one frame backward in the movie
7:'hide' Hide the movie. This does not close the movie, so you will be able to show the movieStruct again by using show.
8:'show' Show the movie after it has been hidden
9:'getDuration' Get a string that represents the length of the movie
10:'getCurrentTime' Gets the current time of the movie
11:'setCurrentTime' Sets the current time of the movie to the string\passed in. Make sure the string is one returned from getCurrentTime
12:'getFrame' Returns a nxmx3 matrix containing RGB data for current frame

Here is a sample quicktime movie that should work with mglMovie: bosque.mov

Digital I/O

Mgl has some functions to handle digital I/O with a National Instruments card (e.g. NI USB 6501) that can be used to read and write digital I/O signals and sine wave analog output. These can be useful to synch to an MR scanner or control an external eye tracker. These functions all live in the directory:

mgl/utils/readDigPort

These need to be compiled specially, in mgl 2.0 by running (earlier versions, just go mex by hand):

mglMake('digio');

You will need to have downloaded the National Instruments drivers (see next section) to compile and use these functions.

Note that these functions are now compatible with both 32-bit and 64-bit Matlab even though the NI software (NI-DAQmx Base is not actually 64-bit compliant on Mac). See here for more details.

How to set up a National Instruments card

You can use a National Instruments card for digital I/O with mgl by doing the following:

  1. Download NI-DAQmx Base. You should use the latest version, as of this writing 3.7.0. You may need to make a free account with NI.
  2. Make sure that the device (e.g. NI USB-6501 or NI USB-6211) has up-to-date firmware, by running FWUpdate (included in Ni-DAQmx Base/bin)
  3. Restart matlab if you have already run readDigPort (the program has to reinit the driver). Also, note that it is best to run matlab without the desktop (matlab -nodesktop) so that you can see all the console messages described below.
  4. Documentation is installed and should be available from the following location
    file:///Applications/National%20Instruments/NI-DAQmx%20Base/documentation/docsource/daqmxbasecfuncindex.htm
  5. Follow the instructions above to compile mglDigIO and related functions.
  6. You should connect your input and output digital lines to the proper port and lines on your NI card that you want. Note that by default mglDigIO is set up to read from port 2 and write to port 1 of your NI card (but you can configure this when you run the mglDigiO('init') command. You should reference your manual for the pinouts on the board you use. For the NI-6501 see here. We usually connect one digital input to: Pin 1 (black, digital ground) Pin 16 (red, port 2 input bit 0) and one digital output to: Pin 32 (black, digital ground) and Pin 27 (red, port 1 output bit 0).
  7. Run mglDigIO('init') and you should see the following:
    >> mglDigIO('init')
    No matching processes belonging to you were found
    (mglStandaloneDigIO) Initializing NI device with digin port: Dev1/port2 digout port: Dev1/port1. End with mglDigIO('quit').
    (mglStandaloneDigIO) Successfully initialized NI device
    (mglStandaloneDigIO) New connection made: 0
    (mglPrivateDigIO) DigIO is running.
  8. You should be able to read digin events using the digin command (note the first time it will report if any of the lines are high, but not report anything for ports that are low).
    >> mglDigIO('digin')
    
    ans = 
    
        type: [1 1 1 1 1 1 1 1]
        line: [0 1 2 3 4 5 6 7]
        when: [934383.731613 934383.731624 934383.731625 934383.731626 934383.731626 934383.731628 934383.731629 934383.731629]
    

You can set up to read digial pulses for the task code in mgl 2.0 by setting digin using mglEditScreenParams.

mglDigIO

availability: mgl 2.0 Mac OS X only
usage: mglDigIO(command,<arg1>,<arg2>)
purpose: This is a mac specific command that is used to control a NI digital IO board. It has been tested with the NI USB 6501. It runs as a thread (on 64-bit runs a separate process and communicates via a socket: see below) that reads digital port 2 and logs any change in state (either up or down). It can also be used to set digital lines on port 1 at a time of your choosing. It is used by the task code if you set mglEditScreenParams to use digin. It is the preferred way of dealing with digital I/O since it keeps excellent timing. Note that if you are trying to read events up to about 250Hz (e.g. a square wave of 250Hz), you should be able to read all events without fail. Faster than that at around 500Hz you will likely start dropping events (this is likely due to how fast the NI-DAQ mxBase driver can pool the device). To use this function, you will need to compile it using mglMake('digin');

Here are thte commands it accepts:

command purpose
1:'init' Init the digIO thread. You need to run this before anything else will work. You can optional specify input and output ports which default to 1 and 2 respectively: mglDigIO('init',inputPortNum,outputPortNum); You can also specify the device number using: mglDigIO('init',inputPortNum,outputPortNum,inputDevnum,outputDevnum). You can call init with different port numbers to reset what ports you want to listen/write to/from without calling quit inbetween.
2:'digin' Returns all changes on the input digital port
3:'digout' Set the output digital port a time of your choosing. This takes 2 other values. The time in seconds that you want the digital port to be set. And the value you want it to be set too. Time can be either an absolute time returned by mglGetSecs or it can be relative to now if it is a negative value: mglDigIO('digout',-5,0) → Sets the output port to 0 five secs from now.
4:'list' Lists status and all pending digout events
5:'ao' Sets the output port to produce a sine wave and then return to 0, you call it with parameters time (like digout above), channel (0 or 1 for A0 or A1), frequency,amplitude (volts peak - it will produce a sine wave that goes from -amplitude to amplitude) and duration in seconds. For example: mglDigIO('ao',-1,0,500,2.5,1);You can optionally set the sample rate (default is 250000 samples/second): mglDigIO('ao',-1,0,500,2.5,1,100000); And you can specify the device number (below will use the default sample rate, and use dev2/ao0): mglDigIO('ao',-1,0,500,2.5,1,[],2);For more info see below
0:'quit' Closes the nidaq ports, after this you won't be able to run other commands. Note that this does not shutdown the digIO thread. The reason for this is that the NIDAQ library is not thread safe, so you can only call its functions from one thread, so to be able to keep starting and stopping reading from the card, the thread is set to continue to run, and quit simply shuts down the nidaq tasks and stops logging events. After you call quit, you can use init again to restart reading/writing. If you need to shutdown the thread, use 'shutdown'
-1:'shutdown' Quits the digIO thread if it is running, after this you won't be able to run other commands. If you plan on starting and stopping digIO collection, you should use init and quit rather than shutdown

Analog output using mglDigIO

mglDigIO supports limited analog output functionality. We have used this with NI USB-6211 which has two analog output ports that can run at 250kHz. At this moment, all that is supported is the writing of a sine wave of specified amplitude and frequency. To set it up, you need to connect the NI USB-6211 output pins correctly. Follow the information in the Pinout diagram for your device. For analog output, we connect to Ground Pin 14 (black) and AO 0 Pin 12 (red) for the Analog output 0, and for analog output 1 we use the same ground pin and AO 1 Pin 13 (red). We also connect digital output on Pin 5 Digital Ground (black) and Pin 6 (red) to get output on Port 1 bit 0. For digital input, we use Pin 5 (black, ground) and Pin 1 (red). Note that to use the input, you will need to set the correct port number (since our default is to use port 2, and for this device input is on port 0).

So, to initialize mglDigIO you would do (note port settings for input on port 0 - default is port 2):

mglDigIO('init',0);

Then, for example, to output a sine wave 4 seconds from now which goes from -2 to 2 volts at 500HZ for 3 seconds on analog output port 0, you would do:

mglDigIO('ao',-4,0,500,2,3);

Note the following limitation. You must start one event at least 25 ms after the last event even if they are on different output channels. That is, you cannot have staggered output going at the same time on different channels. (the 25 ms is to give enough set up time to so set the new output state). So, you can show a sine wave on one channel followed by a sine wave on another channel like this:

mglDigIO('ao',-4,0,500,2,3);
mglDigIO('ao',-8,1,250,5,3);

But, the following will give an error and not run the second sine wave:

% This will not work!!!
mglDigIO('ao',-4,0,500,2,3);
mglDigIO('ao',-7,1,250,5,3);

However, you can produce a sine wave of the same frequency on both output channels at the same time for the same duration with different amplitudes. The following will produce a sine wave in 4 seconds at 500Hz for a duration of 3 seconds, with the sine wave going from -2 to 2 volts on analog output channel 0 and -4 to 4 volts on analog output channel 1:

mglDigIO('ao',-4,[0 1],500,[2 4],3);

But, for the time being you can't have different frequencies, different starting times or different durations. The reason for this is because of the NI-DAQmx Base API. It does not seem to allow only setting one analog output task at a time and gives me an error if I try to load the sine wave data for a second independently created output task even if it is on a different channel. In principle we could workaround this by loading onto the NI card the whole buffer of what we want to present for each channel (like having one start with 0 and then the sine wave and the other one having a different delay followed by a different sine wave). However, this is not what we have implemented now. Right now, we just load one cycle of a sine wave on to the card and then have the card repeat that over again for the desired duration. This is why we cannot have different frequencies (they would require buffers of different lengths).

You can also change the sampling frequency. This might be useful if you do not need a lot of time resolution on the signal produced since it requires less memory transfer on to the card:

mglDigIO('ao',-4,0,500,2,3,100000);

You can also specify a different device (e.g. dev2/ao0 would be the 2nd NI device connected to your computer - not sure how you are supposed to know which device is which). This could be useful, for example, if you want to have one device for digital io and another for analog:

mglDigIO('init');
mglDigIO('ao',-4,0,500,2,3,100000,2);
mglDigIO('digout',-1,255);

The above should initialize a digital input/output for Dev1 and analog output on Dev2.

writeDigPort

usage: writeDigPort(portNum,val);
purpose: write an ouput to the National Instruments board. portNum defaults to 2, to write from Dev1/port2. The first time you read it needs to open the port to the NI device which can take some time. Subsequent calls will be faster. Note that you can only open one port at a time, so if you need to read from two different ports it will always be closing and reopening the ports which will cause a performance hit (consider rewriting the code to keep multiple ports open if you need this). Also, if you want to switch between reading and writing on a single port, you will need to manually close the port in between read/write calls by setting portNum = -1 (see below).

portNum can also be set to:

-1 closes any open port
-2 displays which port (if any) is open.

Note that in the distribution, writeDigPort is not compiled. It always returns 0. To use it to read your NI card, you will need to mex readDigPort.c, this requires you to install the NI-DAQmx Base Frameworks.

readDigPort

usage: readDigPort(<portNum>)
purpose: read the National Instruments board digital input. portNum defaults to 1, to read from Dev1/port1. The first time you read it needs to open the port to the NI device which can take some time. Subsequent calls will be faster. Note that you can only open one port at a time, so if you need to read from two different ports it will always be closing and reopening the ports which will cause a performance hit (consider rewriting the code to keep multiple ports open if you need this). Also, if you want to switch between reading and writing on a single port, you will need to manually close the port in between read/write calls by setting portNum = -1 (see below).

portNum can also be set to:

-1 closes any open port
-2 displays which port (if any) is open.

Note that: in the distribution, readDigPort is not compiled. It always returns 0. To use it to read your NI card, you will need to mex readDigPort.c, this requires you to install the NI-DAQmx Base Frameworks.

64-bit

The NI-DAQmx Base library is currently (4/20/2013) available only for 32-bit (Note that version 3.6 will run on a 64 bit platform, but you can't create 64 applications). To get around this on 64-bit Matlab, we run a separate function called mglStandaloneDigIO and communicate with that function through a socket. This is all done in the background for you so there is nothing that you have to do if you use mglDigIO.

The way it works is as follows. When you init, by doing mglDigIO('init') the function mglStandaloneDigIO is started. This function will connect to the NI card and answer commands through a socket (named .mglDigIO in your home directory). The function mglDigIO when called to get digin events or set digout events connects through the socket and communicates with mglStandaloneDigIO. This function will continue to run until you quit matlab or run mglDigIO('shutdown'). If you need to shut this function down from outside matlab, you can do from a command line:

killall mglStandaloneDigIO

Measurement Computing USB-7204

Measurement Computing makes a USB based digital I/O device with a 64 bit library for Mac which potentially could be a replacement for National Instruments which does not have a 64-bit library. However, we found that the driver needs to have c# support through the Mono Framework and were not able to find a way to make this compatible with Matlab through a Mex file. For reference, the driver can be downloaded from here.

The hardware manual is: hardware manual The software manual is: software manual.

EyeLink Eyetracker functions

Overview

We have functions to interface Matlab with the EyeLink scanner. These functions allow you to calibrate the scanner and get current eye position information, etc. These are all referenced below. We have included 64-bit binaries based on a beta-version of Eyelink's frameworks.

To run the eyelink commands, you will need to have the Eyelink Frameworks installed. If they are not installed, you may see an error like this:

>> mglEyelinkOpen
Invalid MEX-file '/Users/justin/proj/mgl/mgllib/mglEyelink/mglPrivateEyelinkOpen.mexmaci64':
dlopen(/Users/justin/proj/mgl/mgllib/mglEyelink/mglPrivateEyelinkOpen.mexmaci64, 1): Library not loaded:
@executable_path/../Frameworks/edfapi.framework/Versions/A/edfapi
  Referenced from: /Users/justin/proj/mgl/mgllib/mglEyelink/mglPrivateEyelinkOpen.mexmaci64
  Reason: image not found

The Eyelink libaries can be downloaded from the Eyelink CD - or, if you are using 64-bit, follow instructions here to get the beta. The ones you will need are called eyelink_core.framework (used for interfacing with the eye tracker) and edfapi.framework (used for reading the edf files that the tracker makes). These frameworks will appear as directories in Library/Frameworks (either in the root directory or your home directory). You also need the SDL frameworks (SDL.framework, SDL_ttf.framework, SDL_mixer.framework, SDL_image.framework, SDL_gfx.framework). There is also an eyelink_core_graphics.framework, but this framework should not be necessary. Once you have successfully installed the Frameworks, you should also be able to compile the Eyelink code (this is only necessary to do if you are using 32-bit Matlab or some other platform for which the code does not work):

mglMake('Eyelink')

Follow the Eyelink instructions for setting up your Eyelink computer and connecting to it. But, essentially, you need to do at least the following:

  1. Connect the Eyelink computer to your stimulus computer with an Ethernet cross cable (should be provided by SR Research).
  2. Set your Network settings correctly. Usually you will have plugged the Eyelink computer into the second network adaptor. So you will then need to go to System Preferences/Network and set “Ethernet 2” to the following
    Configure IPv4: Manually
    IP Address: 100.1.1.2
    Subnet Mask: 255.255.255.0
    Router, DNS Server and Search Domains are irrelevant (I think)
  3. Boot up the Eyelink computer.
  4. Run the command mglEyelinkOpen in matlab on the stimulus computer. You should see the green bar in the top right hand corner of the Eyelink computer change to say: TCP/IP Link Open
  5. Run the command mglEyelinkClose and it should change back to: Link Closed

If the system does not find a connected Eyelink computer, you will likely see an error like this:

>> mglEyelinkOpen
displayApi: Socket BIND failed: -1 48 port 4000
displayAPI: /Users/sugy/dev/displayAPI/macdispapi/../common/w32_link.c 564 failed to create socket
Cannot initialize link: Check network and TCP/IP setup(mglPrivateEyelinkOpen) Connection type is 0
Connection failed: could not establish a link.

If you use the mgl task code, you do not need to call any of the following functions explicitly.

mglEyelinkEDFRead

purpose: Reads an Eyelink file into matlab
usage: mglEyelinkEDFRead(filename,<verbose>)

argumentvalue
filenameName of EDF file you want to read
verboseSet to 1 to display verbose information, defaults to 0

mglEyelinkOpen

purpose: Opens a TCP/IP link between matlab and the EyeLink eyetracker.
usage: mglEyelinkOpen(ip,conntype);

argument value
ip The IP address of the eyewink eye tracker, defaults to 100.1.1.1
conntype 0, open up a direct link, 1 initializes a dummy connection useful for debugging.
% open the link
% calls mglPrivateEyelinkOpen, default ip is '100.1.1.1', default conntype is 0
try
  mglEyelinkOpen('100.1.1.1', 0);
catch err
  mglEyelinkOpen('100.1.1.1', 1);
  disp(sprintf('(mglEyelinkOpen) Establishing a dummy connection with the EyeLink'));
end

mglEyelinkCMDPrintF

purpose: Sends a command to the eyetracker
usage: mglEyelinkCMDPrintF('an EyeLink command');

argument value
a string anything recognized by EyeLink (see the manual)
% set up some variables
mglEyelinkCMDPrintF('screen_pixel_coords = 0 0 %d %d', mglGetParam('screenWidth'), mglGetParam('screenHeight')); 
mglEyelinkCMDPrintF('calibration_type = HV9');
mglEyelinkCMDPrintF('file_event_filter = RIGHT,FIXATION,SACCADE,BLINK,MESSAGE,BUTTON'); 
mglEyelinkCMDPrintF('file_sample_data = RIGHT,GAZE,AREA,GAZERES,STATUS'); 
mglEyelinkCMDPrintF('sample_rate = 500');

mglEyelinkSetup

This function switches the Eyelink Software into the 'setup' state where you can adjust thresholds, change settings and run the calibration routine. You can use any of the keyboard commands that are available from the Eyelink software (on the Eyelink computer keyboard). This function returns when you exit the Eyelink setup state (via the ESC key), usually after you have successfully calibrated or validated. The keys you can use are listed below, for more information refer to the Eyelink Users Manual.

The basic keys are

Setup Screen

All Keyboard Commands

Setup Screen
Video Overlay Only
Calibration Screen
During Calibration
After Calibration
Validation Screen
During Validation
Validation Result
After Validation

mglEyelinkEDFOpen

purpose: Open up a new datafile to store the eye data
usage: mglEyelinkEDFOpen(filename)

mglEyelinkRecordingStart

purpose: Tell the eyetracker to start recording samples
usage: mglEyelinkRecordingStart(startvector)

argument value
1 0 0 0 edf-sample (record eye samples in the current eyelink .edf file)
0 1 0 0 edf-event (record events in the current eyelink .edf file)
0 0 1 0 link-sample (send eye position samples back to matlab)
0 0 0 1 link-event (send events back to matlab)

Pass in either a vector for recording state: e.g. [1 0 0 0] where the elements are [file-sample file-event link-sample link-event] or up to four string arguments that set the recording state

mglEyelinkRecordingStart('file-sample','file-event',
                         'link-sample','link-event');

mglEyelinkEDFPrintF

purpose: Insert a message into the recorded datastream
usage: mglEyelinkEDFPrintF(message)

argument value
meessage text string that you want to insert

mglEyelinkGetCurrentEyePos

purpose: Get the X and Y coordinate of the current eye position
usage: pos = mglEyelinkGetCurrentEyePos()

argument value
pos returns the eye position in the current device coordinates - e.g. visual angle if visual angle coordinates are set.

mglEyelinkClose

purpose: Close the link between matlab and the eyetracker
usage: mglEyelinkClose

Test/Demo programs

Run these test programs without any parameters and they should display on your second monitor. With an optional single argument you can pass the number of the display you want to display on.

A quick overview

The task structure can be used to help code experiments, it is completely separate from the basic mgl libarary that is used to display to the screen (in that you do not have to use the task code to use the basic mgl functions).

The structure for these experiments involves three main variables:

myscreen: Holds information about the screen parameters like resolution, etc.
task: Holds info about the block/trial/segment structure of the experiment
stimulus: Holds structures associated with the stimulus.

To create and run an experiment, your program will do the following:

  1. Initialize the screen.
  2. Set up the task structure. The task structure holds information about the parameters you want to randomize over and the timing of your experiment.
  3. Initialize the stimulus. Here you will create all the necessary bitmaps or display structures that you will need to display your stimulus.
  4. Create callback functions. These functions will run at various times in the experiment like at the beginning of a trial or when the subject responds with a keypress or before each display refresh. They are the main way that you program how your stimulus will display and what to do when you get subject responses etc.
  5. Create a display loop. This is the part that actually runs your experiment. Essentially all you have to do is call updateTask which handles all the hard work of running your task.

The basic idea of how to set up your experiment with these structures requires defining some terms. Going from the largest organization down to the smallest:

A simple example experiment can be found in mgl/task:

testExperiment

testExperiment

The code for textExperiment is a good starting place for creating a new experiment since it contains all the essential elements for using these functions.

Let's start by briefly going through each one of the steps above in reference to the function testExperiment. Note that when you actually want to program your own task, you can either start by editing testExperiment.m or use the function taskTemplate.m (be sure to copy these to a new name). taskTemplate.m is an even more stripped down version of testExperiment.m that contains only the necessary essentials to start using the code (and everywhere there is a comment that begins with fix: you will need to make changes to customize for your experiment). There are also some more templates that can be used as starting places:

Initialize the screen

This can be done very simply just by calling

	% initalize the screen
	myscreen = initScreen;

This call will handle opening up of the screen with appropriate parameters and setting the gamma table.

If you want to add specific parameters for your computer in mgl 2.0 just use mglEditScreenParams.

Setup the task structure

In the testExperiment, the task structure is a cell array that actually contains two separate tasks that will be run in the course of the experiment.

This sets the first task to be the fixation staircase task. If you don't want to use the fixation task then you can omit this part:

	% set the first task to be the fixation staircase task
	[task{1} myscreen] = fixStairInitTask(myscreen);

This is the first “phase” of our task. Not all tasks need to have different phases, but in this case we want the experiment to start with dots moving incoherently for 10 seconds and then we want trials to run in the next phase.

	% set our task to have two phases. 
	% one starts out with dots moving for incohrently for 10 seconds
	task{2}{1}.waitForBacktick = 1;
	task{2}{1}.seglen = 10;
	task{2}{1}.numBlocks = 1;
	task{2}{1}.parameter.dir = 0;
	task{2}{1}.parameter.coherence = 0;

Each one of the fields in the task set the behavior of that phase of the task.

The next phase of the task will be the one that actually runs the trials.

	% the second phase has 2 second bursts of directions, followed by	 
	% a top-up period of the same direction
	task{2}{2}.segmin = [2 6];
	task{2}{2}.segmax = [2 10];
	task{2}{2}.parameter.dir = 0:60:360;
	task{2}{2}.parameter.coherence = 1;
	task{2}{2}.random = 1;

In this task, we have a block of trials in which we will show trials with different motion directions. You set what parameters you want to use in the “parameter” part of your task. Note that you can use any name for parameters that you like. Here we call them dir for direction and coherence for motion coherence. Note that we have only one value of motion coherence so all trials will be run with a motion coherence of 1.

	task{2}{2}.parameter.dir = 0:60:360;
	task{2}{2}.parameter.coherence = 1;

We also have to decide the order in which parameters will be presented in a block of trials. The default is to run them sequentially (in this case directions 0 then 60 then 120 etc). To randomize the order, we set:

	task{2}{2}.random = 1;

Our trial will have two segments, a 2 second segment in which the stimulus is presented and a 6-10 second long intertrial interval:

	task{2}{2}.segmin = [2 6];
	task{2}{2}.segmax = [2 10];

The task code will automatically keep track of the variables in the parameter field, so that you can later access them to find out which direction of motion was shown on what trial. You will be able to do this by using the function getTaskParameters.

Initialize the stimulus.

The stimulus is kept in a global variable so that if the variable is very large, we don't incur overhead with passing it around all the time. If you want to have the stimulus variable saved at the end of the experiment, you can call the function initStimulus as below. Note that you do not need to call initStimulus if you do not want to save the stimulus structure.

	% init the stimulus
	global stimulus; 
	myscreen = initStimulus('stimulus',myscreen);
	stimulus = initDots(stimulus,myscreen);

The function initDots is specific for creating the dots stimulus for this test experiment, you will substitute your own function for creating your stimulus.

Create callback functions

Callbacks are the way that you control what happens on different portions of the trial and what gets drawn to the screen. A callback is simply a function that gets called at a specific time. You write the function and you let updateTask handle when that function needs to be called.

There are two required callbacks:

The first required callback that is used in this program is the one that gets called every time a segment starts.

	function [task myscreen] = startSegmentCallback(task, myscreen)
 
	global stimulus;
	if (task.thistrial.thisseg == 1)
		stimulus.dots.coherence = task.thistrial.coherence;
	else
		stimulus.dots.coherence = 0;
	end
	stimulus.dots.dir = task.thistrial.dir;

What it does is it looks in the “thistrial” structure for what segment we are on, if we are not in segment one (i.e. the intertrial interval) it sets the motion coherence to 0, otherwise it sets it to whatever the parameter coherence is set to (defined in the task.parameter.coherence field). It also sets the direction of motion of the dots.

The second (and most important) callback is the one used to draw the stimulus to the screen:

	function [task myscreen] = screenUpdateCallback(task, myscreen)
 
	global stimulus 
	mglClearScreen;
	stimulus = updateDots(stimulus,myscreen);

You can put your stimulus drawing routines in here. In this program, we simply clear the screen and draw the dots. This function gets called every display refresh.


Once these functions are defined in your file, you tell the programs to use these callbacks by using initTask to register the callbacks.

	% initialize our task with only the two required callbacks
	for phaseNum = 1:length(task{1})
		[task{1}{phaseNum} myscreen] = initTask(task{1}{phaseNum},myscreen,@startSegmentCallback,@screenUpdateCallback);
	end

NOTE: It is necessary to register the callbacks in a specific order. The correct order for registering callbacks is: startSegmentCallback, screenUpdateCallback, getResponseCallback, startTrialCallback, endTrialCallback, startBlockCallback

It doesn't matter exactly how you name the callbacks, what matters is what order you call them in. If there is a callback that you are not defining, you can enter it as [] in the initTask call, or leave it out:

for example,

	[task myscreen] = initTask(task,myscreen,@startSegment, @screenUpdate, @getResponse, [],[], @startBlock);

or

	[task myscreen] = initTask(task,myscreen, @startSegment, @screenUpdate, @getResponse);

More details can be found in the callbacks section.

Create a display loop

Now that everything is setup to run your experiment all you need is a display loop that calls updateTask to run each one of the tasks that are being displayed. Then to flip the front and back buffer of the display to show your stimulus, you call tickScreen. This is the main loop in which your program is run. It also checks for whether the user hit the <ESC> key, and ends the program when it has been hit.

	phaseNum = 1;
	while (phaseNum <= length(task{2})) && ~myscreen.userHitEsc
		% update the dots
		[task{2} myscreen phaseNum] = updateTask(task{2},myscreen,phaseNum);
		% update the fixation task
		[task{1} myscreen] = updateTask(task{1},myscreen,1);
		% flip screen
		myscreen = tickScreen(myscreen,task);
	end

At the very end you end the task which will save out information about your experiment.

	myscreen = endTask(myscreen,task);

Integration with an eye tracker

The task structure also provides easy integration with an eye tracker. The basic functionality is handled by a set of callback functions that handle interacting with the eye tracker. Currently, support for the SR Research (http://www.sr-research.com/) EyeLink trackers is fully supported. The current eye position is also available for constructing simple gaze contingent displays. See here.

Experimental parameters

Basics

For your experiment you can choose what parameters you have and what values they can take on. You do this by adding parameters (of your choosing) into the parameter part of a task variable:

task.parameter.myParameter1 = [1 3 5 10];
task.parameter.myParameter2 = [-1 1];

You can add any number of parameters that you want. updateTask will chose a value on each trial and put those values into the thistrial structure:

task.thistrial.myParameter1
task.thistrial.myParameter2

would equal the setting on that particular trial. In each block every combination of parameters will be presented. You can randomize the order of the parameters by setting:

task.random = 1;

Note that parameter should really just be used for the parameters over which you want to randomize your experiment. For example, you may be testing several contrasts in your experiment, that should be coded as a parameter. You may also have some random variables, things like which segment that target should be presented in for example–things that need to be randomized, but are not a crucial parameter you are testing. For these types of variables, you should use randVars instead of parameter (see below).

What if I have parameters that are not single numbers

You may have a parameter that is an array rather than a single number, for example a string:

task.parameter.strings = {'string1','string2','string3'};

The variable strings will be set in task.thistrial:

task.thistrial.strings

What if I have a group of parameters

You may have stimuli in which the parameters are grouped into different sets. For example you might want to show two types of grating patches. One tilted to the left with a high contrast and low spatial frequency and the other tilted to the right with low contrast and high spatial frequency.

Then you could do

task.parameter.groupNum = [1 2];
task.private.group{1}.orientation = -10;
task.private.group{1}.contrast = 1;
task.private.group{1}.sf = 0.2;
task.private.group{2}.orientation = 10;
task.private.group{2}.contrast = 0.1;
task.private.group{2}.sf = 4;

On each trial, you get the parameters by doing

task.thistrial.thisgroup = task.private.group{task.thistrial.groupNum};

randVars

For variables that you just want to have some randomization over, you can declare them as randVars. For example, you might want to specify a target interval which should be either 1 or 2 on any given trial, but you don't want that to be block randomized. Then you can declare that variable as a uniform randomization:

task.randVars.uniform.targetInterval = [1 2];

This variable will then be available in task.thistrial.targetInterval.

You may also want to have the variable block randomized, like a parameter, but the blocks should be independent of the main parameter:

task.randVars.block.blockedVar = [-1 0 1];

This will guarantee that on every three trials, blockedVar will be set to each one of the possible values -1,0 and 1.

Note that with randVars the randomization is chosen at the beginning of the experiment and by default 250 trials are randomized after which you will cycle back through the variables. If you need more than 250 trials, you can set:

task.randVars.len_ = 500;

Storing variables calculated during a trial

If you want to store a value calculated during a trial (e.g. a user entered value or a calculated value) you can use the 'calculated' type of randVar. The variables defined in the calculated struct array are initialized to the value(s) specified. The variables are then made available in the 'task.thistrial' as with other parameters and randVars. However, the all variables that were defined as 'calculated' are saved back to the randVars variable array at the end of the trial. These calculated values will then be available when you extract the parameters using getTaskParameters. For example, you can declare in your task variable:

task{1}.randVars.calculated.myVar = nan;

Note that the default value (if you don't set it in a callback, will be the one set above, i.e. nan). Next in any callback, you can set that variable. For example in the response callback:

task.thistrial.myVar = 15;

After the experiment is done that variable can be accessed through getTaskParameters just like any other parameter or randVar. Note that if you set the variable to something other than a scalar, the calculated variable will be stored as a cell array.

You may also (optionally) specify all the values that your calculated variable may take on. This is useful if during any given run you won't necessarily encounter all possible values. To do this set the variable name with an underscore after it:

task{1}.randVars.calculated.myVar_ = 1:15;

Using your own random sequence

You might have your own randomization routine and want to use that to randomize parameters. You can do that with randVars:

task.randVars.myRandomParameter = [...];

Then myRandomParameter will be available in task.thistrial.myRandomParameter in the order you specify in the array. “myRandomParameter” can be any name you choose. Note in this example there is no type definition before the variable name (e.g., block or uniform). Variables declared this way will be treated by MGL as a real sequence, whereas the normal usage (with block/uniform field) is treated as declaration of the levels of a variable, which is used to generate a sequence by MGL.

Creating a parameter sequence after running the stimulus program

Sometimes you may want to compute a parameter sequence after you have run the stimulus program. For example, if you want to compute a new variable sequence to do event-related processing based on the existing data within your stim file. To do so, you can use the function addCalculatedVar. You run this on a stimfile. For example, say you have a stimfile with 5 trials, and you wanted to add the variable 'myCalcVar' with values [0 3 2 4 1] for those trials:

addCalculatedVar('myCalcVar',[0 3 2 4 1],'100727_stim01');

After running this, the variable myCalcVar will appear when you run getTaskParameters and you can use it to do event-related analysis from the mrLoadRet GUI. Note that addCalculatedVar will overwrite the stimfile 100727_stim01.mat, but will save an original copy called 100727_stim01_original.mat.

You may also (optionally) specify all the values that your calculated variable may take on. This is useful if during any given run you won't necessarily encounter all possible values. To do this set the variable name with an underscore after it:

addCalculatedVar('myCalcVar',[0 3 2 4 1],'100727_stim01','allval',0:5);

See also this how-to.

Segment times

How to setup segment times

Each trial can be divided into multiple segments where different things happen, like for instance you might have a stimulus segment and response segment that you want to have occur for 1.3 and 2.4 seconds respectively:

task.seglen = [1.3 2.4];

At the beginning of each segment the callback startSegment will be called and you can find out which segment is being run by looking at:

task.thistrial.thisseg

How to randomize the length of segments

If you want to randomize the length of segments over a uniform distribution, like for instance when you want the first segment to be exactly 1.3 seconds and the second segments to be randomized over the interval 2-2.5 seconds:

task.segmin = [1.3 2];
task.segmax = [1.3 2.5];

In this case, do not specify task.seglen.

If you want the second interval to be randomized over the interval 2-2.5 seconds in intervals of 0.1 seconds (i.e. you want it to be either 2,2.1,2.2,2.3,2.4 or 2.5:

task.segmin = [1.3 nan];
task.segmax = [1.3 nan];
task.segdur{2} = [2:0.1:2.5];

Or, if you want different durations with different probabilities (the above would make each of the segment durations equally likely):

task.segmin = [1.3 nan];
task.segmax = [1.3 nan];
task.segdur{2} = [1 2 8];
task.segprob{2} = [0.8 0.1 0.1];

This would make the second segment have durations of either 1 2 or 8 seconds with the 1 second one having a probability of 0.8 and the others having 0.1 probability. You can also specify multiple segments to have different durations like:

task.segmin = [1.3 nan nan];
task.segmax = [1.3 nan nan];
task.segdur{2} = [2:0.1:2.5];
task.segdur{3} = [1 2 8];
task.segprob{3} = [0.8 0.1 0.1];

This would make segment 2 and segment 3 behave as in the above two examples.

You can also have a segment wait until a backtick happens, so that you can easily synch to volumes, for example:

task.segmin = [1.3 2];
task.segmax = [1.3 2.5];
task.synchToVol = [0 1];

This will cause the second segment to last a random amount of time between 2 and 2.5 seconds and then wait until a backtick occurs before going on to the next trial. Note that when using synchToVol it is a good idea to make the segment for which you are waiting for a volume acquisition to happen slightly shorter than you actually want. This way the segment time will be finished and it will be waiting for the volume acquisition to continue.

How to wait for user input before moving to next segment

Sometimes you will want to wait for user input to decide when to end a segment of the trial, rather than pre-set a time. To do this, you need to: (1) set the segment length to inf, (2) take user input for that segment, and (3) in the responseCallback, end the segment when the subject responds. [Note that if you want to limit how much time the user has to respond, but still wait for input, you can set the segment length to something less than inf, e.g. 5 seconds; this means that the segment will end either when the subject responds, or when 5 seconds have elapsed, whichever comes first.]

An example of how this might be implemented, in the case when the second of three segments waits for subject input before terminating:

% in the main task body:
task.seglen = [.5 inf 2];
task.getresponse = [0 1 0];
% At the end of the responseCallback function:
task = jumpsegment(task);

For other uses of jumpsegment, and for how to use jumpsegment(task, inf), see how to program a dual task.

Keeping time in seconds, volumes or refreshes

Trial segments can keep time in either seconds (default), volumes or monitor refreshes.

To change timing to use volumes:

task.timeInVols = 1;

To change timing to use monitor refreshes (note that is probably not a great idea to keep time in monitor refreshes since if you drop a frame, your timing will be altered).

task.timeInTicks = 1;

With timeInVols or timeInTicks, your segment times should now be integer values that specify time in Vols or monitor refreshes (e.g.):

task.seglen = [3 2];

Note, that the default (time in seconds) adjusts for segment overruns that might occur when you drop monitor frames, but the timeInTicks will not and is therefore usually less accurate.

Callbacks

Callbacks are the way that you control what happens on different portions of the trial and what gets drawn to the screen. They are simply functions that get called at specific times in the experiment.

It doesn't matter exactly what you call them, but it does matter exactly what order you register them in.

There are two required callbacks, and the rest are optional. If for some reason you don't need one of the required callbacks, you can just leave it empty, but you must still define it.

Callbacks are also discussed in the overview.

Registering callbacks

You must register your callbacks with the initTask function, in the following order:

[task myscreen] = initTask(task,myscreen,@startSegmentCallback,@screenUpdateCallback,@getResponseCallback,@startTrialCallback,@endTrialCallback,@startBlockCallback);

You do not need to specify all the callbacks, only startSegmentCallback and screenUpdateCallback. To omit any of the callbacks, either don't pass it in to initTask or set the appropriate argument to []. Make sure that you return task and myscreen.

For example, you might have

[task myscreen] = initTask(task,myscreen,@startSegmentCallback,@screenUpdateCallback,[],@startTrialCallback,[],@startBlockCallback);

or

[task myscreen] = initTask(task,myscreen,@startSegmentCallback,@screenUpdateCallback,@getResponseCallback);

screenUpdateCallback (required)

function [task myscreen] = screenUpdateCallback(task, myscreen)
% do your draw functions in here.

Note that you will normally declare a global variable named stimulus that contains any textures or information about the stimulus and use that in here. Remember that screenUpdateCallback gets called every frame update. For a refresh rate of 60 Hz that means it definitely has to run within 1/60 th of a second, or else the program will start to drop frames and become slow. You should therefore make this function as simple as possible. For example, if you are using textures, call mglCreateTexture in your myInitStimulus function and only use the precomputed texture here in an mglBltTexture function.

Another option that you can consider is that for many types of stimulus you don't have to update the screen every frame refresh. For something like moving dots or a drifting gabor you will need to update the frame every screen refresh, but if you just want to show a static gabor for a full segment, you can use the flushMode=1 feature that is described below in startSegmentCallback.

startSegmentCallback (required)

The other mandatory callback is the one that is called at the beginning of each segment:

function [task myscreen] = startSegmentCallback(task, myscreen)

The variable task.thistrial will have fields set to what the parameters are for that trial. For instance if you have dir as one of your parameters, then you will have the field task.thistrial.dir set to one of the directions (chosen by updateTask).

If you are only drawing to the screen at the start of every segment, then you can use the flushMode=1 feature. Say for example you want to clear the screen and draw your texture to the screen and that is all that will happen in the segment then you can do something like:

mglClearScreen;
mglBltTexture(stimulus.tex,[4 0]);
myscreen.flushMode = 1;

Note that in this case you do not do any drawing in the screenUpdateCallback (this function will be empty). You only do drawing in the startSegmentCallback. This assumes that the only time the screen changes is when you start a new segment of your trial.

getResponseCallback (optional)

You can (optionally) define a callback for when the subject hits a response key:

function [task myscreen] = getResponseCallback(task,myscreen)

If you don't have subject responses in your experiment, you can just put this one line in with nothing after it.

There is a field called

task.thistrial.whichButton

This will get filled with which button was pressed (a number from 1-9). Note that if two keys are pressed down at the same time, it will only return the first in the list (e.g. if 1 and 2 are simultaneously pressed, it will return 1).

If you want to get all the keys that are pressed, you can look at

task.thistrial.buttonState

This will be an array where each element will have 0 or 1 depending on whether the key was pressed or not.

Note that the getResponseCallback will only be called if in the task structure you have set the appropriate segment of the getResponse variable. For example, if you have a two segment trial, and you want to get subject responses in the second segment of the trial you would do:

task.getResponse = [0 1];

You may also set a getResponse segment to 2. What this does is similar to setting myscreen.flushMode = 1. It prevents mglFlush from being called to update the screen while you are waiting for a keyboard press. This will get much more accurate keyboard timing, but will not allow the screen to update while you are waiting (i.e. you have to have a static display–no moving dots or flickering gratings or anything).

task.getResponse = [0 2];

If you want to get other keys, rather than the defined keys 1-9, for example if you want the keypad numbers, you can override which keys will be checked with:

myscreen.keyboard.nums = [84 85];
myscreen = initScreen(myscreen);

This is called at the beginning of your program. Note that to get the keycodes that correspond to a key, you can either use:

mglCharToKeycode({'a' 'b' 'c'})

or, for keys that you can't write like the keypad numbers or the esc key, run the program:

mglTestKeys

and type the keys you want and it will print out the correct keycode.

The getResponseCallback will get called every time the subject presses a button, so if the subject presses two buttons one after the other during the response period, getResponseCallback will be called twice. If you want to ignore the 2nd button press you can do:

if task.thistrial.gotResponse == 0
  %your response code here
end

task.thistrial.gotResponse will be set to 1 the second time the subject presses a key.

startTrialCallback (optional)

You can (optionally) define a callback that gets called at the beginning of each trial

function [task myscreen] = startTrialCallback(task,myscreen);

endTrialCallback (optional)

You can (optionally) define a callback that gets called at the end of each trial

function [task myscreen] = endTrialCallback(task,myscreen);

startBlockCallback (optional)

You can (optionally) define a callback that gets called at the beginning of a block

[task myscreen] = startBlockCallback(task,myscreen)

Saving data into a stimfile

Stimfiles

After you have run an experiment, all three variables (myscreen, task and your stimulus variable) will get saved into a file called

yymmdd_stimnn.mat

Where yymmdd is the current date, and nn is a sequential number starting at 01. This file will be stored in the current directory or in the directory ~/data if you have one.

After these get saved, you can access all the variables for your experiment by using

e = getTaskParameters(myscreen,task);

This will return a structure that contains the starting volume of each trial, what each variable was set to, the response of the subject and reaction time, among other things. For most purposes this should contain all the information you need to reconstruct what was presented on what trial and what the subject's response was.

Note that there is a variable called myscreen.saveData which tells the task structure whether to save the stim file or not. The default on your computer is probably set not to save the stim file. When you run on the computer in the scanner room, it will save the file automatically. For debugging purposes this is usually what you want so that you don't save unnecessary stim files every time you test your program. However if you want to save the stim file on your test computer to look at, you can add the following to your code where you call initScreen:

myscreen.saveData = 1;
myscreen = initScreen(myscreen);

The variables stored in the stim file contain all the information you should need to recreate what happened in your experiment. In fact, it even contains a full listing of the file you used when running the experiment. This is useful since often you might make minor changes to the program and forget what version you were using when you ran an experiment. You can access a listing from the task variable:

task{1}{1}.taskFileListing

Directory to save stimfiles in

By default, mgl will save the data in ~/data if that directory exists, and in the current directory if ~/data doesn't exist. To save data to a specific directory instead of to these defaults, set

myscreen.datadir = datadirname;

where datadirname is the full path of the desired directory.

Retrieving data from stimfiles

getTaskParameters

usage: e = getTaskParameters(myscreen,task);
purpose: Gets all the info about your task and its parameters

argument value
myscreen The myscreen variable saved in your stimfile
task The task variable saved in your stimfile. This can be a cell array (task with multiple phases) or a cell array of cell arrays (multiple tasks with multiple phases) or a structure (single task, single phase).
return argument value
e A structure or cell array of structures that contains information about the task you run, including how each parameter and randVar was set on each trial, the stimvol for each trial, reaction times for each trial etc.

See also here

getStimvol

usage: [stimvol stimNames var] = getStimvol(v,'varname',<taskNum=?>,<phaseNum=?>,<segmentNum=?>);
purpose: Gets the stimvols for a variable. This function is available in mrTools. It will call getStimvolFromVarname on each stimfile for the scan and put all the stimvols together. This is useful for a concatenated scan that has multiple stimfiles since it will create the stimvols correctly for the concatenation – honoring junked frames for instance.

argument value
v A view variable returned by newView in mrTools. Make sure to set its curGroup and curScan to the group and scan that you want to get the stimvols for.
varname The name of the variable you want to get stimvols for (see getStimvolFromVarname for all the options that you can use to select subsets of values of the variable
taskNum This is an optional argument for when you have multiple tasks, select which task you want by passing in an argument 'taskNum=2' for instance.
phaseNum This is an optional argument for when you have multiple phases within a task, select which phase you want by passing in an argument 'phaseNum=3' for instance.
segmentNum This is an optional argument for when you have want to get volumes relative to a specific segment of a trial rather than from the beginning of the trial. Select which segment you want by passing in an argument 'segmentNum=2' for instance.
return argument value
stimvol A cell array of arrays. The cell array has one array for each setting of the variable. Each array contains the volume numbers for the times at which the variable in question was set to that particular value.
stimNames A cell array of strings that contains a description of what that particular stimulus setting is.
var A structure that contains the varname, taskNum etc. that you used to get the stimvols

An example:

v = newView;
v = viewSet(v,'curGroup',3);
v = viewSet(v,'curScan',1);
[stimvol stimNames var] = getStimvol(v,'varname','taskNum=2','phaseNum=2');

getStimvolFromVarname

usage:[stimvol stimNames trialNum] = getStimvolFromVarname(varnameIn,myscreen,task,taskNum,phaseNum,segmentNum);
purpose: Gets a cell array that contains the stimulus volumes for a particuar variable name

argument value
varnameIn The variable name that you want to get stimvols for. See below for more information on how this value can be set.
myscreen The myscreen variable saved in your stimfile
task The task variable from your stimfile
taskNum This is an optional argument for when you have multiple tasks. Set to the number of the task you want.
phaseNum This is an optional argument for when you have multiple phases within a task. Set to the number of the phase you want.
segmentNum This is an optional argument for when you have want to get volumes relative to a specific segment of a trial rather than from the beginning of the trial. Set to the number of the segment you want.
return argument value
stimvol A cell array of arrays. The cell array has one array for each setting of the variable. Each array contains the volume numbers for the times at which the variable in question was set to that particular value.
stimNames A cell array of strings that contains a description of what that particular stimulus setting is. If you want to get the values of those variables instead of just the strings, see the function stimValsFromNames in mrTools
trialNum A cell array which contains which trial number each stimvol is associated with is.

varnameIn can be the name of a parameter or randVar. e.g.:

getStimvolFromVarname('dir',myscreen,task,2,2);

It can also be of the form varname(indexVar). For when you have used a parameterCode and an index variable. e.g.:

getStimvolFromVarname('localDir(dirIndex)',myscreen,task);

Or it can be _all_ which returns all trial numbers regardless of stimulus type:

getStimvolFromVarname('_all_',myscreen,task);

Or it can be _every_ which returns every combination of different parameters.

getStimvolFromVarname('_every_',myscreen,task);

For example, say you have the following variables, with the following values:

TrialNum = 1 2 3 4
    var1 = 1 2 1 2
    var2 = 1 1 2 2

Then _all_ would return:

{{[1 2 3 4]}}

and _every_ return would return:

{{1},{2},{3},{4}}

You can also specify every as a cross between the two variables:

getStimvolFromVarname('var1 _x_ var2',myscreen,task);

This syntax offers a bit of flexibility since you can specify which variables get crossed and even for what values, for instance you can specify a cross between var1 and var2 set to the value 1

getStimvolFromVarname('var1 _x_ var2=1',myscreen,task);

Which would return:

{{1},{2}}

You can specify arbitrary and conditions, by doing the following:

getStimvolFromVarname{{'var1=1','var2=[1 2]'},{'var2=1','var2=1'},{'var2=1','var2=2'}}

Which would return

{{[[1 3]},{[2]},{[4]}}

getTaskVarnames

usage:varnames = getTaskVarnames(task);
purpose: Gets a cell array of the variables names in your task. This function can also accept an MLR view instead of the task variable.

argument value
task The task variable from your stimfile
return argument value
varnames A cell array of strings containing the names of parameters and randVars from your task

getParameterTrace

usage: trace = getParameterTrace(myscreen,task,'varname');
purpose: Gets a trace of the variable called for. The time base for the trace is in screen refreshes. e.g.: plot(getParameterTrace(myscreen,task,'dirnum'));

argument value
myscreen The myscreen variable saved in your stimfile
task The task variable from your stimfile
varname The variable that you want to create a trace for
return argument value
trace A vector containing the value of the variable as a function of time in screen refreshes

getVarFromParameters

usage: [varval taskNum phaseNum] = getVarFromParameters('varname',e);
purpose: Gets the variable settings for each trial

argument value
varname The name of the variable of interest
e A structure retruned from getTaskParameters
return argument value
varval An array of what the particular variable was set to on each trial
taskNum The number of the task in which the variable was defined. If defined in multiple tasks will return the last task that it was defined in. To get another task, useg getTaskParameters to select which task to get information from.
phaseNum The number of the phase in which the variable was defined

makeTraces

For most people, using getTaskParameters is the easiest way to get what happened on each trial. But there is another mechanism that allows you to see the specific timing of events as traces. This is saved in the traces field of the myscreen variable. This field stores when each volume was collected and what stimulus was presented. Using this information you can reconstruct the volume when each stimulus occurred. It is set up so the first row contains an array which has a one every time a volume was acquired (i.e. whenever a backtick was received) and zeros elsewhere. The timebase for the array is in monitor refreshes, so every 60 elements shouls be one second. Take a look at what this trace has by doing:

myscreen = makeTraces(myscreen);
plot(myscreen.traces(1,:));

You can also plot in seconds, relative to the beginning of the experiment:

plot(myscreen.time,myscreen.traces(1,:));

The other important trace is the one corresponding to myscreen.stimtrace:

plot(myscreen.traces(myscreen.stimtrace,:));

This will contain the information about which trial was presented as long as you have set the writeTrace variable correctly (see next section).

Integrating eye tracking with a task

There is a generic interface to the eye tracking functionality. As long as the appropriate callback functions are written for an eye tracker, no code needs to be changed in your task.

To configure MGL to use an eye tracker you must initialize the eye tracker support. The eye tracker configuration is specified in the eyetracker field of myscreen.

You should start by specifying whether to save the data (in a file) and what data you need.

 myscreen.eyetracker.savedata = true;
 myscreen.eyetracker.data = [1 1 1 0]; % don't need link events

You also need to specify one of your tasks (or which of your phases) you want to use to control the eye data timing. This task will define what is a block, trial, etc.

	task{1}.collectEyeData = true;

Next you initialize MGL's eye tracker support for your eye tracker.

 myscreen = initEyeTracker(myscreen, 'Eyelink');

And finally you need to run a calibration.

	myscreen = calibrateEyeTracker(myscreen);

How to end the experiment

In general, the easiest way to code the stimulus is to have it continue indefinitely until the scanner stops scanning. After the scan is finished and you want to stop the stimulus you hit the ESC key. This way you never have the stimulus stop before the scanner does, and it doesn't hurt to keep having the stimulus go past the end of the scan.

If instead you want to only collect a specific number of blocks of trials and stop, then you would set:

task{1}.numBlocks = 4;

say, to run for 4 blocks of trials and then stop. Or if you want to run for a specific number of trials and stop, then you can do:

task{1}.numTrials = 17;

which would run for 17 trials and stop. These variables default to inf so that the experiment only stops when the user hits ESC.

How to make a grating

You may want to look at the code mglTestTex. Here is sample code. (For mgl version 1.5, use makeGrating and makeGaussian instead of mglMakeGrating and mglMakeGaussian)

mglOpen;
mglVisualAngleCoordinates(57,[16 12]);
mglClearScreen(0.5);
grating = mglMakeGrating(10,10,1.5,45,0);
gaussian = mglMakeGaussian(10,10,1,1); 
gabor = 255*(grating.*gaussian+1)/2;
tex = mglCreateTexture(gabor);
mglBltTexture(tex,[0 0]);
mglFlush;

Here are some variations. Run the code above first for these example.

Here is a sharp bordered patch (set to be 2 std of the gaussian)

mglClearScreen(0.5);
gabor = 255*(grating.*(gaussian>exp(-2))+1)/2;
tex = mglCreateTexture(gabor);
mglBltTexture(tex,[0 0]);
mglFlush;

A square wave grating:

mglClearScreen(0.5);
gabor = 255*(sign(grating).*gaussian+1)/2;
tex = mglCreateTexture(gabor);
mglBltTexture(tex,[0 0]);
mglFlush;

A plaid

mglClearScreen(0.5);
grating1 = mglMakeGrating(10,10,1.5,45,0);
grating2 = mglMakeGrating(10,10,1.5,135,0);
gabor = 255*((grating1/2+grating2/2).*gaussian+1)/2;
tex = mglCreateTexture(gabor);
mglBltTexture(tex,[0 0]);
mglFlush;

A checkerboard:

mglClearScreen(0.5);
grating1 = mglMakeGrating(10,10,1.5,45,0);
grating2 = mglMakeGrating(10,10,1.5,135,0);
gabor = 255*(sign(grating1/2+grating2/2).*gaussian+1)/2;
tex = mglCreateTexture(gabor);
mglBltTexture(tex,[0 0]);
mglFlush;

Flickering checkerboard:

mglClearScreen(0.5);
grating1 = mglMakeGrating(10,10,1.5,45,0);
grating2 = mglMakeGrating(10,10,1.5,135,0);
gabor1 = 255*(sign(grating1/2+grating2/2).*gaussian+1)/2;
gabor2 = 255*(sign(-grating1/2-grating2/2).*gaussian+1)/2;
tex1 = mglCreateTexture(gabor1);
tex2 = mglCreateTexture(gabor2);
for i = 1:5
  mglBltTexture(tex1,[0 0]);
  mglFlush;
  mglWaitSecs(0.1)
  mglBltTexture(tex2,[0 0]);
  mglFlush;
  mglWaitSecs(0.1)
end

Drifting grating

phases = [0:10:359];
for i = 1:length(phases)
  grating = mglMakeGrating(10,10,1.5,135,phases(i));
  gabor = 255*(grating.*gaussian+1)/2;
  tex(i) = mglCreateTexture(gabor);
end
for i = 1:length(phases)*5
  mglClearScreen(0.5);
  mglBltTexture(tex(rem(i,length(phases))+1),[0 0]);
  mglFlush;
end

How to use 10-bit contrast

If you want to use 10-bits so as to be able to display finer contrast gradations, you need to remap the usual 8-bit contrast steps (0:255) into a subset of the larger 10-bit (1024) contrast table. This can be done using a piece of code called setGammaTable that can be included in your code as a subfunction (written by JG and FP and found at ~shani/matlab/MGLexpts/setGammaTable.m), but there are some details to be careful of.

First, you will want to ‘reserve’ some colors that you will want to be able to use and leave unaffected by the resetting of the gamma table. This allows you to show, for example, a high-contrast fixation cross at the same time that you’re showing a low-contrast target. If you don’t reserve some colors, you won’t be able to have anything high-contrast at the same time as you use the 10-bit capacity. See example code taskTemplateContrast10bit.m where four colors are saved, and a low-contrast target is shown (written by SO and found at mgl/task/taskTemplateContrast10bit.m).

How to run a dual task

If you want to run two tasks at once, for example, an RSVP task at fixation and a detection task in the periphery, you will create two tasks and call one from within the other. You should construct it so one task (e.g. detection) is the main task and the other task (e.g. fixation-RSVP) is the subsidiary task.

The subsidiary task needs to be constructed like a regular task, with its own initialization and callbacks, but without the updateTask loop. It will be updated from within the main task.

The main task will be constructed as usual, but an extra line will appear to set the subsidiary task and to update it. For example, to set the fixation task as the subsidiary, you will add a line in the main task like this:

task{2} = fixationTask(myscreen);

Then, the update loop of the main task will look like this:

phaseNum = 1;
while (phaseNum <= length(task{1})) && ~myscreen.userHitEsc
 % update the task
 [task{1} myscreen phaseNum] = updateTask(task{1},myscreen,phaseNum);
 [task{2} myscreen] = updateTask(task{2},myscreen,1);
 % flip screen
 myscreen = tickScreen(myscreen,task);
end
% if we got here, we are at the end of the experiment
myscreen = endTask(myscreen,task);

The key to getting this to work is to control the timing. One way to do this is to have the main task set some variables which tell the subsidiary task whether or not to run. In order to do this, have the stimulus variable set as a global variable in both tasks. Set two stimulus subfields as flags, e.g. stimulus.startSubsidiary and stimulus.endSubsidiary, in order to control the subsidiary task. Then have the subsidiary task check the status of these flags, and start or stop accordingly.

In order to get the subsidiary task to start and stop when the appropriate flags are set, you will need to do the following:

Set the first segment of the subsidiary task to have infinite length. That makes the subsidiary wait in the first segment until the main task calls it. When the main task wants to start the subsidiary task, it will set the stimulus.startSubsidary flag to 1, and this will cause the subsidiary to jump to the next segment as follows:

In the screenUpdate callback of the subsidiary task, have a loop that checks to see whether the stimulus.startSubsidiary flag is set to 1. (This should be done in screenUpdate so that it can check all the time.) Have an if-loop that tells the task to skip ahead to the next segment as soon as the flag == 1. (It’s a good idea to reset the flag to 0):

if(stimulus.startSubsidiary == 1)
 stimulus.startSubsidiary = 0;
 task = jumpSegment(task);
end

When you’re ready to end the subsidiary task, have the main task set the stimulus.endSubsidiary flag to 1, and have the following if-loop in the subsidiary’s screenUpdate callback:

if(stimulus.endSubsidiary == 1)
 stimulus.endSubsidiary = 0;
 task = jumpSegment(task,inf);
end

The ‘inf’ argument in the jumpSegment function call tells the task to jump to the end of all the segments and start the next trial. This puts the subsidiary task back into the state of being in the infinite first segment, waiting for the start flag to be reset to 1 by the main task.

Example code can be found in taskTemplateDualMain.m and taskTemplateDualSubsidiary.m

How to calibrate the monitor

Moncalib

To calibrate a monitor, you can use the program moncalib.m in the utils directory. It is set up to work with the PhotoResearch PR650 photometer/colorimeter (which the Lennie lab has) and a serial port adaptor (use the one from the Carrasco lab it is a white Keyspan USA-28 and says Carrasco Lab on it–the one that is in the bag with the photometer is a white translucent Keyspan USA-28X B and doesn't seem to work properly). It can also be used with a Minolta or TopCon photometer/colorimeter. The serial port interface for matlab is included in the mgl distribution but can also be found on the Mathworks website [1]. To use the Keyspan USA-28 adaptor you will need to download a driver from [2].

There are a few points that you should pay special attention to:

PR650 REMOTE MODE 
(XFER) s/w ver 1.02 
CMD 51 NAK

This indicates that you pressed the return while the photometer is waiting for a transfer signal (not sure what it is), and hence entered the XFER mode. If you wait another 2 secs or so it will enter the control mode, now press 'return' you should see this:

PR650 REMOTE MODE 
(CTRL) s/w ver 1.19 
CMD B

Basically there is about 2-3 secs time window you should press 'return' to get to this state.

If you cannot install the serial port interface or don't want to automatically calibrate using the USB cable you can also use the program to run manually with any photometer by typing in the luminance measurements yourself.

The program moncalib will save a calibration file in the local directory. For you to use this calibration file, you can store it in one of two places. Either in your own program directory under a directory called displays:

./displays

Or you can store it in the general displays directory

mgl/task/displays

InitScreen should automatically find the correct table by checking your computer name and looking for the file in these two places. If you do not use the standard filename, or have multiple calibrations for the same computer (like if you have multiple monitors calibrated), you can use a specific file by setting myscreen.calibFilename

myscreen.calibFilename = 'mycalibrationfile.mat';
myscreen = initScreen(myscreen);

Note that the calibFilename can be a literal filename as in the above, or you can specify a portion of the name that will get matched in a file from the displays directory (e.g. computername_displayname would matcha any file in the displays directory that looks like *computername_displayname*.mat).

The name of the file usually created by moncalib will be:

xxxx_computername_yymmdd.mat

Where xxxx is a sequential number starting at 0001 and yymmdd is the date of the calibration. This stores a variable called calib which contains all the information about the calibration. You can quickly plot the data in calib by doing:

load 0001_stimulus-g5_LCD_061004
moncalib(calib);

The most important field of calib is the table field which holds the inverse lookup table to linearize the monitor.

10 bit gamma tables

The NVIDIA GeForce series of video cards have 10 bit gamma tables (these are the only ones we have tested):

ATI 10 bit cards:

It is always the best to use the bit test in moncalib because some drivers do not allow 10-bit control on 10-bit DAC cards. You can also query the display card to see if it says that it supports a 10 bit gamma:

displayInfo = mglDescribeDisplays

Check the field gammaTableWidth to see if it is 10.

Calibration devices

Moncalib can talk directly through the USB/serial port to the PhotoResearch PR650, Topcon SR-3A, Minolta CS-100a or Minolta LS-100. If you want to use another photometer you can input the readings manually or consider writing a few lines of code in moncalib to support your photometer type.

Note that there are some commercially available devices to calibrate monitor screens which create color profiling information (e.g. [5] [6] [7]. We have tested one of these called Spyder2Pro which allows you to linearize the monitor output but found that is not yet suitable for psychophysics purposes. The calibration program crashes when you use the default settings to linearize the monitor (an email to the tech support confirmed this is a bug in their software). Using advanced settings it worked but it could only test luminance at 5 output levels. The linearization that it achieved was not accurate enough when tested with the PR650 (it looked like they are doing some sort of spline fit of the points and the luminance as a function of monitor output level looked like a wavy line around the ideal).

How to run an experiment with the same random sequence as a previous one

You can do this by calling initScreen with the randstate of the previous experiment

initScreen([],previousMyscreen.randstate);

This will insure that all the parameters, randVars and segment times are generated with the same random sequence as the previous experiment.

Alternatively, you can run both experiments starting with the same randstate (which can be an integer value). For example

initScreen([],11);

Will run the experiment with exactly the same randomization sequence every time.

How to change the back tick code

Different scanners and set up have different keyboard codes for the backtick sent out at each volume (e.g., back tick is '`' at NYU and '\' at Columbia). If you are waiting for or synching to the back tick you need to change the backtick character (actually the keyboard code of such character) that task is waiting for. This can be done by setting the backtick character in mglEditScreenParams. If instead you would like to change this programmatically, you can do the following after calling initScreen.

% 1. choose a back tick character
backTickCharacter = '\';
 
% 2. get its keyboard code
backTickCode = mglCharToKeycode({backTickCharacter})
 
% 3. save the code into the myscreen structure during its initialization
myscreen.keyboard.backtick = backTickCode;

The keyboard code is set also for other basic characters in mgl (i.e., the 'escape' key that exit a task loop, the 'return' key and the number keys used to get button responses). All these keys are stored in the field 'keyboard' of 'myscreen' (e.g., myscreen.keyboard.num). A good way to check the codes of several keys is to use the program mglTestKeys.m, which returns the code for characters typed in the matlab prompt.

How to set up a National Instruments card digital I/O card

You can use a National Instruments card for digital I/O with mgl by doing the following:

  1. Download NI-DAQmx Base. You may need to make a free account with NI.
  2. Make sure that the device (NI USB-6501) has up-to-date firmware, by running FWUpdate (included in Ni-DAQmx Base/bin)
  3. Restart matlab if you have already run readDigPort (the program has to reinit the driver)
  4. Documentation is installed and should be available from:
    file:///Applications/National%20Instruments/NI-DAQmx%20Base/documentation/docsource/daqmxbasecfuncindex.htm
  5. More information is available in the discussion of digital I/O.

You can set up to read digial pulses by setting digin using mglEditScreenParams.

How to open a small mgl-window in the current display

If you would like to test your code and stimuli by opening a small mgl window while keeping the rest of your desktop available you can do two things:

  1. Set up a 'debug' display using mglEditScreenParams
    1. Type mglEditScreenParams in matlab.
    2. Click to Add a new display on the first dialog box that appears.
    3. Hit Ok to edit that display.
    4. After that set its displayName to “debug”.
    5. Set screenNumber to 0 (this will set to a mgl window instead of a full screen). Set screenWidth and screenHeight to your choice of size for the window e.g. 400 and 200. Set the displayPos to the desired x and y position where the window should display.
    6. You can test the settings by hitting the “Test screen params” button.
    7. Now when running your experimental code for debugging open the screen using the command: initScreen('debug'); This will automatically load the settings for the windowed display and the stimuli will appear in a small mgl window.
    8. If you want to use the full screen settings, call initScreen without any arguments, or set the displayName for your full screen context in mglEditScreenParams to something like 'experiment' and open the screen in your program with initScreen('experiment');
  1. Manually code settings
    1. set the myscreen.screenNumer field to 0;
    2. choose the size and position of the mgl window in the current display
    3. set those in the variable myscreen
    4. init the screen using initScreen(myscreen);
  % get the resolution of main display
  res = mglResolution(1);
 
  % set the parameters of the mgl window
  myscreen.screenNumber = 0;  % tell mgl to open a small window instead of a full-screen one
  myscreen.screenWidth  = 200;% choose the horizontal size of the window
  myscreen.screenHeight = 150;% choose the vertical size of the window
 
  % now set the position of the window relative to the size of the main display.
  myscreen.displayPos = [res.screenWidth - myscreen.screenWidth res.screenHeight - myscreen.screenHeight];
 
  % init screen and open up the window
  myscreen = initScreen(myscreen);
 

How to add a new variable after you have run your experiment

Sometimes after you run an experiment you may want to add a new variable. For instance, you may have run a signal detection experiment and you want to add a new variable which tracks whether each trial was a hit, miss, false alarm or correct reject. Or maybe you want to mark certain trials as junk trials (for example when the subject does not response or there is an error trial) so that they will not be included with the good data in a deconvolution analysis (you should always still include these trials in your design, you just can later ignore the computed responses for these junk trials). If you add a new variable, then you can do deconvolution from the MLR menu item based on the new computed variable.

For example, you may want to mark a few trials in an experiment with different orientation stimuli as being junk. So, for example you start out with a variable with three orientation settings.

task{1}.parameters.orientation = [0 90 180];

After you run the experiment, you have different trials with different orientations:

>> e = getTaskParameters('090217_stim02');
>> e{1}(1).parameter.orientation
  [90 0 180 90 180 0 0 180 90 180 90 0]

Say you know the trial 3 and 7 are trials that you want to avoid in your deconvolution analysis. Then you should set them to have some dummy value, say -1

 >> newOrient = e{1}(1).parameter.orientation;
 >> newOrient(3) = -1;
 >> newOrient(7) = -1;
 >> newOrient
 [90 0 -1 90 180 0 -1 180 90 180 90 0];

And you can save this new variable back to your stimfile:

>> addCalculatedVar('newOrient',newOrient,'090217_stim02','taskNum=1','phaseNum=1')
(addCalculatedVar) Saving variable newOrient into 090217_stim02.mat for taskNum=1 phaseNum=1

This will change your stimfile to include the changed variable, you can check that it worked:

>> e = getTaskParameters('090217_stim02');
>> e{1}(1).randVars

ans = 

    newOrient: [90 0 -1 90 180 0 -1 180 90 180 90 0];

Now when you do your deconvolution analysis, you should be able to deconvolution based on the variable 'newOrient' and you will get an extra trial type that is the deconvolved responses for all the trials that you set to be -1. If these are junk trials then you can just ignore that response.

See also here for more info.

How to save info into a trace for keeping fine time resolution data

You may want to save information at a fine time resolution (and not just for each trial). If you just need to save a value that changes for each trial, you should use a calculated var, however sometimes it is necessary to save information that may change during a trial for later access. To do this, you can save the variable into a “trace”. This is in fact how mgl saves all task info. The trace is stored into a special field in the myscreen variable and you can later reconstruct a time record of what happened. To do this, you first need to add the trace to your task variable (do this after you call initScreen but before calling initTask).

[task{2}{1} myscreen] = addTraces(task{2}{1},myscreen,'someData');

The above example is for a task with two tasks and one phase, but substitute whatever task variable you have. The name of the trace ('someData' in the example above) should be something that won't interfere with other task variable fields so pick something distinct. A field task{2}{1}.someDataTrace will be added. This will carry the number of the trace which is used for adding data.

Then when you want to save new data (and this can be done at anytime - in updateScreenCallback or startSegmentCallback, etc. You do the following

myscreen = writeTrace(someData,task.someDataTrace,myscreen);

Where someData is a scalar value. Note that if the value someData is the same as the last value saved, nothing will actually be saved (to save memory). Only changes in the value are recorded and the trace is later reconstructed. If you need to have the data stored, set force:

myscreen = writeTrace(someData,task.someDataTrace,myscreen,1);

Note that with every call to writeTrace the current volume number and exact time are recorded.

When the stimulus program has been run, you can recover a trace of your variable, by creating the traces

myscreen = makeTraces(myscreen);

Then there will be a field 'traces' added which will contain all the traces. The trace you want to look at is the one encoded in the row defined as task.someDataTrace. The time points are defined by the field myscreen.time. So you can plot the value by doing:

plot(myscreen.time,myscreen.traces(task.someDataTrace,:);

How to save data into a subject specific directory

See subject ID.

How to display images / textures faster

The easiest way to create a texture and display is something like this:

mglOpen;
mglClearScreen
mglScreenCoordinates
texture = mglCreateTexture(round(rand(100,100,3)*255));
mglBltTexture(texture,[mglGetParam('screenWidth')/2 mglGetParam('screenHeight')/2]);
mglFlush;
mglDeleteTexture(texture);

Note that this can be slow due to mglCreateTexture needing to convert the matlab matrix into the texture format that OpenGL expects. One way to get around this is to precompute your textures in an initialization routine (for example if you have a small set of images you need to display). And then when you want to display you just call mglBltTexture which is very fast. Another is that you may be able to use a 1D texture (like if you just want to show a grating). See mglTestTex for examples of these strategies.

But, you may need to create images every screen refresh (for example, if you are showing some random noise stimulus which updates every frame refresh). To do this, you might want to try to format your matlab array in exactly the format that OpenGL expects and pass that to mglCreateTexture. The format is unsigned byte RGBA x imageWidth x imageHeight:

mglOpen;
mglClearScreen
mglScreenCoordinates
r = uint8(floor(rand(3,100,100)*256));
r(4,:,:) = 255;
texture = mglCreateTexture(r);
mglBltTexture(texture,[mglGetParam('screenWidth')/2 mglGetParam('screenHeight')/2]);
mglFlush;
mglDeleteTexture(texture);

Note that uint8 specifies unsigned byte format and that the color values for each pixel are the first dimension (rather than the last dimension). The fourth dimension is the alpha channel which in this case is set to 255 so that there is no alpha blending. This can speed things up considerably. For a largish image, say 600×600 mglCreateTexture will take (on a fast machine) under 2ms with this version, but could be 10s of ms with the regular version above. Remember also, to always delete textures once you are done using them.

Another trick that you might be able to use is to use openGL to scale an image rather than scaling it yourself. OpenGl has different modes of image scaling, for example linear interpolation or nearest neighbor. We default to using linear interpolation, but if you want nearest-neighbor, you can set that when your call mglCreateTexture:

mglOpen;
mglClearScreen
mglScreenCoordinates
r = uint8(floor(rand(3,100,100)*256));
r(4,:,:) = 255;
texture = mglCreateTexture(r,[],0,{'GL_TEXTURE_MAG_FILTER','GL_NEAREST'});
mglBltTexture(texture,[mglGetParam('screenWidth')/2 mglGetParam('screenHeight')/2 800 800]);
mglFlush;
mglDeleteTexture(texture);

There is also a way to just change the data buffer that a texture refers to without using mglCreateTexture multiple times. See help on mglBindTexture and mglTestTexFast.

Another trick that can be used for grating stimuli is to note that they are actually one dimensional and that you don't need to compute a full 2D texture. Instead you can make a 1D texture and let BltTexture repeat that sinusoid in the other space dimension. See mglTestTex for examples of this.

How to add a fixation task

We often use a fixation task which is a 2AFC fixation dimming task in which the fixation cross dims twice and the subject has to respond with either a button press on 1 or 2 to indicate which interval was dimmer. This is run on a staircase so that the difficulty is adjusted throughout a scan to maintain vigilance and attention level. To add this task to your code requires only a few lines of code. First, you need to initialize the fix task:

[fixTask myscreen] = fixStairInitTask(myscreen);

Then in your display loop, make sure to update the fix task. Note that in any call that requires sending the task variable, you should send both the main task and fixTask by putting them into a cell array - see the call to tickScreen below:

% while the main task still has a phase to complete
while (phaseNum <= length(task)) && ~myscreen.userHitEsc
  % update main task
  [task myscreen phaseNum] = updateTask(task,myscreen,phaseNum);
  % update the fixation task
  [fixTask myscreen] = updateTask(fixTask,myscreen,1);
  % flip screen
  myscreen = tickScreen(myscreen,{fixTask task});
end

After this, you just want to make sure that in the call to endTask you send the fixTask so that the subjects performance gets saved correctly:

myscreen = endTask(myscreen,{task fixTask});

Note that you will not have to draw any fixation crosses in your own code since this will be handled by the fixTask. For examples of code that use the fixation task you can look at mglRetinotopy or testExperiment. Also, there is a task which can be used to train subjects on the task (makes the fixation cross really big with words describing what to do):

fixStairTest

Also, if you want to change parameters of the fixation task, for example the timing of the intervals, the size of the cross, etc., you can set fields in the global fixStimulus before calling fixStairInitTask - for example:

global fixStimulus
fixStimulus.diskSize = 5;
fixStimulus.fixWidth = 10;
fixStimulus.fixLineWidth = 8;
fixStimulus.stimTime = 2;
fixStimulus.interTime = 1;
fixStimulus.responseTime = 5;

You should check the code of fixStairInitTask for a full listing of parameters you can set in this way.

How to make an exact experiment length

You may want to have your experiment take a fixed amount of time - even though the individual trials have randomized lengths. For instance, you may want to run an experiment in the scanner for a certain number of volumes but have randomized inter-trial intervals. To do this, you can simply set seglenPrecompute in your task variable to true:

task{1}.seglenPrecompute = 1;

Then make sure that you have set either numTrials or numBlocks:

task[1}.numTrials = 15;

For example, try running the test experiment

taskTemplateSeglenPrecompute

And you will see a message like this

(initTask) Computing 10 trials with average length 6.500000
(initTask:seglenPrecompute) Total length: 64.43 Desired length: 65.00 Diff: -0.57

Note that there is a slight difference between the desired length and the actual length. What seglenPrecompute is doing is that it computes a randomized set of trial lengths which should come out close to the average length but due to random variation will not be the right length. So, it then computes a new trial length and chooses a random trial. If replacing that random trial with the new trial gets the total closer to the desired length it exchanges the old trial for the new trial (it also does this with some small probability regardless of whether it makes things better or worse so as not to get stuck in a local minimum). After a while it will get a sequence that is close to the desired length (which is the average length of trial x number of trials). But, only to within some tolerance. If you want it more exact, set:

task{1}.seglenPrecomputeSettings.idealDiffFromIdeal = 0.1;

If you make it too strict, it might not be able to find a good sequence - you might need to increase the number of trials it tries to replace:

task{1}.seglenPrecomputeSettings.maxTries = 1000;

The code computes the average length of trial. This computation can be a bit complicated if you have multiple segments that have randomized trial lengths. In particular, if you have more than 2 randomized trial segments before a synchToVol then we have not yet implemented a computation that can compute the average length. If this is your case, you can either implement it (and let us know so we can include your solution) or just pass your average trial length:

task{1}.seglenPrecomputeSettings.averageLen = 15;

The seglenPrecompute feature can handle what happens when you synchToVol. But it needs then to know how long it takes to acquire each volume (often - but not necessarily - your TR). So you set that in the framePeriod field:

task{1}.seglenPrecomputeSettings.framePeriod = 1.5;

You can try it by running the test program. The test program requires 25 volumes to run, so test it by doing the following:

mglSimulateRun(1.5,25,5);
taskTemplateSeglenPrecompute(2);

This will print out information about what trials have been computed:

(initTask) Computing 5 trials with average length 7.500000
(initTask:seglenPrecompute) Total length: 37.50 Desired length: 37.50 Diff: 0.00
(initTask:seglenPrecompute) 25 volumes needed
(initTask:seglenPrecompute) trialLen: 6.00 freq: 0.40 (2/5, 0.33 expected)
(initTask:seglenPrecompute) trialLen: 7.50 freq: 0.20 (1/5, 0.33 expected)
(initTask:seglenPrecompute) trialLen: 9.00 freq: 0.40 (2/5, 0.33 expected)

Note that the expected frequencies and the actual frequencies don't match. That's ok. For so few trials it shouldn't and that is not what we are trying to match anyway. We are just trying to match the overall length of the task.

You can also set it up so that a segment has a set of durations that it can run for with associated probabilities and get trial sequence with a fixed length:

task{1}.segdur{3} = [1 3 8];
task{1}.segprob{3} = [0.2 0.5 0.3];

This then makes segment 3 have durations either 1, 3 or 8 seconds long. The probabilities for each duration is then 0.2, 0.5 and 0.3 respectively. Note that if you omit segprob then the probabilities will be equal for each duration. you can test how this works by running:

taskTemplateSeglenPrecompute(3);

You should the following:

(initTask) Computing 20 trials with average length 12.800000
(initTask:seglenPrecompute) Total length: 256.00 Desired length: 256.00 Diff: 0.00
(initTask:seglenPrecompute) trialLen: 10.00 freq: 0.20 (4/20, 0.20 expected)
(initTask:seglenPrecompute) trialLen: 12.00 freq: 0.50 (10/20, 0.50 expected)
(initTask:seglenPrecompute) trialLen: 16.00 freq: 0.30 (6/20, 0.30 expected)

Note that in the case above, the proportion of trials happens to come out just as expected, but this is not necessarily the case. The code is trying to keep the total experiment length the same. It is *not* trying to get the proportion of trials the same each run. In fact, randomly you should have some times in which you have more or less than the expected probability. For example, another run gives:

(initTask) Computing 20 trials with average length 12.800000
(initTask:seglenPrecompute) Total length: 256.00 Desired length: 256.00 Diff: 0.00
(initTask:seglenPrecompute) trialLen: 10.00 freq: 0.10 (2/20, 0.20 expected)
(initTask:seglenPrecompute) trialLen: 12.00 freq: 0.65 (13/20, 0.50 expected)
(initTask:seglenPrecompute) trialLen: 16.00 freq: 0.25 (5/20, 0.30 expected)

If you want to see even more (or less) verbose information about the precompute, you can set the verbose parameter:

task{1}.seglenPrecomputeSettings.verbose = 2;

How to get complete control of trial lengths

If you want to have complete control of the segment lengths of individual trials, you can precompute them yourselves. For example, you could precompute the four following trials:

task{1}.seglenPrecompute.seglen{1} = [1 10];
task{1}.seglenPrecompute.seglen{2} = [2 1];
task{1}.seglenPrecompute.seglen{3} = [3 4];
task{1}.seglenPrecompute.seglen{4} = [2 1 5];

And they will be played in that order. Note that if you run for more than four trials in this case, you will cycle back to the first trial in the list. Also note that not all the trials have to have the same number of segments. You can do whatever makes sense to you. Also, you can add any variable you like to be computed on a trial by trial basis. This allows you to associate it each seglen with some variable that has meaning to you. For example:

task{1}.seglenPrecompute.myVar = {'huh','wow','uhm','yowsa'};

Then the variable myVar shows up with the matching value in task.thistrial.myVar. To test this run:

taskTemplateSeglenPrecompute(4);

Task function reference

mglEditScreenParams

availability: mgl 2.0
You can now use a GUI to set all of your screen parameters so that when you call initScreen everything is customized for your system. The settings will be stored across matlab sessions in a file called .mglScreenParams in your home directory. If you had been passing in a screenParams field to initScreen in your mgl program, you should convert to using mglEditScreenParams. To set the parameters you need to have mrTools installed (see Getting Started).

Start by setting your parameters:

mglEditScreenParams

This will bring up a dialog that looks like this:

This first dialog box allows you to manage all of the displays that you have for your computer. You can keep multiple different screen parameters if you have multiple displays on your computer for instance (one for each display). You may also want to keep different display settings for different uses, for example if you like to debug your code with a windowed context, then create another display parameter for the windowed context and set the displayName to say, debug. Then when you run initScreen you can select which display settings to use by passing in the displayName. For example, initScreen('debug').

In this dialog box you can add, delete or restore default settings for each display setting.

Next, you should edit the screen parameters for your desired display by selecting the display and clicking OK. You should see a dialog like this:

Here you can set all of your screen parameters (including the displayName which is how you access the settings with initScreen). You can test the settings using the “testSettings” button. The help button gives you information about all the settings. Here is a list of what the settings do:

Name Function
computerName The name of the computer for these screen parameters
displayName The display name for these settings. This can be left blank if there is only one display on this computer for which you will be using mgl. If instead there are multiple displays, then you will need screen parameters for each display, and you should name them appropriately. You then call initScreen(displayName) to get the settings for the correct display
useCustomScreenSettings If you leave this unchecked then mgl will open up with default screen settings (i.e. the display will be chosen as the last display in the list and the screenWidth and ScreenHeight will be whatever the current settings are). This is sometimes useful for when you are on a development computer – rather than the one you are running experiments on
screenNumber The screen number to use on this display. 0 is for a windowed contex. Less than 1 gives a transparent windowed context
screenWidth The width in pixels of the screen
screenHeight The height in pixels of the screen
framesPerSecond Refresh rate of monitor
displayDistance Distance in cm to the display from the eyes. This is used for figuring out how many pixels correspond to a degree of visual angle
displaySize Width and height of display in cm. This is used for figuring out how many pixels correspond to a degree of visual angle
displayPos This is only relevant if you are using a windowed context (e.g. screenNumber=0). It will set the position of the display in pixels where 0,0 is the bottom left corner of your display.
autoCloseScreen Check if you want endScreen to automatically do mglClose at the end of your experiment.
flipHorizontal Click if you want initScreen to set the coordinates so that the screen is horizontally flipped. This may be useful if you are viewing the screen through mirrors
flipVertical Click if you want initScreen to set the coordinates so that the screen is vertically flipped. This may be useful if you are viewing the screen through mirrors
hideCursor Click if you want initScreen to hide the mouse for this display.
backtickChar Set the keyboard character that is used a synch pulse from the scanner. At NYU this is the backtick character. If you use a different character enter it here. If you enter a number then this will be interpreted as a character code (see mglCharToKeycode).
responseKeys Sets which keys you want to use for response keys. This should be a space delimited string, for example: 1 2 3 4 5 6 7 8 9 is the default, which uses the number keys. If you want to use keycodes, for example to use the numberic key pad, you can do: k84 k85 k86 k87 k88 k89 k90 k92 k93
eatKeys Sets whether to eat keys. That is, any key that initScreen uses, for example the response keys and the backtickChar will not be sent to the terminal. See the function mglEatKeys for more details.
saveData Sets whether you want to save an stim file which stores all the parameters of your experiment. You will probably want to save this file for real experiments, but not when you are just testing your program. So on the desktop computer set it to 0. This can be 1 to always save a data file, 0 not to save data file,n>1 saves a data file only if greater than n number of volumes have been collected)
calibType Choose how you want to calibrate the monitor. This is for gamma correction of the monitor. Find latest calibration works with calibration files stored by moncalib, and will look for the latest calibration file in the directory task/displays that matches this computer and display name. If you want to specify a particular file then select that option and specify the calibration file in the field below. If you don''t have a calibration file created by moncalib then you might try to correct for a standard gamma value like 1.8 (mac computers) or 2.2 (PC computers). Or a gamma value of your choosing
calibFilename Specify the calibration filename. This field is only used if you use Specify particular calibration from above
diginPortNum Click this if you want to use the National Instruments cards digital io for detecting volume acquisitions and subject responses. If you use this, the keyboard will still work (i.e. you can still press backtick and response numbers. This uses the function mglDigIO – and you will need to make sure that you have compiled mgl/util/mglPrivateDigIO.c
diginAcqLine This is the line from which to read the acquisition pulse (i.e. the one that occurs every volume)
diginAcqType This is how to interpert the digial signals for the acquisition. If you want to trigger when the signal goes low then set this to 0. If you want trigger when the signal goes high, set this to 1. If you want to trigger when the signal changes state (i.e. either low or high), set to [0 1]
diginResponseLine This is the lines from which to read the subject responses. If you want to specify line 1 and line 3 for subject response 1 and 2, you would enter [1 3], for instance. You can have up to 7 different lines for subject responses.
diginResponseType This is how to interpert the digial signals for the responses. If you want to trigger when the signal goes low then set this to 0. If you want trigger when the signal goes high, set this to 1. If you want to trigger when the signal changes state (i.e. either low or high), set to [0 1]
testDigin Click to test the digin settings
testSettings Click to test the monitor settings

initScreen

purpose: initializes the screen
usage: myscreen = initScreen(<myscreen>,<randstate>)

argument value
myscreen Contains any desired initial parameters, can be left off if you are just using all defaults
randstate Sets the initial status of the random number generator. This can either be an integer value, or it can be the field myscreen.randstate to set the state back to what it was on a particular experiment.

This function initializes the screen by calling mglOpen, and also handles a number of different default initialization procedures such as setting up the gamma table with the correct linearization table. You should call this once at the beginning of the experiment. The variable myscreen will contain many fields associated with the status of the screen and records events like volume acquisitions and trial/segment times etc.

There are a number of parameters that will be specific to your computer/display that you can set by setting the screenParams field of myscreen to save information about your setup:

myscreen.screenParams{1} = {'yoyodyne.cns.nyu.edu',[],2,1280,1024,57,[31 23],60,1,1,[],[],[0 0]}; 
myscreen = initScreen(myscreen);

This will set parameters for your screen. The parameters in order are

MGL 2.0: The initScreen program looks for a file called by default mgl/task/mglScreenParams.mat which contains a cell array of computer display information parameters like the above (as long as you don't set myscreen.screenParams before calling initScreen). This way you can save a file which contains information about all the computers in your lab and always load that up rather than having each stimulus program contain all the information. You can do this by changing the name of the file that initScreen loads by setting:

screenParams{1} = {'yoyodyne.cns.nyu.edu',[],2,1280,1024,57,[31 23],60,1,1,[],'yoyodyne',[0 0]};   
screenParams{2} = {'terranova.bnf.brain.riken.jp',[],0,800,600,57,[31 23],60,1,0,[],[],[0 0]};   
save ~/Desktop/myScreenParams.mat screenParams
mglSetParam('screenParamsFilename','~/Desktop/myScreenParams.mat');

initStimulus

purpose: initializes the global stimulus variable name
usage: myscreen = initScreen('stimulusName',myscreen);

argument value
stimulusName string that contains the name of the global variable that is used for your stimulus (i.e. if you had global stimulus, then this should be 'stimulus')
myscreen myscreen variable returned by initScreen

Note that this function, only needs to be called if you want to save the stimulus in your stim file. Since stimulus is a global variable, if you call this function, at the end of the experiment it will get the global variable with the name you specified here and save it in your stim file. If you do not need to save your stimulus variable, you do not need to call this function.

initTask

purpose: initializes a task variable
usage: [task myscreen] = initTask(task,myscreen,startSegmentCallback,screenUpdateCallback, trialResponseCallback, <startTrialCallback>, <endTrialCallback>, <startBlockCallback>)

argument value
task Parameters for the particular task (note this must be a struct not a cell array, for a cell array, call initTask fore each element of the cell array.
myscreen Variable returned by initScreen
startSegmentCallback Function pointer that will be called at start of a segment
screenUpdateCallback Function pointer that will be called every screen update (i.e. for a 60Hz buffer once every 1/60 of a second)
trialResponseCallback Function pointer that will be called when the subject responds and getResponse is set
startTrialCallback Function pointer that will be called at start of a trial
endTrialCallback Function pointer that will be called at end of a trial
startBlockCallback Function pointer that will be called at start of a block

The task variable gets set up as explained above. Here is a list of valid fields:

field value
verbose display verbose message when running tasks (probably shouldn't be set for real experiment since print statements can be slow)
parameter task parameters
seglen array of length of segments (used when not using segmin and segmax)
segmin array of minimum length of segment
segmax array of maximum length of segment
segdur a cell array (not all cells have to be set they can be empty) which specifies an array of values for the length of a segment for example segdur{2} = [1 3 5] would set segment 2 to have durations 1, 3 or 5 seconds long
segprob a cell array (not all cells have to be set they can be empty) which specifies an array of values for the probability of how often each segdur will happen, so if we have segdur{2} = [1 3 5] and segprob{2} = [0.4 0.5 0.1], then this would set segment 2 to have durations 1, 3 or 5 seconds long with probability 0.4 0.5 and 0.1 respectively. Note that if no segprob is set for a non-empty segdur than each of the durations in segdur will occur with equal probability.
synchToVol array where one means that the segment will synch to the next volume acquisiton once the segment is finished.
getResponse array where one means to get subject responses during that segment, set to zero means that subject responses will be ignored and the responseCallback will not be called
numBlocks number of blocks of trials to run before stopping
numTrials number of trials to run before stopping
waitForBacktick wait for a backtick before starting task phase
random randomize the order of parameters for each trial when set to 1, otherwise have the parameters go in order
timeInTicks when set to 1, segment legnths are in screen updates (not in seconds)
timeInVols when set to 1, segment lengths are in volumes (not in seconds)
segmentTrace internal variable that controls what trace this task will use to save out segment times (usually you will not set this)
responseTrace internal variable that controls what trace this task will use to save out subject responses (usually you will not set this)
phaseTrace internal variable that controls what trace this task will use to save out the phase number (usually you will not set this)
parameterCode For parameters that have groups
private A parameter that you can do whatever you want with
randVars random variables
fudgeLastVolume When you synchToVol or keep time in volumes, and want to have the experiment run for a set number of trials, the experiment won't usually end because in the last segment it is waiting for a volume to come in that never will. If you set this to 1, it will fudge that last one so that the experiment ends one TR after the last volume is aquired.

updateTask

purpose: updates the task
usage: [task myscreen phaseNum] = updateTask(task,myscreen,phaseNum)

argument value
task task variable, note task must be a cell array. If you only have one task phase, make phaseNum=1 and task a cell array of length one.
myscreen myscreen variable returned by initScreen
phaseNum The task phase you are currently updating. If you only have one phase, set to 1, for multiple phases, update Task will take care of switching from one phase to the next.


tickScreen

purpose: updates the screen
usage: [myscreen task] = tickScreen(myscreen,task);

argument value
myscreen myscreen variable returned by initScreen
task task variable

This function calls mglFlush to update the screen when it is needed and also checks for volumes and keys etc. Called with in main loop.

upDownStaircase

Implements a staircase for control of stimulus variable values. Type 'help upDownStaircase' for details. Also see taskTemplateStaircase.m for examples of using this function.

jumpSegment

Allows you to force a move to the next segment or the next trial:

task = jumpSegment(task) % this will end the segment and move to the next one
task = jumpSegment(task,inf) % this will end the trial and start a new trial

Eyelink

Overview

You can use the Eyelink eye tracker with mgl and the task code. First, make sure that the mgl eyelink is compiled by following instructions here.

  1. Using mglEditScreenParams select use of the Eyelink by setting eyeTrackerType to Eyelink.
  2. Set parameters such as the number of calibration points you want to use, how far those calibration points are from the center of the screen, sample rate and what data you want to save, by using mglEyelinkParams.
  3. In your experiment make sure to call eyeCalibDisp (see the task program taskTemplateSaccade for an example)
      myscreen = eyeCalibDisp(myscreen);
  4. This will run the calibration routine, see under mglEyelinkSetup for a full reference on text commands that can be used to calibrate the scanner. As a quick guide, the basic procedure goes something like this.
    1. Hit ENTER to bring up an image of the eye
    2. Make sure that the eye is centered appropriately.
    3. Hit ESC to close this image of the eye.
    4. HIT C to run the calibration routine. You will need to hit enter after fixating the first fixation point, after that you should just be able to fixate each fixation point in turn and it will go through the calibration.
    5. Hit Enter to accept the calibration.
    6. Hit ESC to end the calibration routine and start your stimulus program.
  5. Upon ending your stimulus program, mgl will save a file with an edf extension that stores the eye position information.
  6. You can now read the saved data using getTaskEyeTraces.

To test this whole procedure, you may wish to use the stimulus program taskTemplateSaccade. This program has the subject saccade to a number of eccentric positions and back to the fixation. After you are done, you should have saved eye traces that look like ones below.

Optionally, you may also want to test eye position for significant effects across conditions, which can be tested using the function (see help in function):

mglCheckEyepos

mglEyelinkParams

A simple GUI that allows you to set parameters for the EyeLink eye tracker. You can set the whether you want 5 or 9 calibration points (set calibrationType to HV5 or HV9, respectively). You can set the sample rate and which data you want to save. You can also set the calibration area - the area of the screen that you want to use for calibration. Sometimes it is useful to move the calibration in from the edges of the screen when you are having trouble getting pupil lock at far eccentricities.

getTaskEyeTraces

getTaskEyeTraces can be used to load the eye tracker data. It works like getTaskParameters, but also returns the eye traces sorted by trial. You can load up the data for an experiment. For example, from a run of taskTemplateSaccade

>> getTaskEyeTraces('100811_stim01')
(getTaskEyeTraces) Opening edf file /Users/justin/data/eyetracker/10081101.edf took 3 secs 374 ms
(getTaskEyeTraces) Extracting trial by trial data for 63 trials took 0 secs 842 ms
(getStimvolFromVarname) taskNum=[1], phaseNum=[1], segmentNum=[1]
(getstimvol) Same trial in multiple conditions.
(getStimvolFromVarname) targetAngle=0.0000000 targetRadius=8.0000000: 3 trials
(getStimvolFromVarname) targetAngle=45.0000000 targetRadius=8.0000000: 4 trials
(getStimvolFromVarname) targetAngle=90.0000000 targetRadius=8.0000000: 4 trials
(getStimvolFromVarname) targetAngle=135.0000000 targetRadius=8.0000000: 4 trials
(getStimvolFromVarname) targetAngle=180.0000000 targetRadius=8.0000000: 4 trials
(getStimvolFromVarname) targetAngle=225.0000000 targetRadius=8.0000000: 4 trials
(getStimvolFromVarname) targetAngle=270.0000000 targetRadius=8.0000000: 4 trials
(getStimvolFromVarname) targetAngle=315.0000000 targetRadius=8.0000000: 4 trials
(getStimvolFromVarname) targetAngle=0.0000000 targetRadius=12.0000000: 4 trials
(getStimvolFromVarname) targetAngle=45.0000000 targetRadius=12.0000000: 4 trials
(getStimvolFromVarname) targetAngle=90.0000000 targetRadius=12.0000000: 4 trials
(getStimvolFromVarname) targetAngle=135.0000000 targetRadius=12.0000000: 4 trials
(getStimvolFromVarname) targetAngle=180.0000000 targetRadius=12.0000000: 4 trials
(getStimvolFromVarname) targetAngle=225.0000000 targetRadius=12.0000000: 4 trials
(getStimvolFromVarname) targetAngle=270.0000000 targetRadius=12.0000000: 4 trials
(getStimvolFromVarname) targetAngle=315.0000000 targetRadius=12.0000000: 4 trials

This will return a structure similar to the one returned by getTaskParameters except there will be an “eye” field:

ans = 

                  nTrials: 63
              trialVolume: [1x63 double]
                trialTime: [1x63 double]
             trialTicknum: [1x63 double]
                   trials: [1x63 struct]
                 blockNum: [1x63 double]
            blockTrialNum: [1x63 double]
                 response: [1x63 double]
             reactionTime: [1x63 double]
    originalTaskParameter: [1x1 struct]
           responseVolume: [1x63 double]
                parameter: [1x1 struct]
                 stimfile: [1x1 struct]
                      eye: [1x1 struct]

This will have the eye position data sorted by trial:

ans = 

     hPos: [63x2002 double]
     vPos: [63x2002 double]
    pupil: [63x2002 double]
     time: [1x2002 double]

Note that hPos and vPos fields contains eye position data with respect to the center of the screen in degrees.

It will also display a figure (this can be turned off by setting the input parameter dispFig=0) of the eye position data sorted by trial type

Sample programs can be found in the /Task subfolder, and include:

When things don’t work, there are some very simple bugs you should check for before losing hope.

Short segment lengths cause dropped frames

Setting the length of the segments of a task to “0 second” can result in lots of dropped frames. This happens because mgl adjusts automatically for the difference between the expected (“0 second”) and the actual length of a segment (“always > 0 seconds”). Thus, if the lengths are set to “0 seconds” mgl is forced to make continual adjustments, what affects the speed of the frames display and causes dropped frames.

Return variables

Many of the routines return multiple values–it is really important that you receive these properly. For example, initTask returns both the task and myscreen. Make sure that you call it as follows:

[task{1} myscreen] = initTask(task{1},myscreen,@startSegmentCallback,@screenUpdateCallback,@responseCallback);

If you did not have the myscreen in the left hand side, then initTask will not set fields (primarly myscreen.stimtrace) correctly in your myscreen–which can make saving out traces incorrect. Look out for this not only in initTask but in any function like updateTask to make sure you are always setting the appropriate return variables correctly (check the help on the function–or model your calls after the ones in the templates).

Screen flashes

Sometimes the screen might appear to flash momentarily with your stimulus in a way you don't expect. Often this is due to not calling mglClearScreen at the appropriate place (like at the beginning of a segment or at the beginning of the screenUpdateCallback). The reason for this occurring is often because you are drawing to the back buffer of the double buffered screen and it already has something from before when it was being displayed. Clearing the screen as you start drawing will insure that you don't have any junk from the last screen draw that will show up.

You may also notice screen flashes if you use mglSetGammaTable to reset the gamma table at certain points in your experiment. mglSetGammaTable relies on Mac OS X functions that are not part of the Open GL library, and therefore will not necessarily have the correct timing. Sometimes the gamma table will get set too soon and sometimes too late. If you notice this problem you should add a brief dummy segment in your trial to set the gamma table before the segment that contains your stimuli. Minimizing the command window during the experiment has also been found to substantially reduce these timing issues.

Matlab control

Don't let the window from which you are calling your code be on the screen that is taken over by MGL, or you will lose the ability to stop the code from running or exit if your code is interupted by an error. If you do this, you can always type option-command-ESC (brings up Force-quit applications) to close the window. This will also quit your matlab session.

Clear all

Mgl keeps the status of the display in the global variable called MGL. You can look at the values set in MGL by doing:

global MGL 
MGL

Be aware that if you clear the MGL variable then mgl will no longer know the status of the screen. The current version handles this by closing the display if you clear the MGL variable. If for some reason, you get stuck with an open display that you cannot close, you can try to call mglOpen again and then close. If you have reset the gamma table, you can go into your System Preferences/Displays/Color and reset the gamma table back to normal there. On Linux, you can use xgamma to set the gamma table, or easier, use NVIDIA's settings manager or the ATI settings manager to restore gamma tables. If you still cannot close the display, you can always do option-open apple-escape and quit out of matlab (on Mac). On Linux, Ctrl-C may work, or you may have to kill the matlab process from a terminal. If you only have one screen and it is locked, you can type Ctrl+Alt+F5 to drop out of the X server temporarily; this will allow you to login and kill the Matlab process without restarting X (Ctrl+Alt+F7 to return to X).

Spelling, capitalization, and periods

If an important variable (e.g. stimulus) is misspelled (e.g. sitmulus), or a variable or function name mis-capitalized (e.g. inittask instead of initTask), or you forget to put the period between a variable name and a field name (e.g. stimuluscontrast instead of stimulus.contrast) things won’t work and it’ll be very confusing! MGL will check for some things that it knows about, but it can’t know how you’ve named your variables…

Your task doesn't exit when it's over

First, make sure you've specified a certain number of trials, otherwise it'll go on forever.

Then, make sure you have a while loop around the updateTask call checking the phase number. Even if you only have one phase, you still need to give updateTask a phaseNum variable as input, because the code will only end when the phaseNum gets too big for the while loop (and updateTask increases the phaseNum after all the trials have been run)

Set the data directory

So as to always know where your data will be saved, it's good to set the data directory by doing:

myscreen.datadir = datadirname;

where datadirname is the full path of the directory to which you'd like the data to be saved.

Make sure your displays aren’t mirrored.

Sometimes nothing at all will happen. This can be because your screens are mirrored.

Some common error messages that have to do with cell arrays and how to fix them

Error message saying you haven't specified task.segmin and task.segmax

Make sure you've called initTask with the exact task and phase. For example:

task{1}.seglen = 1;
task{1} = initTask(task{1},myscreen,@startSegmentCallback,@screenUpdateCallback,@responseCallback);

or

task{1}{1}.seglen = 1;
task{1}{1} = initTask(task{1}{1},myscreen,@startSegmentCallback,@screenUpdateCallback,@responseCallback);

Error message saying 'Cell contents reference from a non-cell array object,'

Make sure you are calling updateTask with a cell array. This can be confusing, because even if you only have one task and only one phase, you still need to define task{1} (rather than just 'task') and then call updateTask with 'task' - e.g.:

task{1}.seglen = 1;
task{1} = initTask(task{1},myscreen,@startSegmentCallback,@screenUpdateCallback,@responseCallback);
phaseNum = 1;
while (phaseNum <= length(task)) && ~myscreen.userHitEsc
 [task myscreen phaseNum] = updateTask(task,myscreen,phaseNum);
end

If you're running two tasks, and need to differentiate them, then even if they each only have one phase, you must define task{1}{1} and task{2}{1} and then call updateTask with task{1} and task{2}. For example:

task{1}{1}.seglen = 1;
task{1}{1} = initTask(task{1}{1},myscreen,@startSegmentCallback,@screenUpdateCallback,@responseCallback);
task{2} = setSecondTask(myscreen); % as long as setSecondTask returns a cell array
phaseNum = 1;
while (phaseNum <= length(task{1})) && ~myscreen.userHitEsc
 [task{1} myscreen phaseNum] = updateTask(task{1},myscreen,phaseNum);
 [task{2} myscreen] = updateTask(task{2},myscreen,1);
end

Error message saying 'Attempt to reference field of non-structure array,'

Make sure that you call updateTask and also return from updateTask with the same cell array (see above).

Error message 'Input argument "tnum" is undefined,'

Make sure you are passing in a phaseNum argument in your updateTask function call.

Problem starting/stopping the eye tracker

To control the ASL eye tracker through digital I/O you must have the file

mgl/task/utils/readDigPort/writeDigPort.c

mex'd. You can do

cd mgl/task/utils/readDigPort
mex writeDigPort.c

This compiles the program to send the digital pulse to the eye tracker. Also, if your task crashes at the beginning after it prints out

Dev1/port2

or something similar to that, you probably just need to recompile writeDigPort.c. If you continue to have problems and want to give up, you can delete the mexfile writeDigPort.mexmac and then mgl won't try and set the digital port (This is the default condition that the mgl library is in, because most computers don't have the NI card installed).

List of some publications that have used mgl. Please contact us if you have an update to this publication list.

Contributors

Design and Implementation

Other contributors

Support

We thank BSI Neuroinformatics Project for generous support.