====== MGL ======
% 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 [[:mgl:TaskReferenceOverview|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 ([[mgl:beta|2.0]]) can be retrieved using [[http://subversion.apache.org/|subversion]]:
svn checkout http://gru.brain.riken.jp/svn/mgl/trunk mgl
We recommend using subversion (see [[grupub:svn|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 [[http://developer.apple.com/tools/xcode/index.html|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 [[http://subversion.apache.org/|subversion website]]. If you are really unable to get subversion, you can download [[http://gru.brain.riken.jp/download/mgl.tar.gz|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 [[:mgl:gettingStarted#Initial Setup|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 [[mgl:beta#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 [[http://subversion.apache.org/|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 ======
- 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.
- Make sure to [[mgl:beta#keyboard_events|enable access for assistive devices]] so that you can use the function mglGetKeyEvent, mglGetMouseEvent, mglPostEvent, mglSimulateRun and mglEatKeys.
- There are a few functions (mglEditScreenParams and mglDoRetinotopy) that require the mrTools GUI functions to be installed. If you are not already using [[mrTools:overview|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 [[mgl:download#older_versions_of_mgl|1.5]] only), then you may need to [[mgl:gettingStarted#recompiling_mgl|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 ======
* mgl/mgllib: The main distribution that has all functions for displaying to the screen.
* mgl/task: A set of higher level routines that set up a structure for running tasks and trials. Relies on functions in mgl/mgllib. You do no need to use any of these functions if you just want to use this library for drawing to the screen.
* mgl/utils: Various utility functions.
====== 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 [[ftp://ftp.mirrorservice.org/sites/sourceware.org/pub/gcc/|here]]).
See [[mgl:knownissues#problems_compiling|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
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:
/Developer/SDKs/MacOSX10.5.sdk/System/Library/Frameworks/
- 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 [[grupub:svn|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:
* mglText
* mglTextDraw
* mglPlaySound
* mglInstallSound
* mglDescribeDisplays
* mglSwitchDisplay
* mglListener
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.
* no support for font-based text - this needs to be upgraded, I started looking into using FreeType for this which is widely available and would be easy to implement in the same texture-based way that the Mac code relies on. Care must be taken to ensure that the code is maximally portable across platforms, so it may be that some Mac-specific idiosyncracies need to be modified
* note that the stroke text works perfectly under Linux, so unless you are very enamoured with a specific font this is a perfectly usable workaround (though some symbols, eg %, are missing currently)
* some differences in the way you specify special keys, but this is generally to Linux' advantage - eg you can use ESC, BACKSPACE etc as names (relies on the XKeySymDef.h or sth like that)
* timing is in general more accurate on Linux, since the clock rate is much higher on modern systems (100-500Hz vs 60Hz on the Mac)
* no parallel port interface yet so you can't use Justin's code for calibration
* no sound - need to decide on a standard to use that is most widely available
* the syncing with OpenGL is idiosyncratic and depends on the graphics card. I have implemented this to use both the SGI video sync extension and the environment variable (both NVIDIA and ATI provide this option). The former is not supported by all OpenGL cards (though most modern ones) and can interact with the environment variable option, so I will make the latter the default, with an option to use the SGI extension when compiling only.
For the time being, only NVIDIA and ATI cards will be supported (because I only have access to those two machines).
* setting screen size and resolution requires the XRandR extension, which is supported on recent X distributions (Xorg 7.0 and later). Older X servers (eg Apple's X11) won't work.
* you need to reuse the X display or you run into memory problems, and the code for doing this needs to be checked for consistency. This is similar to the Mac window situation and relates to the uneasy relationship between Matlab and X. When you open a window, the MGL global variable will contain a window and display pointer that is used on subsequent calls; care must be taken not to clear the MGL variable between calls (once you do so, running MGL is likely to crash Matlab, even if you run clear all, which correctly closes the display).
====== Main screen functions ======
===== mglOpen: Opens the screen =====
**usage:** mglOpen(whichScreen,
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(
file:///Applications/National%20Instruments/NI-DAQmx%20Base/documentation/docsource/daqmxbasecfuncindex.htm
-Follow the instructions [[:mgl:functionreferencedigio|above]] to compile mglDigIO and related functions.
-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 [[http://zone.ni.com/reference/en-XX/help/370466V-01/device_pinouts/6501pinout/|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).
-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.
-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 [[:mgl:taskreference|task code]] in [[mgl:beta|mgl 2.0]] by setting digin using [[mgl:taskReferenceFunctionReference#mglEditScreenParams|mglEditScreenParams]].
===== mglDigIO =====
**availability**: [[mgl:beta|mgl 2.0]] Mac OS X only\\
**usage**: mglDigIO(command,
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)
- Boot up the Eyelink computer.
- 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**
- 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:taskreferenceeyelink|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,
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.
* mglTestAlignment: Alignment of textures
* mglTestDots: Draws dots
* mglTestGamma: GUI controlled gamma
* mglTestLUTAnimation: Gamma LUT animation
* mglTestStencil: Demonstrates stencil functions
* mglTestTex: Draws a gabor
* mglTestTexMulti: Draws many small images to screen
* mglTestText: Draws text
* mglTestKeys: Returns keyboard codes
====== 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:
- Initialize the screen.
- Set up the task structure. The task structure holds information about the parameters you want to randomize over and the timing of your experiment.
- Initialize the stimulus. Here you will create all the necessary bitmaps or display structures that you will need to display your stimulus.
- 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.
- 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 in [[mgl:beta|mgl 2.0]] just use [[mgl:taskReferenceFunctionReference#mglEditScreenParams|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.
* 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;
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 [[mgl:taskReferenceCallbacks|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
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 [[mgl:taskreferenceeyelink|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 [[mgl:taskreference#gettaskparameters|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 [[mgl:taskreferencehowtos#how_to_add_a_new_variable_after_you_have_run_your_experiment|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 [[:mgl:taskreferencehowtos#How_to_run_a_dual_task| 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 [[mgl:TaskReferenceOverview#Create callback functions|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 [[mgl:taskreference#gettaskparameters|here]]
===== getStimvol =====
**usage**: [stimvol stimNames var] = getStimvol(v,'varname',
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 [[http://www.mathworks.com/matlabcentral/fileexchange/loadFile.do?objectId=4952&objectType=file|[1]]]. To use the Keyspan USA-28 adaptor you will need to download a driver from [[http://www.keyspan.com/downloads/homepage_pn_usa28.spml|[2]]].
There are a few points that you should pay special attention to:
* 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.
* 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 [[http://vision.nyu.edu/Tips/VideoCards.html#morebits|[3]]] and on the Psychtoolbox.org discussion group [[http://psychtoolbox.org/PTB-2/mac.html#drivers|[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 =====
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. [[http://usa.gretagmacbethstore.com/index.cfm/act/Catalog.cfm/catalogid/1861/Subcategory/Eye-One%20Solutions/category/Eye-One/browse/null/MenuGroup/_Menu%20USA%20New/desc/Eye-One%20Display%202.htm|[5]]] [[http://www.xritephoto.com/product/optixxr/|[6]]] [[http://www.colorvision.com/|[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 [[mgl:taskreferencefunctionreference#mgleditscreenparams|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:
-Download [[http://joule.ni.com/nidu/cds/fn/p/sn/n23:3478.41.181.5994/lang/en|NI-DAQmx Base]]. You may need to make a free account with NI.
-Make sure that the device (NI USB-6501) has up-to-date firmware, by running FWUpdate (included in Ni-DAQmx Base/bin)
-Restart matlab if you have already run readDigPort (the program has to reinit the driver)
-Documentation is installed and should be available from:
file:///Applications/National%20Instruments/NI-DAQmx%20Base/documentation/docsource/daqmxbasecfuncindex.htm
-More information is available in the discussion of [[:mgl:functionreferencedigio|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:
- Set up a 'debug' display using [[mgl:taskreferencefunctionreference#mgleditscreenparams|mglEditScreenParams]]
- Type mglEditScreenParams in matlab.
- Click to Add a new display on the first dialog box that appears.
- Hit Ok to edit that display.
- After that set its displayName to "debug".
- 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.
- You can test the settings by hitting the "Test screen params" button.
- 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.
- 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');
- Manually code settings
- set the myscreen.screenNumer field to 0;
- choose the size and position of the mgl window in the current display
- set those in the variable myscreen
- 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 [[mgl:taskReferenceParameters#creating_a_parameter_sequence_after_running_the_stimulus_program|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 [[mgl:taskReferenceParameters#storing_variables_calculated_during_a_trial|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 [[:mgl:taskReferenceSID#stimfile_location|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 600x600 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:beta|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 [[mgl:gettingstarted#initial_setup|Getting Started]]).
Start by setting your parameters:
mglEditScreenParams
This will bring up a dialog that looks like this:
myscreen = eyeCalibDisp(myscreen);
- This will run the calibration routine, see under [[:mgl:functionreferenceeyelink#mglEyelinkSetup|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.
- **Hit ENTER** to bring up an image of the eye
- Make sure that the eye is centered appropriately.
- **Hit ESC** to close this image of the eye.
- **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.
- Hit Enter to accept the calibration.
- Hit ESC to end the calibration routine and start your stimulus program.
- Upon ending your stimulus program, mgl will save a file with an **edf** extension that stores the eye position information.
- You can now read the saved data using [[:mgl:taskreferenceeyelink#getTaskEyeTraces|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 [[:mgl:taskReferenceSavingData#retrieving_data_from_stimfiles|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 [[:mgl:taskReferenceSavingData#retrieving_data_from_stimfiles|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