Lesson 3: A 2AFC trial
The goal in this lesson is to generate code to run a two-alternative forced-choice trial on a field of partially coherent moving dots. The subject will decide after a stimulus whether the overall motion of the dots was upward or downward.
Contents
the 'movingDots' function
Just like on a TV cooking show, I've written a useful function based on some of the ideas in Lesson 2 that we can pull out of the oven that animates moving dot stimuli. You can see how to use it by typing:
help movingDots
movingDots(display,dots,duration) Animates a field of moving dots based on parameters defined in the 'dots' structure over a period of seconds defined by 'duration'. The 'dots' structure must have the following parameters: nDots Number of dots in the field speed Speed of the dots (degrees/second) direction Direction 0-360 clockwise from upward lifetime Number of frames for each dot to live apertureSize [x,y] size of elliptical aperture (degrees) center [x,y] Center of the aperture (degrees) color Color of the dot field [r,g,b] from 0-255 size Size of the dots (in pixels) coherence Coherence from 0 (incoherent) to 1 (coherent) 'dots' can be an array of structures so that multiple fields of dots can be shown at the same time. The order that the dots are drawn is scrambled across fields so that one field won't occlude another. The 'display' structure requires the fields: width Width of screen (cm) dist Distance from screen (cm) And can also use the fields: skipChecks If 1, turns off timing checks and verbosity (default 0) fixation Information about fixation (see 'insertFixation.m') screenNum screen number bkColor background color (default is [0,0,0]) windowPtr window pointer, set by 'OpenWindow' frameRate frame rate, set by 'OpenWindow' resolution pixel resolution, set by 'OpenWindow' showdemo movingDots
movingDots.m can put up multiple fields of moving dots along with a fixation spot. Each field can have its own speed, direction, color, lifetime, aperture size and location, size of dots and 'coherence' (more on coherence later).
Here's an example of two fields of overlapping dots on one side of the screen - one red moving upward and one green moving downward.
clear display dots % Set display parameters display.dist = 50; %cm display.width = 30; %cm display.skipChecks = 1; %avoid Screen's timing checks and verbosity % Field 1: Upward moving red dots dots(1).nDots = 100; dots(1).speed = 5; dots(1).direction = 0; dots(1).lifetime = 5; dots(1).apertureSize = [6,6]; dots(1).center = [-7.5,0]; dots(1).color = [255,0,0]; dots(1).size = 5; dots(1).coherence = 1; % Field 2: Downward moving green dots dots(2).nDots = 100; dots(2).speed = 5; dots(2).direction = 180; dots(2).lifetime = 5; dots(2).apertureSize = [6,6]; dots(2).center = [-7.5,0]; dots(2).color = [0,255,0]; dots(2).size = 5; dots(2).coherence = 1; duration = 5; %seconds try display = OpenWindow(display); movingDots(display,dots,duration); catch ME Screen('CloseAll'); rethrow(ME) end Screen('CloseAll');
Motion coherence
The 'strength' of a moving dot stimulus is typically manipulated by changing the 'coherence' of the field, which is the proportion of dots moving in a particular direction. If there are 100 dots with direction 0 (up)0 and the coherence is set to 0.25, then 25 of the dots will move upward and the other 75 will be given random directions uniformly distribted from 0-360 degrees.
Here's an example of a single field of 15% coherent white dots in the center of the screen.
clear dots dots.nDots = 200; dots.speed = 5; dots.direction = 0; dots.lifetime = 12; dots.apertureSize = [12,12]; dots.center = [0,0]; dots.color = [255,255,255]; dots.size = 8; dots.coherence = .15; duration = 5; %seconds try display = OpenWindow(display); movingDots(display,dots,duration); catch ME Screen('CloseAll'); rethrow(ME) end Screen('CloseAll');
Did you see the weak upward motion? 15% coherence should be near your threshold for detecting motion.
The 'drawFixation' function
Notice the fixation spot. This was drawn by my function 'drawFixation.m'. It draws a box-shaped fixation point on top of a circular mask on every frame at the center of the screen. You can see how to use it by getting help:
help drawFixation
display = drawFixation(display) Inserts a fixation point (smaller square inside a larger square) in the center of the screen and calls Screen's 'Flip' function. Can use the field 'fixation' in the display structure, or uses the following default values: display.fixation.size Size of fixation square default is 0.5 degrees display.fixation.mask Size of circular 'mask' that surrounds the fixation default is 2 degrees display.fixation.color Cell array for two outer and inner colors default is {[255,255,255],[0,0,0]} display.fixation.flip Flag for whether or not to call Screen's 'Flip function at the end. Default is 1
The default values were used in the demo above. You can change them by changing values the display structure like this:
display.fixation.size = .25; %default is .5 degrees display.fixation.color = {[255,0,0],[0,0,255]}; %default is white and black display.fixation.mask = 1; %default is 2 degrees try display = OpenWindow(display); movingDots(display,dots,duration); catch ME Screen('CloseAll'); rethrow(ME) end Screen('CloseAll');
Getting key press information
In order to get a subject's response, we need to read the keyboard. The best way to do this is with the PsychToolbox's 'KbCheck' function and its related friends.
% KbCheck returns three arguments: %[ keyIsDown, timeSecs, keyCode ] = KbCheck; % The first argument is a binary (or 'logical') variable that is 'true' if % a key is currently being pressed. The second is a value related to the % time the key was first pressed, and the third is an integer that is a % code for the key that was pressed. To decode the key code, use the % function 'keyCode' which translates the integer into a string. % Typically, KbCheck isn't used all by itself - it needs to be placed in a % loop that keeps calling KbCheck and then handles the output. This may % seem like a hassle, but it's very useful because it gives you a lot of % flexibility about how you handle subject responses. % Here's a simple example using KbCheck to wait for the user to press a key % and then displays the key that was pressed and how long the subject took % to respond. %disable output to the command window ListenChar(2); keyIsDown = 0; startTime = GetSecs; %read the current time on the clock disp('Please press a key.'); while ~keyIsDown %if the key is not down, go into the loop [ keyIsDown, timeSecs, keyCode ] = KbCheck; %read the keyboard end %enable output to the command window ListenChar(0); %determine which key was pressed key = KbName(keyCode); %calculate the reaction time RT = timeSecs-startTime; %display the result disp(sprintf('You pressed the "%s" key after %5.2f seconds',key,RT));
Please press a key. You pressed the "return" key after 19.24 seconds
How does this work? The program goes into the 'while' loop and spins around until the variable 'keyIsDown' changes to anything but zero as the result of a key press. When this happens, the control flows out of the loop where the key is decoded and RT is calculated. The PsychToolbox function 'GetSecs' returns a high-precision clock time, where zero is arbitrary - it's values are useful only with respect to other calls to GetSecs.
% Here's a somewhat silly example using GetSecs. The clock is first read % and stored as variable 'a'. The program then pauses for one second using % Matlab's 'pause' function, and the clock is read again but stored into % the variable 'b'. The values of a and b are basically meaningless on % their own, but their difference reflects the time between calls to % GetSecs: a = GetSecs pause(1) b = GetSecs disp(sprintf('Time Elapsed: %5.4f seconds',b-a));
a = 2.5752e+004 b = 2.5753e+004 Time Elapsed: 1.0005 seconds
You might notice that the time, b-a, is not exactly equal to 1.00 seconds. This discrepancy is due to innacuracy with 'pause', not 'GetSecs'.
The 'waitTill' function
I've written a simple keyboard reading function using 'KbCheck' that aborts the wait after a predetermined amount of time. This is useful for experiments where the timing of the trials is important - such as in fMRI experiments where you have to get on with things if the subject doesn't respond in time. Here's an example of how to use it.
Run the next line of code and type stuff into the keyboard for the next few seconds:
disp('Please press some keys over the next five seconds'); tic [keys,RTs] = waitTill(5); toc for i=1:length(keys) disp(sprintf('You pressed the "%s" key after %5.2f seconds',keys{i},RTs(i))); end
Please press some keys over the next five seconds Elapsed time is 5.100140 seconds.
'waitTill' can take in a second argument that holds the current time on the clock, as defined by the function GetSecs. This will be useful later when we're dealing with trial sequences and want to pause the program until the clock reaches a certain time.
Drawing text in real-world coordinates
Often in experiments we put up text strings for instructions to the observer. We'll do that in a bit to instruct the observer to press a key to begin, and to tell the observer which keys to press for a response.
We used the 'DrawText' function in Screen back in Lession 1. This draws a text string in a specified color with the upper-left corner at a specified location in pixel coordinates. Text parameters can be set using Screen functions like 'TextSize'.
I find this a bit clumsy, especially because I can't center the text. I've written a program 'drawText' that uses Screen's 'DrawText' but centers the text in real-world coordinates centered at the desired location. Here's some help on that function:
help drawText
display = drawText(display,center,str,[col]) Draws text string 'str' centered at center = [x,y] in real-world coordinates. Text attributes can be set by setting the fields in 'display.text'. This sets the attrubute using the corresponding Screen function. These attributes are reset to the orignal values at the end of the function. FIELD NAME DEFAULT SCREEN FUNCTION color black [0,0,0] TextColor style 1 (bold). TextStyle size 18 TextSize font 'Courier New' TextFont See also: Screen('DrawText?')
%Here's an example of how it works: clear display % Set display parameters display.dist = 50; %cm display.width = 30; %cm display.skipChecks = 1; %avoid Screen's timing checks and verbosity % display.text.size = 40; display.text.color = [255,255,0]; % display.text.style =2; display.text.font = 'Helvetica'; try display = OpenWindow(display); display = drawText(display,[0,0],'Don''t you love this Matlab class?'); Screen(display.windowPtr,'Flip'); waitTill(2); display = drawText(display,[0,0],'Don''t you love this Matlab class?',display.bkColor); Screen(display.windowPtr,'Flip'); waitTill(.25); catch ME Screen('CloseAll'); rethrow(ME) end Screen('CloseAll');
A 2AFC trial with feedback
We're ready to present a partially correlated moving stimulus that could be either moving upward or downward, get the subject's response, and provide feedback by flashing the fixation green (correct) or red (incorrect).
First, we'll define the display and dot stimulus from scratch
clear display dots % Set display parameters display.dist = 50; %cm display.width = 30; %cm display.skipChecks = 1; %avoid Screen's timing checks and verbosity % Set up dot parameters dots.nDots = 200; dots.speed = 5; dots.lifetime = 12; dots.apertureSize = [12,12]; dots.center = [0,0]; dots.color = [255,255,255]; dots.size = 8; dots.coherence = .5; duration = 1; %seconds %choose either up or down for the dot direction trialDirection = ceil(rand(1)+.5); %50/50 chance of a 1 (up) or a 2 (down) dots.direction = (trialDirection-1)*180; %1 -> 0 degrees, 2 -> 180 degrees %Start the trial try display = OpenWindow(display); drawText(display,[0,6],'Press "u" for up and "d" for down',[255,255,255]); drawText(display,[0,5],'Press Any Key to Begin.',[255,255,255]); display = drawFixation(display); while KbCheck; end KbWait; %Show the stimulus d movingDots(display,dots,duration); %Get the response within the first second after the stimulus keys = waitTill(1); %Interpret the response provide feedback if isempty(keys) %No key was pressed, yellow fixation correct = NaN; display.fixation.color{1} = [255,255,0]; else %Correct response, green fixation if (keys{end}(1)=='u' && dots.direction == 0) || (keys{end}(1)=='d' && dots.direction == 180) correct = 1; display.fixation.color{1} = [0,255,0]; %Incorrect response, red fixation elseif (keys{end}(1)=='d' && dots.direction == 0) || (keys{end}(1)=='u' && dots.direction == 180) correct = 0; display.fixation.color{1} = [255,0,0]; %Wrong key was pressed, blue fixation else correct = NaN; display.fixation.color{1} = [0,0,255]; end end %Flash the fixation with color drawFixation(display); waitTill(.5); display.fixation.color{1} = [255,255,255]; drawFixation(display); waitTill(.5); catch ME Screen('CloseAll'); rethrow(ME) end Screen('CloseAll');
Exercises
- Edit the program above to loop through 10 trials of the 2AFC motion experiment and calculate your percent correct.
- Repeat this for the following coherence levels: .1,.2,.3,.4,.5. Then plot the percent correct on the y-axis as function of the coherence on the x-axis. This is a 'psychometric function'
- Use the 'drawText' and 'waitTill' to measure the response times on a Stroop task: Put up, for example, the word 'GREEN' in red text and have the subject type the first letter of the written word. Compare the RT's for this compared to when the color of the text matches the word.