This page provides help for using MGL task structures to program experiments.

You can also check out some examples.

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.

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:

  • Task: Task refers to the overall experiment. The task is the top level structure. It contains all the parameters that you are testing as well as the information about how the trials are to be run. A task might be the parameters for a set of trials in which you show different visual stimuli. Or a set of trials that run a psychophysical staircase. Note that in some cases you might have more than one task running at the same time. For example, if you are running a retinotopy scan, you may want to have the retinotopic stimuli as one task and a staircased fixation task as the second task.
  • Phases: Tasks may sometimes have more than one phase. For example you may want to show an adaptation stimulus for 30 seconds at the beginning of your experiment in one phase, and then go on to the next phase of the experiment in which you will have randomized trials.
  • Blocks: A block is a set of trials in which each combination of parameters is presented in one trial. The code takes care of properly randomizing your trials so that in each block of trials each stimulus type is presented once. (You can also choose not to randomize).
  • Trials: A single trial of an experiment.
  • Segments: Segments divide up the time in a trial. For example you may have one segment with a fixation cross, another segment where the stimulus is presented and a final segment where the subject responds. What each segment does, how many you have and how long they last are all up to you and define how a trial works.

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:

  • taskTemplateStaircase: This is a task that implements a simple staircase task.
  • taskTemplateContrast10bit: Shows you how to use the 10-bit capacity for fine contrast steps
  • taskTemplateDualMain: This is an example of the main task in a dual task pair, to show how to run dual tasks.
  • taskTemplateDualSubsidiary: This is an example of the subsidiary task in a dual task pair, to show how to run dual tasks.

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 add a line like the following:

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

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

  • computerName
  • displayName (optional–for computers with multiple displays like lcd and projector)
  • displayNumber
  • screenWidth (in pixels)
  • screenHeight (in pixels)
  • displayDistances (in cm)
  • displaySize (in cm)
  • framesPerSecond (in Hz)
  • autoCloseScreen (1 to close screen at end of experiment, 0 to leave it open)
  • saveData (1 to save data file, 0 not to save data file,n>1 saves a data file only if you exceed n number of volumes)
  • monitorGamma (The monitor gamma to correct for if you do not have a calibration file. Macs are supposed to have a gamma of 1.8)
  • calibFilename (the name of the calibration file–usually just the computer name–see below under moncalib)
  • flipHV (Whether to flip the screen horizontally and/or vertically–an array of length two 0=no flip, 1 = flip)

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.

  • waitForBacktick=1: The task phase will only start running after we receive a keyboard backtick (`).
  • seglen = 10: The segment will run for 10 seconds.
  • numBlocks = 1: There will be one block of trials before we run on to the next phase of the task.
  • paramater.dir = 0: We set the parameter dir to have a value of 0.
  • parameter.coherence = 0: We set the parameter coherence to have a value of 0.

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;
% and set to remember the values for dir
task{2}{2}.writetrace{1}.tracenum = 1;
task{2}{2}.writetrace{1}.tracevar{1} = 'dir';
task{2}{2}.writetrace{1}.usenum = 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];

Finally, to keep track of what direction was shown on one trial we can keep a “trace” of the dir parameter. This is not the ony way to get the information about what was shown on what trial, but it is fairly convenient. A trace will start with the value 0 and then on the segment of our choosing will change to the value of the parameter that was presented on the trial. By plotting the trace you can see the timing of your trials and the parameter that was chosen. In this case, we want to write out the direction parameter on the first segment of the trial, so we have writetrace{1} (if we wanted the second segment we would do writetrace{2} etc). We are going to use the 1st trace to store our information (you can have as many traces as you want to track different variables). We want to save the parameter 'dir' and instead of writing out 0, 60, 120 etc. we “usenum” which means that we will write out the corresponding number (i.e. 1, 2, 3 etc) used to represent the parameter.

% and set to remember the values for dir
task{2}{2}.writetrace{1}.tracenum = 1;
task{2}{2}.writetrace{1}.tracevar{1} = 'dir';
task{2}{2}.writetrace{1}.usenum = 1;

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

See more details on callback functions below

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.

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

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 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};

What if I have parameters that are not single numbers

You may have a parameter that is an array rather than a single number. Again, do something like the above (1.3)

task.parameter.stringNum = [1 2 3];
task.strings = {'string1','string2','string3'}

and get the appropriate string on each trial by doing:

task.thistrial.thisstring = task.strings{task.thistrial.stringNum};

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;

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.

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 possible:

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 above

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). Caution: whichButton is defined by the index of the button pressed in the list of possible buttons. If your button list is [1 2 3 4 5] and the user presses “5”, you will get back “5”. If your button list is [5] and the user presses “5”, you will get back “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];

If you want to mouse button events instead of keyboard events, then set task.getResponse to 4:

task.getResponse = [0 4];

You will then get back fields:

task.thistrial.mouseButton
task.thistrial.mouseWhen
task.thistrial.mouseX
task.thistrial.mouseY

Which will contain the relevant information about the mouse event. You can get either mouse or keyboard events by doing:

task.getResponse = [0 3];

Note that if you do not set getResponse, keyboard and mouse events will be ignored and not recorded into the stimfile.

You may also set a getResponse segment to 2. This is no longer necessary since we get keyboard presses using a background thread which gets timestamps directly from the OS, but it used to be used to do something 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 used to be (but no longer is) necessary to get 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)

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.

Saving data

stim files

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

getTaskParameters('yymmdd_stimnn.mat');

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

You can also access different aspects of your task variables with the following helper functions:

getTaskParameters

Gets all the info about your task and its parameters. Can either be called on a stimfile:

e = getTaskParameters('yymmdd_stimnn.mat');

or on the myscreen/task variables:

e = getTaskParameters(myscreen,task);

This will return a structure that looks like this:

>> e = getTaskParameters('100618_stim02')

e = 

                  nTrials: 250
              trialVolume: [1x250 double]
                trialTime: [1x250 double]
             trialTicknum: [1x250 double]
                   trials: [1x250 struct]
                 blockNum: [1x250 double]
            blockTrialNum: [1x250 double]
                 response: [1x250 double]
             reactionTime: [1x250 double]
    originalTaskParameter: [1x1 struct]
           responseVolume: [1x250 double]
                 randVars: [1x1 struct]
                parameter: [1x1 struct]

Note that in the fields parameter and randVars you will have access to the trial-by-trial values of your parameters and randVars. In this case we have a parameter called scrambleFactor:

>> e.parameter

ans = 

    scrambleFactor: [1x250 double]

You can also get more complete information about each trial (the segment occurrence times, etc.) in the field trial. For example, to access information about the 15th trial:

>> e.trials(15)       

ans = 

          response: 2
    responseVolume: []
      reactionTime: 1.28420000000915
            traces: [1x1 struct]
           segtime: [35.0010760000005 35.5003559999896]
            volnum: [0 0]
           ticknum: [82708 83999]

Finally, note that all volume numbers represent the beginning of a trial or a segment and are rounded to the closest volume number. Thus if your trial or segment started at time 0.76 seconds and your frame period (TR) was 1.5 seconds, then you would see a volume number of 2 rather than 1.

getTaskVarnames

Gets a cell array of the variables names in your task

varnames = getTaskVarnames(task);

getParameterTrace

Gets a trace of the variable called for

plot(getParameterTrace(myscreen,task,'varname');

getStimvolFromVarname

Gets a cell array that contains the stimulus volumes for a particuar variable name

stimvol = getStimvolFromVarname(varname,myscreen,task);

getVarFromParameters

Gets the variable settings for each trial

getVarFromParameters('varname',getTaskParameters(myscreen,task));

Traces

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

writeTrace

The following information is only useful for people who need to save extra information in the traces.

Saving your own variables

If you want to save information on values that you calculate yourself, you will call the function writeTrace. The syntax is:

myscreen = writeTrace(data,tracenum,myscreen,force);

where data is the scalar value you want to save. Tracenum is the trace you want to save to. Note that the first tracenum from the above section is actually saved to myscreen.stimtrace which is usually set to 5. Therefore you will want to save in some trace above myscreen.stimtrace–for example myscreen.stimtrace+1. You will usually want to set force = 1, see the help on writeTrace if you need more information.

This writeTrace function can be called anywhere in your code, for example in the startSegmentCallback. If you had a variable called myParam set to some value you want to save, you will add the code:

myscreen = writeTrace(myParam,myscreen.stimtrace+1,myscreen,1);

Then after calling getTaskParameters, your variable settings will be available in the traces field.

choosing a directory

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.


How-Tos

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). 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].

  • Tricky–When using the automated calibration via the serial port, the program will ask you to turn on the PR650 and then press 'return' within 5 secs. You might not want to press 'return' right away, or you may get something like this on the photometer:
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.

  • Tricky2–When doing the automated calibration, turn off screensavers and energysaver, otherwise the screen will go blank after a while and you'll be measuring luminance of blank sreens.

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

  • NVIDIA GeForce FX 6600 (In the G5 in the magnet room)
  • NVIDIA GeForce FX 7300 GT (brownie Mike Landy's psychophysics room)
  • NVIDIA GeForce FX ????? (Jackson the G5 in the psychophysics room)

ATI 10 bit cards:

  • any Randeon card for desktop computers above series 7000 has 10-bits DAC resolution (laptop cards don't have it necessarely or drivers do not access it)
  • some more information about this can be found on Denis Pelli webpage [3] and on the Psychtoolbox.org discussion group [4].

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

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.

Function reference

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.

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
segquant array of quantization of segment lengths (used with segmin and segmax–i.e. if you want segments to be randomized in steps of 0.6 seconds, then set the sequant for that segment to be 0.6)
synchToVol array where one means that the segment will synch to the next volume acquisiton once the segment is finished.
writeTrace traces to write out (usually internal variable, that you do not have to set)
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 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