Stimulus timing issue

Dear Community,

Info:
Psychtoolbox-3 (3.0.19)
MATLAB platform (2021b)
ubuntu (22.04)

I have implemented an easy finger tapping reaction time task using Psychtoolbox.
First, the participants are presented with a white circle which means “prepare for the movement”. Then they see a green circle which means “press the button ASAP”.

I’ve set up the following timing for these stimuli:
white circle - 1-1.5 sec (random)
green circle - 1 sec.

But then I run the task it seems that these timed do not work as expected. I have triple checked the script, but I am fairly new to PTB and Matlab. Could you please suggest what can be the issue?

I am also running a more complex task using PTB which seem to have a similar timing problems.

Please find the script for Finger tapping task below.

Best,
Kat

%% FINGER TAPPING TASK
% Clear the workspace and the screen
sca;
close all; 
clear;  

% Here we call some default settings for setting up Psychtoolbox
PsychDefaultSetup(2);    

% Participant details           
subjID = 's1_pac_sub05'; % s1_pac_sub00; old - 's1_sub00_NT'     
taskID = '_BL';
outFileName = [subjID, taskID];
home_dir = '~/Desktop/Delayed cursor rotation task/data';
subjFoldPath = fullfile(home_dir, subjID);

% Check if the subject folder exists
if ~exist(subjFoldPath, 'dir')
    mkdir(subjFoldPath);
end
cd(subjFoldPath);

% Check if the file exists in the current folder to prevent overwriting
if exist([outFileName, '.mat'], 'file')
    error(sprintf('File %s exists. Please rename the file!', outFileName));
end


% Debugging mode
% PsychDebugWindowConfiguration;


% Get the screen numbers
screens = Screen('Screens');

% Draw we select the maximum of these numbers. So in a situation where we
% have two screens attached to our monitor we will draw to the external screen
screenNumber = max(screens); 

% Define screen color and circle properties
colorScreen = 0; % 0 for black, 0.5 for gray
circleSize = 100;
whiteColor = [255 255 255]; % White    
greenColor = [0 255 0]; % Green
textSize = 36;

% Define timing
prepareDuration = 1 + rand(1) * 0.5; % 0.5-1 sec
stimulusDuration = 1; % in seconds 
photodiodeDuration = 0.1;   

% Number of trials
totalTrials = 120; % 120 
breakAfterTrials = 40; % 40
    
% Set up serial port 
portParams = 'BaudRate=115200 DTR=1 Terminator=10';
com = '/dev/ttyACM0'; %COM port for serial connection
SerPort = IOPort('OpenSerialPort', com, portParams);    % Get handle for serial port
% 100 - launch, 101 - st_instr, 102 - break, 103 - fin_instr, 10 - new trial, 11 - prepare, 12 - isi1, 13 - go, 14 - isi2
IOPort('Write', SerPort, ['WRITE 100 1000 0' char(10)]);


% Open an on screen window and color it black.
[window, windowRect] = PsychImaging('OpenWindow', screenNumber, colorScreen);

% set priority level
topPriorityLevel = MaxPriority(window);
Priority(topPriorityLevel);     

% Get the size of the on screen window in pixels.
[screenXpixels, screenYpixels] = Screen('WindowSize', window);
SetMouse(screenXpixels, 0, window);

% Get the centre coordinate of the window in pixels
[xCenter, yCenter] = RectCenter(windowRect);

% Enable alpha blending for anti-aliasing
% ???????????????????
Screen('BlendFunction', window, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

% rect settings for protodiode
photRect = [0 0 30 30]; %size of rectangle to display for photodiode

% note and save start time
start_time = GetSecs; % NOT LOGGED
%outVar.start_time = (start_time);


% INSTRUCTIONS
% Present instructions
Screen('TextSize', window, 36);
DrawFormattedText(window, ['Welcome to our experiment!' ...
                    '\n\nPlease follow the instructions below:' ...
                    '\n\n\nwhen you see a WHITE circle - prepare to move' ...
                    '\n\nwhen you see a GREEN circle - PRESS  <ENTER>  WITH YOUR INDEX FINGER' ...
                    '\n\n\n<< Press ANY KEY to START >>'], ...
                    'center', 'center', [255 255 255]);
Screen('Flip', window);
% 100 - launch, 101 - st_instr, 102 - break, 103 - fin_instr, 10 - new trial, 11 - prepare, 12 - isi1, 13 - go, 14 - isi2
IOPort('Write', SerPort, ['WRITE 101 1000 0' char(10)]);
KbWait;   
    

% Loop through trials
 for trial = 1:totalTrials
    
    % Check for ESC key press to exit
    [~, ~, keyCode] = KbCheck;
    if keyCode(KbName('ESCAPE'))
        break; % Exit the loop if ESC key is pressed
    end
    

    % NEW TRIAL
    % 100 - launch, 101 - st_instr, 102 - break, 103 - fin_instr, 10 - new trial, 11 - prepare, 12 - isi1, 13 - go, 14 - isi2
    IOPort('Write', SerPort, ['WRITE 10 1000 0' char(10)]);
    trial_start = GetSecs;
    

    % PREPARE
    % Photodiode loop
    for iter = 1:2
        % Present dot and rectangle simultaneously for 0.1 seconds in iteration 1
        if iter == 1
            % Present white circle and rect
            Screen('DrawDots', window, [xCenter yCenter], circleSize, whiteColor, [], 2);
            % Trigger for photodiode
            Screen('FillRect', window, whiteColor, photRect);
            % Flip to the screen
            Screen('Flip', window);
            % 100 - launch, 101 - st_instr, 102 - break, 103 - fin_instr, 10 - new trial, 11 - prepare, 12 - isi1, 13 - go, 14 - isi2
            IOPort('Write', SerPort, ['WRITE 11 1000 0' char(10)]);
            % get times
            prepare_sig = GetSecs - trial_start;
            WaitSecs(photodiodeDuration);
        % Present only white circle
        else
            % Present white circle
            Screen('DrawDots', window, [xCenter yCenter], circleSize, whiteColor, [], 2);
            % Flip to the screen
            Screen('Flip', window);
            WaitSecs(prepareDuration - photodiodeDuration);
        end
    end


    % ISI1
    % Present empty screen for 100 ms
    Screen('Flip', window);
    % 100 - launch, 101 - st_instr, 102 - break, 103 - fin_instr, 10 - new trial, 11 - prepare, 12 - isi1, 13 - go, 14 - isi2
    IOPort('Write', SerPort, ['WRITE 12 1000 0' char(10)]);
    isi_sig1 = GetSecs - trial_start;
    WaitSecs(0.1); % 500 milliseconds
    

    % GO
    % Photodiode loop
    for iter = 1:2
        % Present dot and rectangle simultaneously for 0.1 seconds in iteration 1
        if iter == 1
            % Present green circle and rect
            Screen('DrawDots', window, [xCenter yCenter], circleSize, greenColor, [], 2);
            % Trigger for photodiode
            Screen('FillRect', window, whiteColor, photRect);
            % Flip to the screen
            Screen('Flip', window);
            % 100 - launch, 101 - st_instr, 102 - break, 103 - fin_instr, 10 - new trial, 11 - prepare, 12 - isi1, 13 - go, 14 - isi2
            IOPort('Write', SerPort, ['WRITE 13 1000 0' char(10)]);
            % get times
            go_sig = GetSecs - trial_start;

            % Wait 0.1 sec for ENTER key press
            KbWait([], 2, GetSecs + (photodiodeDuration));
            key_press_fast = GetSecs - trial_start - go_sig;
            if key_press_fast < 0.1
                % Send a keypress trigger 15 to the EEG
                IOPort('Write', SerPort, ['WRITE 15 1000 0' char(10)]);
                disp(['Reaction time: ', num2str(key_press_fast)]);
            else
                key_press_fast = nan;
                disp('No key was pressed within 0.1 sec.');
            end

        % Present only green circle
        else
            % Present green circle
            Screen('DrawDots', window, [xCenter yCenter], circleSize, greenColor, [], 2);
            % Flip to the screen
            Screen('Flip', window);
            % Wait 1 sec for ENTER key press
            KbWait([], 2, GetSecs + (stimulusDuration - photodiodeDuration));
            key_press_slow = GetSecs - trial_start - go_sig;
            if key_press_slow < 0.9
                % Send a keypress trigger 15 to the EEG
                IOPort('Write', SerPort, ['WRITE 15 1000 0' char(10)]);
                disp(['Reaction time: ', num2str(key_press_slow)]);
            else
                key_press_slow = nan;
                disp('No key was pressed between 0.1 and 1 sec.');

            end            
        end
    end
    

    % ISI2
    % Present empty screen for 500 ms
    Screen('Flip', window);
    % 100 - launch, 101 - st_instr, 102 - break, 103 - fin_instr, 10 - new trial, 11 - prepare, 12 - isi1, 13 - go, 14 - isi2
    IOPort('Write', SerPort, ['WRITE 14 1000 0' char(10)]);
    isi_sig2 = GetSecs - trial_start;
    WaitSecs(0.5); % 500 milliseconds
    

    % SAVING THE DATA
    %! outVar.timing(trial,:) = ([trial_start, prepare_sig, isi_sig1, go_sig, isi_sig2]);
    outVar.trial_start(trial,:) = trial_start;
    outVar.prepare_sig(trial,:) = prepare_sig; 
    outVar.isi_sig1(trial,:) = isi_sig1;
    outVar.go_sig(trial,:) = go_sig;
    outVar.key_press_fast(trial,:) = key_press_fast;
    outVar.key_press_slow(trial,:) = key_press_slow;
    outVar.isi_sig2(trial,:) = isi_sig2;
        
    
    % BREAK
    % Check for break
    if mod(trial, breakAfterTrials) == 0 && trial ~= totalTrials
        Screen('TextSize', window, 36);
        DrawFormattedText(window, 'BREAK \n\n<< Press ANY KEY to continue >>', 'center', 'center', [255 255 255]);
        Screen('Flip', window);
        % 100 - launch, 101 - st_instr, 102 - break, 103 - fin_instr, 10 - new trial, 11 - prepare, 12 - isi1, 13 - go, 14 - isi2
        IOPort('Write', SerPort, ['WRITE 102 1000 0' char(10)]);
        KbWait;
    end

end


% FINISH
% End of experiment
Screen('TextSize', window, 36);
DrawFormattedText(window, ['Experiment finished. \n\nThank you for participation!' ...
                        '\n\n<< Press ANY KEY to exit >>'], 'center', 'center', [255 255 255]);
Screen('Flip', window);
% 0 - launch, 1 - st_instr, 2 - break, 103 - fin_instr, 10 - new trial, 11 - prepare, 12 - isi1, 13 - go, 14 - isi2
IOPort('Write', SerPort, ['WRITE 103 1000 0' char(10)]);
KbWait; 
sca;
% close port
IOPort('Close', SerPort);


% SAVING THE DATA
% save times       
save([outFileName '.mat'],'outVar');

% Convert structure to table
outTable = struct2table(outVar);

% Add column names
outTable.Properties.VariableNames = {'trial_start', 'prepare_sig', 'isi_sig1', ...
                                     'go_sig', 'key_press_fast', 'key_press_slow', 'isi_sig2'};

% Write table to CSV file
csvFileName = [outFileName '.csv'];
writetable(outTable, csvFileName);

WaitSecs is not the way to time your stimuli. Instead, do timing by
showing each stimulus for the right number of frames to achieve the
expected duration given the framerate of your display. You can do this
using the when display, e.g., if you want to display screen 1 for 1
sec before screen 2, you’d do something like (from my head):

  • drawing for screen one
    presentation_t = Screen(wpnt,‘Flip’);
  • drawing for screen 2
    Screen(wpnt,‘Flip’, presentation_t+1);
1 Like

The Introductory PDF has sections about timing:

1 Like

Hi Mario,

Thanks a lot for the reference!

In your tutorial I found tro methods for tracking for timing:

  • using Screen('Flip) function and recording ‘VBLTimestamp’ output as a time stamp;
  • using GetSecs and WaitSecs(duration).

I was using the latter method, but aparantly it’s not optimal. Could you suggest what the implication are of using it for the time tracking in my task?

It is not optimal in the sense that stimuli will be shown longer or
shorter than wanted

1 Like

I have a couple of demos on how to do correct timing here

One demo for flipping to the screen each frame and another for flipping 1+n frames.

The basis of needing timing in “frames” is the refresh rate of a monitor and the fact that this essentially “quantises” time. For example, for a 60Hz display in minimum unit of time one can present a stimulus for is 1/60th of a second.

PTB has excellent timing strategies as details in the various documentation which ensures accurate timing.

Peter

2 Likes