Measure reaction time in milliseconds

Hello Everyone, I am trying to measure reaction time from onset of an image stimulus untill user press ‘return’. And later on this reaction time , should be saved in a file with the response of the stimulus.I have tried to start clock after stimulus is shown and to collect reaction time I am using function getTime(), when user press ‘return’ ; but it gives me implausible millisecond reaction time like 0.000617261801381; Here is the code, can please anyone help.

# -*- coding: utf-8 -*-

from __future__ import absolute_import, division
from psychopy import gui, visual, core, data, event, logging, sound
from psychopy.constants import (NOT_STARTED, STARTED, PLAYING, PAUSED,
                                STOPPED, FINISHED, PRESSED, RELEASED, FOREVER)
import numpy as np  # whole numpy lib is available, prepend 'np.'
from numpy import (sin, cos, tan, log, log10, pi, average,
                   sqrt, std, deg2rad, rad2deg, linspace, asarray)
from numpy.random import random, randint, normal, shuffle
import os  # handy system and path functions
import sys  # to get file system encoding
from threading import Timer

from random import shuffle
import codecs, os
import time,subprocess
from re import match
import sys  
from psychopy.core import getTime

reload(sys)  
sys.setdefaultencoding('utf8')

PATH = 'C:\\Users\\VR 6-Diff\\OneDrive\\creativity\\Programming_creativity_slides\\__Programming\\__Programming\\5KombinierenVonObjekten'  #please enter path of your system
OUTPATH = '{0:s}\\results\\'.format(PATH)  # output path for storing the results
sans = ['Gill Sans MT', 'Arial','Helvetica','Verdana'] #use the first font found on this list

exp_name = '5Kombinieren Von Objekten'
exp_info = {'Versuchspersonnummer': '', 'Versuchspersonnummer (repeat)': ''}

dlg = gui.DlgFromDict(dictionary=exp_info, title=exp_name)
if dlg.OK is False:
    core.quit()  # user pressed cancel
while exp_info['Versuchspersonnummer'] != exp_info['Versuchspersonnummer (repeat)']:
    dlg = gui.DlgFromDict(dictionary=exp_info, title='Please insert matching number in both fields')
    if dlg.OK is False:
        core.quit()  # user pressed cancel


exp_info['date'] = data.getDateStr()  # add a simple timestamp
exp_info['exp_name'] = exp_name

output_file = OUTPATH + exp_info['exp_name'] + '_{0:02d}.txt'.format(int(exp_info['Versuchspersonnummer']))
if os.path.isfile(output_file):
    print 'Error: File already exists. Restart experiment and choose subject number which is not used yet.'
    exit(1)

myWin = visual.Window(size =[1366,768], monitor= "testmonitor",color = (230,230,230),allowGUI = False, fullscr = True, # please change your screen size here.
                        winType='pyglet',colorSpace = 'rgb255', units = 'deg')

def saveThisResponse(captured_string):
    outfile = OUTPATH + exp_info['exp_name']+ '_{0:02d}.txt'.format(int(exp_info['Versuchspersonnummer']))
    f = open(outfile, 'a') #open our results file in append mode so we don't overwrite anything
    f.write(captured_string) #write the string they typed
    f.write('_typed at %s' %time.asctime()) #write a timestamp (very course)
    f.write( '_Reaction Time is' +'_{:f}'.format(Reaction_time))
    #f.write('_ reaction time is %5.4f milliseconds %f'%Rt)
    f.write('\n') # write a line ending
    f.close() #close and "save" the output file

def updateTheResponse(captured_string):
    CapturedResponseString.setText(captured_string)
    CapturedResponseString.draw()
    ResponseInstuction.draw()
    myWin.flip()

scale =0.6

files_instr = [f for f in os.listdir('{0:s}/instr_pics/'.format(PATH))if match(r'KO_instr_[1-4]+.png', f)]
for file in files_instr:
    instr_screen = visual.ImageStim(myWin, image='{0:s}/instr_pics/{1:s}'.format(PATH,file),pos =(0.1,-0.1))
    instr_screen.size*= scale
    instr_screen.draw()#draw
    myWin.flip()#show
    event.waitKeys()#click


ready = visual.ImageStim(myWin, image = '{0:s}/Ready&Thanks_pic/Ready.png'.format(PATH),pos =(0.2,-0.2))
ready.size*= scale
ready.draw()
myWin.flip()
event.waitKeys()

scale2 = 0.6
files = [f for f in os.listdir('{0:s}/stim_pics/'.format(PATH))if match(r'KOitem_[0-9]+.png', f)]
for file in files:
    stimulus = visual.ImageStim(myWin, image='{0:s}/stim_pics/{1:s}'.format(PATH,file),size=(25,10),units = 'deg',pos =(0,5))
    #stimulus.size*= scale2
    ResponseInstuction = visual.ImageStim(myWin, image = '{0:s}/Ready&Thanks_pic/Type_instr.png'.format(PATH),pos = (0,-2),size=(15,5)) 
    CapturedResponseString = visual.TextStim(myWin, 
                        units='norm',height = 0.1,
                        pos=(0,-0.5), text='',
                        font=sans, bold = True,
                        alignHoriz = 'center',alignVert='center',
                        color='Black')
    captured_string = '' #empty for now.. 
                             
    stimulus.draw() #draw stimulus1 screen
    ResponseInstuction.draw()  # draw instruction
    myWin.flip() # show instruction
    # until the participant hits the return key
    clock = core.Clock()
    subject_response_finished = 0 # only changes when they hit return

    while clock.getTime () <= 30 and subject_response_finished == 0 :
        stimulus.draw()
        RT = core.Clock()# start clock here when stimulus is shown
        for key in event.getKeys():
            if key in ['escape']:
                myWin.close()
                core.quit()
            elif key in ['return'] :
                myWin.flip()
                rt = RT.getTime()
                Reaction_time =  rt*10000
                print 'time is %.4f '%(Reaction_time)
                print captured_string #show in debug window
                saveThisResponse(captured_string) #write to file
                captured_string = '' #reset to zero length 
                subject_response_finished = 1 #allows the next trial to start
                RT.reset()
                clock.reset()
            elif key in ['delete','backspace']:
                captured_string = captured_string[:-1] #delete last character
                updateTheResponse(captured_string)
            elif key in ['space']:
                captured_string = captured_string+' '
                updateTheResponse(captured_string)
            elif key in ['period']:
                captured_string = captured_string+'.'
                updateTheResponse(captured_string)
            elif key in ['comma']:
                captured_string = captured_string+','
                updateTheResponse(captured_string)
            elif key in ['lshift','rshift']:
                pass #do nothing when some keys are pressed
            else: 
                captured_string = captured_string+key
                updateTheResponse(captured_string)
                event.clearEvents(eventType='keyboard')
    
thanks_screen = visual.ImageStim(myWin, image='{0:s}/Ready&Thanks_pic/Thanks.PNG'.format(PATH))
thanks_screen.draw()
myWin.flip()
event.waitKeys()
myWin.close()

I think the problem is the above line - the clock is being created every frame and will start from 0 - so any reaction get will be taken from the new clock. I think you need to create the clock object at the start and then use the functions detailed in the API to reset it after the response is given.

1 Like

Dear Oli

Thanks a lot for your response
I initiated clock at the beginning where i am initializing routines.

   while clock.getTime () <= 60 and subject_response_finished == 0 :
        stimulus.draw()
        for key in event.getKeys():
            if key in ['escape']:
                myWin.close()
                core.quit()
            elif key in ['return'] :
                Rt = RT.getTime()
                #Reaction_time = Rt*1000.0
                #millis = float(round(Rt() * 1000))
                print (Rt)
                #print'time is %.4f '%(Reaction_time)
                print captured_string #show in debug window
                saveThisResponse(captured_string) #write to file
                captured_string = '' #reset to zero length 
                subject_response_finished = 1 #allows the next trial to start
                myWin.flip()
                RT.reset()
                clock.reset()

But still, time is not precise. I am a beginner here; Please help :slight_smile:

Not precise how? Still giving you very low values or just giving you inaccurate ones?

I’m also not clear why you need RT and clock to be separate. They should be doing the exact same thing, because clock starts from 0 when the image is first presented. In other words, this should work:

rt=clock.getTime()

That alone might clear it up, if for some reason having two clocks was the issue.

I have the same problem, with a SART test, but I need to convert the data that I have already acquire…
I have a csv file with the test of a lot of people involved in my research… and I cannot modify the script and do the test again… :frowning:

In attached the script that I used and a example of csv file.
The script is made by Erik Marsja (http://www.marsja.se/sustained-attention-task-in-psychopy/#comment-4796)

Help me!!!
Thank you

RT Sub_id Task Stimulus Age Cresp Sex Trial Date Accuracy
1.0966010093688965 Prova SART 9 2 space Male 1 2015-11-28_19:53 0
1.1157429218292236 Prova SART 2 2 space Male 2 2015-11-28_19:53 0
1.1162469387054443 Prova SART 4 2 space Male 3 2015-11-28_19:53 0
1.1158220767974854 Prova SART 5 2 space Male 4 2015-11-28_19:53 0
1.0978949069976807 Prova SART 1 2 space Male 5 2015-11-28_19:53 0
1.1156039237976074 Prova SART 3 2 Noresponse Male 6 2015-11-28_19:53 0
1.1158201694488525 Prova SART 8 2 space Male 7 2015-11-28_19:53 0
1.1164860725402832 Prova SART 8 42 space Male 1 2017-03-21_09:33 Noresponse 0

-- coding: utf-8 --

“”"
Created on Sat Jun 27 22:05:30 2015
Sustained Attention to Response Task & Vigalance Task

@author: erik @ marsja dot se
"""

from psychopy import visual, core, data, sound, gui, event
import glob, os

from random import shuffle

class Experiment():
’’'
Two tasks;

1)Sustained Attention To Response task
Robertson et al., (1997)
x digits (x pairs of x digits)
Each digit is presented for 250 Msec followed by a 900 Msec mask
(Mask= ring with diaognoal cross in the middle, diameter=29mm).

Digit 3 is no response digit, prefixed quasi-randomly distributed among x trials

Digit onset-to-onset 1150 ms

2)Vigilance task (modified SART)
Same as above but responses on digit 3 only (withold on all other digits)
'''

def __init__(self, digits, nTrials, name):
    #Should look this over and probably change it
    self.name = name
    self.n_digits = digits
    self.digits = range(1, digits+1)
    self.trials = nTrials

def create_trials(self):
    '''Creates a list of trials.
    '''
    trials = []
    for trial_seq in range(self.trials/self.n_digits):
        shuffled = self.digits[:]
        shuffle(shuffled)
        trials.append(shuffled)
    return trials

def experiment_window(self, color):
    '''For creating the experiment window
    in preferred color
    self.color = color
    '''
    self.win = visual.Window(size=(1920, 1200), fullscr=True, screen=0,
                             allowGUI=False, allowStencil=False,
        monitor=u'testMonitor', color=color, colorSpace=u'rgb',
        blendMode=u'avg',winType=u'pyglet')
    return self.win

def create_text_stim(self, win, text, pos, name, height, color):
    '''Creates  text stimulu1,
    self.text = text
    self.pos = pos
    self.name = name
    self.height = height
    self.color = color'''
    self.text = text
    self.pos = pos
    self.name = name
    self.height = height
    self.color = color
    text_stimulus = visual.TextStim(win=self.win, ori=0, name=self.name,
        text=self.text,    font=u'Arial',
        pos=self.pos, height=self.height,
        color=self.color, colorSpace=u'rgb')
    return text_stimulus

def present_stim(self, stim, target):
    self.stimulus = stim
    self.target = target
    self.win.flip()
    if self.stimulus == "text":
        self.text_on_screen.setText(target)
        self.text_on_screen.draw()
    elif self.stimulus == "image":
        self.target.draw()
    elif self.stimulus == "sound":
        '''here we will we play the sound
        NOT IMPLEMENTED YET
        '''
        self.target.play()

def experiment_trials(self, trials, expinfo):
    self.exp = expinfo
    self.trialList = []
    self.correctresp = 'space'

    for trial_seq in trials:
        for trial, digit in enumerate(trial_seq):
            trial +=1
            if digit == 3:
                if self.expinfo['Task'] == 'SART':
                    self.correctresp = u'Noresponse'
                elif self.expinfo['Task'] == 'Vigilance':
                    self.correctresp = u'space'

            else:
                if self.expinfo['Task'] == 'SART':
                    self.correctresp = u'space'
                elif self.expinfo['Task'] == 'Vigilance':
                    self.correctresp = u'Noresponse'

            self.trialList.append({'Cresp':self.correctresp,
                                   u'Stimulus':digit, u'Trial':trial,
                                   u'Sub_id':self.exp['Subject Id'],
                                     u'Age':self.exp['Age'],
                                     u'Sex':self.exp['Sex'],
                                     u'Date':self.exp['date'],
                                     u'Task':self.expinfo['Task']})
    self.trialHandler = data.TrialHandler(self.trialList,1, method="sequential")
    self.trialHandler.data.addDataType('Response')
    self.trialHandler.data.addDataType('Accuracy')
    self.trialHandler.data.addDataType('RT')
    return self.trialHandler

def experiment_info(self):
    self.exp_name = u'Sustained Attention'
    self.exp_info = {'Subject Id':'', 'Age':'', 'ExpVersion': 0.4,
                    'Sex': ['Male', 'Female'], 'Task':['SART', 'Vigilance']}
    self.exp_info[u'date'] = data.getDateStr(format="%Y-%m-%d_%H:%M")
    self.infoDlg = gui.DlgFromDict(dictionary=self.exp_info,
                                   title=self.exp_name, fixed=['ExpVersion'])
    self.datafile = u'Data' + os.path.sep + u'DATA_SART.csv'
    if self.infoDlg.OK:
        return self.exp_info
    else:
        return 'Cancelled'

def run_trials(self, trialObj):
    self.trialhandler = trialObj
    self.timer = core.Clock()
    self.warning_frames = int(self.frameR * .5)
    self.target_frames = int(self.frameR * .25)
    self.iti_frames = int(self.frameR * .9)

    # Warning before experiment start
    for frame in range(self.warning_frames):
        self.present_stim('text', 'Pronti!')

    for frame in range(self.warning_frames):
        self.present_stim('image', self.mask)

    for trial in self.trialhandler:
        self.timer.reset()
        keys = event.getKeys(keyList=['space'])

        for frame in range(self.target_frames):
                visualTarget = trial['Stimulus']
                self.present_stim('text', visualTarget)

        for frame in range(self.iti_frames):
                self.present_stim('image', self.mask)

                if keys:
                    trial['RT'] = self.timer.getTime()
                    if keys[0] == trial['Cresp']:
                        trial['Accuracy'] = 1
                    else:
                        trial['Accuracy'] = 0

                    trial['Response'] = keys[0]

                elif len(keys) == 0:
                    if trial['Cresp'] == 'Noresponse':
                        trial['Accuracy'] = 1
                    else:
                        trial['Accuracy'] = 0

                    trial['Response'] = 'Noresponse'

        trial['RT'] = self.timer.getTime()
        self.write_csv(self.datafile, trial)

def write_csv(self,fileName, current_trial):
    import codecs, csv, os
    fullpath = os.path.abspath(fileName)
    if not os.path.isfile(fullpath):
        with codecs.open(fullpath, 'ab+', encoding='utf8') as f:
            csv.writer(f, delimiter=';').writerow(current_trial.keys())
            csv.writer(f, delimiter=';').writerow(current_trial.values())
    else:
        with codecs.open(fullpath, 'ab+', encoding='utf8') as f:
            csv.writer(f, delimiter=';').writerow(current_trial.values())

def create_dir(self, dirname):
    import os
    if not os.path.isdir(dirname):
        os.create_dirs(dirname)

def run_experiment(self):
    self.create_dir('Data')
    self.expinfo = self.experiment_info()
    if self.expinfo == 'Cancelled':
        print 'User cancelled'
        core.quit()

    self.win = self.experiment_window(color = 'black')
    self.frameR = self.win.getActualFrameRate()
    self.load = Preloading()
    self.files = self.load.load_files("Stimuli", "png", "image", self.win)
    self.txtfiles = self.load.load_files("Stimuli", "txt", "text", self.win)
    self.mask = self.files['circleMask'][0]
    self.text_on_screen = self.create_text_stim(self.win, text='',
                                     pos=[0.0,0.0], name='Visual Target',
                                     height=0.07, color='White')
    self.txtfiles[self.expinfo['Task']].draw()
    self.win.flip()
    event.waitKeys()
    event.clearEvents()
    self.trials = self.create_trials()
    trialsToRun = self.experiment_trials(self.trials, self.expinfo)
    self.run_trials(trialsToRun)
    core.quit()

class Preloading():

def __init__(self):
    self.path = os.getcwd()

def load_files(self, directory, extension, filetype, win='', which_files='*', stim_list=[]):
    """ Load all the pics and sounds"""
    self.dir = directory
    self.extension = extension
    self.filetype = filetype
    self.wi = win
    self.which_files = which_files

    if isinstance(self.extension, list):
        file_list = []
        for current_ext in self.ext:
            file_list.extend(glob.glob(
                                    os.path.join(self.path,
                                                   self.directory,
                                                   self.which_files+current_ext)))
    else:
        file_list = glob.glob(os.path.join(self.path,directory, self.which_files+self.extension))
        filematrix = {} #initialize filematrix  as a dict because it'll be accessed by picture names, cound names, whatver
    for num,curFile in enumerate(file_list):
        fullpath = curFile
        fullfilename = os.path.basename(fullpath)
        stimfile = os.path.splitext(fullfilename)[0]

        if filetype == "image":
            from psychopy import visual
            try:
                surface = pygame.image.load(fullpath) #gets height/width of the image
                stim = visual.SimpleImageStim(win, image=fullpath)
                filematrix[stimfile] = ((stim,fullfilename,num,surface.get_width(),surface.get_height(),stimfile))
            except: #no pygame, so don't store the image dims
                stim = visual.SimpleImageStim(win, image=fullpath)
                filematrix[stimfile] = ((stim,fullfilename,num,'','',stimfile))

        elif filetype == "sound":
            soundRef = sound.Sound(fullpath)
            filematrix[stimfile] = ((soundRef))
        elif filetype=='text':
            from psychopy import visual
            import codecs
            with codecs.open(fullpath, 'r', encoding='utf8') as f:
                textRef = visual.TextStim(win, text=f.read(), wrapWidth=1.2, alignHoriz='center', alignVert='center', height=0.06)

            filematrix[stimfile] = ((textRef))

    #check
    if stim_list and set(filematrix.keys()).intersection(stim_list) != set(stim_list):
        popupError(str(set(self.stim_list).difference(filematrix.keys())) + " does not exist in " + self.path+'\\'+directory)

    return filematrix

def main():
test = Experiment(digits=9, nTrials=225, name=“Sustained Attention”)
test.run_experiment()

if name == “main”:
main()

1 Like

Hello,

Typically you don’t create/reset clocks every time you want to measure an interval, clocks are unlikely to overflow over the course of your experiment. To get the RT, you take the difference between the clock readings at both ends of your interval.

Sorry i don’t understand how to solve the problem. It’s the first time that i use PsychoPy. Is it too much if i ask you for an example with my data?

31.087.314.796.168.300 02MA SART 1 61 space Male 1 2017-04-10_19:29 space 1
30.821.982.321.795.000 02MA SART 3 61 Noresponse Male 2 2017-04-10_19:29 space 0
33.413.978.461.176.100 02MA SART 9 61 space Male 3 2017-04-10_19:29 space 1
3.123.569.601.215.420 02MA SART 4 61 space Male 4 2017-04-10_19:29 space 1
3.058.467.075.228.690 02MA SART 2 61 space Male 5 2017-04-10_19:29 space 1
30.474.563.252.646.400 02MA SART 6 61 space Male 6 2017-04-10_19:29 space 1
3.047.118.414.659.050 02MA SART 8 61 space Male 7 2017-04-10_19:29 space 1
3.045.989.622.361.950 02MA SART 5 61 space Male 8 2017-04-10_19:29 space 1
3.093.371.093.738.820 02MA SART 7 61 space Male 9 2017-04-10_19:29 space 1

Thank you very much for your help!