OS and Device specs:
|Processor|Intel(R) Core™ Ultra 5 125U 1.30 GHz|
|Installed RAM|16.0 GB (15.5 GB usable)|
System type|64-bit operating system, x64-based processor|
|Pen and touch|No pen or touch input is available for this display|
|Edition|Windows 11 Pro|
|Version|24H2|
|Installed on|28/11/2024|
|OS build|26100.2605|
Display frequency 60 Hz.
PsychoPy version: 2024.2.4
Standard Standalone? (y/n): tried both standalone and with other python installation
What are you trying to achieve?:
I am trying to send a trigger whenever a white box is displayed to my EEG amplifier (BrainAmps) with their triggerbox in between the computer and the amplifier. The triggerbox takes data via USB cable from the PC and forwards the trigger information to amplifier via their trigger cable. so I am using serial port for communication with the triggerbox. I have also attached a photosensor to detect the on/off of white box. The photosensor signal is directly connected to the amplifier and therefore, recorded in-sync with the EEG signals.
The issue is that there is a delay of 52 ms between the trigger and photosensor output. The trigger signal is recorded 52ms earlier than the photosensor signal switches from low to high. I did 20 repetitions to get an average delay, but almost all trials had 52ms difference, with 1 or 2 trials having 54 ms delay.
What did you try to make it work?:
I have tried to google and search on the psychopy forum and followed the suggestions to modify the display settings such as changing the default zoom from 125% to 100%, pixel resolutions, modifying experiment settings, and monitor center.
I developed a basic experiment in builder, and modified the code as well to use win.callOnFlip to send the serial output. For timing of box display, I used frame N option.
I have tried different sampling rates and also different baudrates for the serial port. No difference was found in the delay.
I cannot find how to turn off triple buffering in my Windows settings. Can someone please guide about that, and what other things/settings I can change to remove this delay. If any further details are required, please let me know.
Thanks.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
This experiment was created using PsychoPy3 Experiment Builder (v2024.2.4),
on January 02, 2025, at 16:26
If you publish work using this script the most relevant publication is:
Peirce J, Gray JR, Simpson S, MacAskill M, Höchenberger R, Sogo H, Kastman E, Lindeløv JK. (2019)
PsychoPy2: Experiments in behavior made easy Behav Res 51: 195.
https://doi.org/10.3758/s13428-018-01193-y
"""
# --- Import packages ---
from psychopy import locale_setup
from psychopy import prefs
from psychopy import plugins
plugins.activatePlugins()
prefs.hardware['audioLib'] = 'ptb'
prefs.hardware['audioLatencyMode'] = '3'
from psychopy import sound, gui, visual, core, data, event, logging, clock, colors, layout, hardware
from psychopy.tools import environmenttools
from psychopy.constants import (NOT_STARTED, STARTED, PLAYING, PAUSED,
STOPPED, FINISHED, PRESSED, RELEASED, FOREVER, priority)
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, choice as randchoice
import os # handy system and path functions
import sys # to get file system encoding
from psychopy.hardware import keyboard
import serial
# --- Setup global variables (available in all functions) ---
# create a device manager to handle hardware (keyboards, mice, mirophones, speakers, etc.)
deviceManager = hardware.DeviceManager()
# ensure that relative paths start from the same directory as this script
_thisDir = os.path.dirname(os.path.abspath(__file__))
# store info about the experiment session
psychopyVersion = '2024.2.4'
expName = 'square' # from the Builder filename that created this script
# information about this experiment
expInfo = {
'participant': f"{randint(0, 999999):06.0f}",
'session': '001',
'date|hid': data.getDateStr(),
'expName|hid': expName,
'psychopyVersion|hid': psychopyVersion,
}
# --- Define some variables which will change depending on pilot mode ---
'''
To run in pilot mode, either use the run/pilot toggle in Builder, Coder and Runner,
or run the experiment with `--pilot` as an argument. To change what pilot
#mode does, check out the 'Pilot mode' tab in preferences.
'''
# work out from system args whether we are running in pilot mode
PILOTING = core.setPilotModeFromArgs()
# start off with values from experiment settings
_fullScr = True
_winSize = [1920, 1200]
# if in pilot mode, apply overrides according to preferences
if PILOTING:
# force windowed mode
if prefs.piloting['forceWindowed']:
_fullScr = False
# set window size
_winSize = prefs.piloting['forcedWindowSize']
def showExpInfoDlg(expInfo):
"""
Show participant info dialog.
Parameters
==========
expInfo : dict
Information about this experiment.
Returns
==========
dict
Information about this experiment.
"""
# show participant info dialog
dlg = gui.DlgFromDict(
dictionary=expInfo, sortKeys=False, title=expName, alwaysOnTop=True
)
if dlg.OK == False:
core.quit() # user pressed cancel
# return expInfo
return expInfo
def setupData(expInfo, dataDir=None):
"""
Make an ExperimentHandler to handle trials and saving.
Parameters
==========
expInfo : dict
Information about this experiment, created by the `setupExpInfo` function.
dataDir : Path, str or None
Folder to save the data to, leave as None to create a folder in the current directory.
Returns
==========
psychopy.data.ExperimentHandler
Handler object for this experiment, contains the data to save and information about
where to save it to.
"""
# remove dialog-specific syntax from expInfo
for key, val in expInfo.copy().items():
newKey, _ = data.utils.parsePipeSyntax(key)
expInfo[newKey] = expInfo.pop(key)
# data file name stem = absolute path + name; later add .psyexp, .csv, .log, etc
if dataDir is None:
dataDir = _thisDir
filename = u'data/%s_%s_%s' % (expInfo['participant'], expName, expInfo['date'])
# make sure filename is relative to dataDir
if os.path.isabs(filename):
dataDir = os.path.commonprefix([dataDir, filename])
filename = os.path.relpath(filename, dataDir)
# an ExperimentHandler isn't essential but helps with data saving
thisExp = data.ExperimentHandler(
name=expName, version='',
extraInfo=expInfo, runtimeInfo=None,
originPath='C:\\square.py',
savePickle=True, saveWideText=True,
dataFileName=dataDir + os.sep + filename, sortColumns='time'
)
thisExp.setPriority('thisRow.t', priority.CRITICAL)
thisExp.setPriority('expName', priority.LOW)
# return experiment handler
return thisExp
def setupLogging(filename):
"""
Setup a log file and tell it what level to log at.
Parameters
==========
filename : str or pathlib.Path
Filename to save log file and data files as, doesn't need an extension.
Returns
==========
psychopy.logging.LogFile
Text stream to receive inputs from the logging system.
"""
# set how much information should be printed to the console / app
if PILOTING:
logging.console.setLevel(
prefs.piloting['pilotConsoleLoggingLevel']
)
else:
logging.console.setLevel('warning')
# save a log file for detail verbose info
logFile = logging.LogFile(filename+'.log')
if PILOTING:
logFile.setLevel(
prefs.piloting['pilotLoggingLevel']
)
else:
logFile.setLevel(
logging.getLevel('info')
)
return logFile
def setupWindow(expInfo=None, win=None):
"""
Setup the Window
Parameters
==========
expInfo : dict
Information about this experiment, created by the `setupExpInfo` function.
win : psychopy.visual.Window
Window to setup - leave as None to create a new window.
Returns
==========
psychopy.visual.Window
Window in which to run this experiment.
"""
if PILOTING:
logging.debug('Fullscreen settings ignored as running in pilot mode.')
if win is None:
# if not given a window to setup, make one
win = visual.Window(
size=_winSize, fullscr=_fullScr, screen=0,
winType='pyglet', allowGUI=False, allowStencil=False,
monitor='testMonitor', color=[-1.0000, -1.0000, -1.0000], colorSpace='rgb',
backgroundImage='', backgroundFit='contain',
blendMode='avg', useFBO=True,
units='norm',
checkTiming=False # we're going to do this ourselves in a moment
)
else:
# if we have a window, just set the attributes which are safe to set
win.color = [-1.0000, -1.0000, -1.0000]
win.colorSpace = 'rgb'
win.backgroundImage = ''
win.backgroundFit = 'contain'
win.units = 'norm'
if expInfo is not None:
# get/measure frame rate if not already in expInfo
if win._monitorFrameRate is None:
win._monitorFrameRate = win.getActualFrameRate(infoMsg='Attempting to measure frame rate of screen, please wait...')
expInfo['frameRate'] = win._monitorFrameRate
win.hideMessage()
# show a visual indicator if we're in piloting mode
if PILOTING and prefs.piloting['showPilotingIndicator']:
win.showPilotingIndicator()
return win
def setupDevices(expInfo, thisExp, win):
"""
Setup whatever devices are available (mouse, keyboard, speaker, eyetracker, etc.) and add them to
the device manager (deviceManager)
Parameters
==========
expInfo : dict
Information about this experiment, created by the `setupExpInfo` function.
thisExp : psychopy.data.ExperimentHandler
Handler object for this experiment, contains the data to save and information about
where to save it to.
win : psychopy.visual.Window
Window in which to run this experiment.
Returns
==========
bool
True if completed successfully.
"""
# --- Setup input devices ---
ioConfig = {}
ioSession = ioServer = eyetracker = None
# store ioServer object in the device manager
deviceManager.ioServer = ioServer
# create a default keyboard (e.g. to check for escape)
if deviceManager.getDevice('defaultKeyboard') is None:
deviceManager.addDevice(
deviceClass='keyboard', deviceName='defaultKeyboard', backend='ptb'
)
# return True if completed successfully
return True
def pauseExperiment(thisExp, win=None, timers=[], playbackComponents=[]):
"""
Pause this experiment, preventing the flow from advancing to the next routine until resumed.
Parameters
==========
thisExp : psychopy.data.ExperimentHandler
Handler object for this experiment, contains the data to save and information about
where to save it to.
win : psychopy.visual.Window
Window for this experiment.
timers : list, tuple
List of timers to reset once pausing is finished.
playbackComponents : list, tuple
List of any components with a `pause` method which need to be paused.
"""
# if we are not paused, do nothing
if thisExp.status != PAUSED:
return
# start a timer to figure out how long we're paused for
pauseTimer = core.Clock()
# pause any playback components
for comp in playbackComponents:
comp.pause()
# make sure we have a keyboard
defaultKeyboard = deviceManager.getDevice('defaultKeyboard')
if defaultKeyboard is None:
defaultKeyboard = deviceManager.addKeyboard(
deviceClass='keyboard',
deviceName='defaultKeyboard',
backend='PsychToolbox',
)
# run a while loop while we wait to unpause
while thisExp.status == PAUSED:
# check for quit (typically the Esc key)
if defaultKeyboard.getKeys(keyList=['escape']):
endExperiment(thisExp, win=win)
# sleep 1ms so other threads can execute
clock.time.sleep(0.001)
# if stop was requested while paused, quit
if thisExp.status == FINISHED:
endExperiment(thisExp, win=win)
# resume any playback components
for comp in playbackComponents:
comp.play()
# reset any timers
for timer in timers:
timer.addTime(-pauseTimer.getTime())
def run(expInfo, thisExp, win, globalClock=None, thisSession=None):
"""
Run the experiment flow.
Parameters
==========
expInfo : dict
Information about this experiment, created by the `setupExpInfo` function.
thisExp : psychopy.data.ExperimentHandler
Handler object for this experiment, contains the data to save and information about
where to save it to.
psychopy.visual.Window
Window in which to run this experiment.
globalClock : psychopy.core.clock.Clock or None
Clock to get global time from - supply None to make a new one.
thisSession : psychopy.session.Session or None
Handle of the Session object this experiment is being run from, if any.
"""
# mark experiment as started
thisExp.status = STARTED
# make sure window is set to foreground to prevent losing focus
win.winHandle.activate()
# make sure variables created by exec are available globally
exec = environmenttools.setExecEnvironment(globals())
# get device handles from dict of input devices
ioServer = deviceManager.ioServer
# get/create a default keyboard (e.g. to check for escape)
defaultKeyboard = deviceManager.getDevice('defaultKeyboard')
if defaultKeyboard is None:
deviceManager.addDevice(
deviceClass='keyboard', deviceName='defaultKeyboard', backend='PsychToolbox'
)
eyetracker = deviceManager.getDevice('eyetracker')
# make sure we're running in the directory for this experiment
os.chdir(_thisDir)
# get filename from ExperimentHandler for convenience
filename = thisExp.dataFileName
frameTolerance = 0.001 # how close to onset before 'same' frame
endExpNow = False # flag for 'escape' or other condition => quit the exp
# get frame duration from frame rate in expInfo
if 'frameRate' in expInfo and expInfo['frameRate'] is not None:
frameDur = 1.0 / round(expInfo['frameRate'])
else:
frameDur = 1.0 / 60.0 # could not measure, so guess
# Start Code - component code to be run after the window creation
# Create serial object for device at port 'COM6'
serialCom6 = serial.Serial(
port='COM6',
baudrate=9600,
bytesize=8,
parity='N',
stopbits=1,
timeout=None,
)
# --- Initialize components for Routine "trial" ---
white_square = visual.Rect(
win=win, name='white_square',
width=(1200/1920/4, 1/4)[0], height=(1200/1920/4, 1/4)[1],
ori=0.0, pos=(-1, 1), draggable=False, anchor='center',
lineWidth=1.0,
colorSpace='rgb', lineColor=[1.0000, 1.0000, 1.0000], fillColor=[1.0000, 1.0000, 1.0000],
opacity=None, depth=0.0, interpolate=True)
black_square = visual.Rect(
win=win, name='black_square',
width=(1200/1920/4, 1/4)[0], height=(1200/1920/4, 1/4)[1],
ori=0.0, pos=(-1, 1), draggable=False, anchor='center',
lineWidth=1.0,
colorSpace='rgb', lineColor=[-1.0000, -1.0000, -1.0000], fillColor=[-1.0000, -1.0000, -1.0000],
opacity=None, depth=-1.0, interpolate=True)
# point serialPort to device at port 'COM6' and make sure it's open
serialPort = serialCom6
serialPort.status = NOT_STARTED
if not serialPort.is_open:
serialPort.open()
# create some handy timers
# global clock to track the time since experiment started
if globalClock is None:
# create a clock if not given one
globalClock = core.Clock()
if isinstance(globalClock, str):
# if given a string, make a clock accoridng to it
if globalClock == 'float':
# get timestamps as a simple value
globalClock = core.Clock(format='float')
elif globalClock == 'iso':
# get timestamps in ISO format
globalClock = core.Clock(format='%Y-%m-%d_%H:%M:%S.%f%z')
else:
# get timestamps in a custom format
globalClock = core.Clock(format=globalClock)
if ioServer is not None:
ioServer.syncClock(globalClock)
logging.setDefaultClock(globalClock)
# routine timer to track time remaining of each (possibly non-slip) routine
routineTimer = core.Clock()
win.flip() # flip window to reset last flip timer
# store the exact time the global clock started
expInfo['expStart'] = data.getDateStr(
format='%Y-%m-%d %Hh%M.%S.%f %z', fractionalSecondDigits=6
)
# set up handler to look after randomisation of conditions etc
trials = data.TrialHandler2(
name='trials',
nReps=20.0,
method='sequential',
extraInfo=expInfo,
originPath=-1,
trialList=[None],
seed=None,
)
thisExp.addLoop(trials) # add the loop to the experiment
thisTrial = trials.trialList[0] # so we can initialise stimuli with some values
# abbreviate parameter names if possible (e.g. rgb = thisTrial.rgb)
if thisTrial != None:
for paramName in thisTrial:
globals()[paramName] = thisTrial[paramName]
if thisSession is not None:
# if running in a Session with a Liaison client, send data up to now
thisSession.sendExperimentData()
for thisTrial in trials:
currentLoop = trials
thisExp.timestampOnFlip(win, 'thisRow.t', format=globalClock.format)
if thisSession is not None:
# if running in a Session with a Liaison client, send data up to now
thisSession.sendExperimentData()
# abbreviate parameter names if possible (e.g. rgb = thisTrial.rgb)
if thisTrial != None:
for paramName in thisTrial:
globals()[paramName] = thisTrial[paramName]
# --- Prepare to start Routine "trial" ---
# create an object to store info about Routine trial
trial = data.Routine(
name='trial',
components=[white_square, black_square, serialPort],
)
trial.status = NOT_STARTED
continueRoutine = True
# update component parameters for each repeat
# store start times for trial
trial.tStartRefresh = win.getFutureFlipTime(clock=globalClock)
trial.tStart = globalClock.getTime(format='float')
trial.status = STARTED
thisExp.addData('trial.started', trial.tStart)
trial.maxDuration = None
# keep track of which components have finished
trialComponents = trial.components
for thisComponent in trial.components:
thisComponent.tStart = None
thisComponent.tStop = None
thisComponent.tStartRefresh = None
thisComponent.tStopRefresh = None
if hasattr(thisComponent, 'status'):
thisComponent.status = NOT_STARTED
# reset timers
t = 0
_timeToFirstFrame = win.getFutureFlipTime(clock="now")
frameN = -1
# --- Run Routine "trial" ---
# if trial has changed, end Routine now
if isinstance(trials, data.TrialHandler2) and thisTrial.thisN != trials.thisTrial.thisN:
continueRoutine = False
trial.forceEnded = routineForceEnded = not continueRoutine
while continueRoutine:
# get current time
t = routineTimer.getTime()
tThisFlip = win.getFutureFlipTime(clock=routineTimer)
tThisFlipGlobal = win.getFutureFlipTime(clock=None)
frameN = frameN + 1 # number of completed frames (so 0 is the first frame)
# update/draw components on each frame
# *white_square* updates
# if white_square is starting this frame...
if white_square.status == NOT_STARTED and frameN >= 1:
# keep track of start time/frame for later
white_square.frameNStart = frameN # exact frame index
white_square.tStart = t # local t and not account for scr refresh
white_square.tStartRefresh = tThisFlipGlobal # on global time
win.timeOnFlip(white_square, 'tStartRefresh') # time at next scr refresh
# add timestamp to datafile
thisExp.timestampOnFlip(win, 'white_square.started')
# update status
white_square.status = STARTED
white_square.setAutoDraw(True)
# if white_square is active this frame...
if white_square.status == STARTED:
# update params
pass
# if white_square is stopping this frame...
if white_square.status == STARTED:
if frameN >= 60:
# keep track of stop time/frame for later
white_square.tStop = t # not accounting for scr refresh
white_square.tStopRefresh = tThisFlipGlobal # on global time
white_square.frameNStop = frameN # exact frame index
# add timestamp to datafile
thisExp.timestampOnFlip(win, 'white_square.stopped')
# update status
white_square.status = FINISHED
white_square.setAutoDraw(False)
# *black_square* updates
# if black_square is starting this frame...
if black_square.status == NOT_STARTED and frameN >= 61:
# keep track of start time/frame for later
black_square.frameNStart = frameN # exact frame index
black_square.tStart = t # local t and not account for scr refresh
black_square.tStartRefresh = tThisFlipGlobal # on global time
win.timeOnFlip(black_square, 'tStartRefresh') # time at next scr refresh
# add timestamp to datafile
thisExp.timestampOnFlip(win, 'black_square.started')
# update status
black_square.status = STARTED
black_square.setAutoDraw(True)
# if black_square is active this frame...
if black_square.status == STARTED:
# update params
pass
# if black_square is stopping this frame...
if black_square.status == STARTED:
if frameN >= 120:
# keep track of stop time/frame for later
black_square.tStop = t # not accounting for scr refresh
black_square.tStopRefresh = tThisFlipGlobal # on global time
black_square.frameNStop = frameN # exact frame index
# add timestamp to datafile
thisExp.timestampOnFlip(win, 'black_square.stopped')
# update status
black_square.status = FINISHED
black_square.setAutoDraw(False)
# if serialPort is starting this frame...
if serialPort.status == NOT_STARTED and white_square.status == STARTED:
# keep track of start time/frame for later
serialPort.frameNStart = frameN # exact frame index
serialPort.tStart = t # local t and not account for scr refresh
serialPort.tStartRefresh = tThisFlipGlobal # on global time
# win.timeOnFlip(serialPort, 'tStartRefresh') # time at next scr refresh
# # add timestamp to datafile
# thisExp.addData('serialPort.started', t)
# # update status
# serialPort.status = STARTED
# serialPort.write(bytes('chr(1)', 'utf8'))
# serialPort.status = STARTED
win.callOnFlip(serialPort.write, bytes('1', 'utf8')) # Send trigger on flip
thisExp.addData('serialPort.started', tThisFlipGlobal) # log exact flip time
serialPort.status = STARTED
# if serialPort is stopping this frame...
if serialPort.status == STARTED:
# is it time to stop? (based on global clock, using actual start)
if tThisFlipGlobal > serialPort.tStartRefresh + 1.0-frameTolerance:
# keep track of stop time/frame for later
serialPort.tStop = t # not accounting for scr refresh
serialPort.tStopRefresh = tThisFlipGlobal # on global time
serialPort.frameNStop = frameN # exact frame index
# add timestamp to datafile
thisExp.addData('serialPort.stopped', t)
# update status
serialPort.status = FINISHED
serialPort.write(bytes('0', 'utf8'))
serialPort.status = FINISHED
# check for quit (typically the Esc key)
if defaultKeyboard.getKeys(keyList=["escape"]):
thisExp.status = FINISHED
if thisExp.status == FINISHED or endExpNow:
endExperiment(thisExp, win=win)
return
# pause experiment here if requested
if thisExp.status == PAUSED:
pauseExperiment(
thisExp=thisExp,
win=win,
timers=[routineTimer],
playbackComponents=[]
)
# skip the frame we paused on
continue
# check if all components have finished
if not continueRoutine: # a component has requested a forced-end of Routine
trial.forceEnded = routineForceEnded = True
break
continueRoutine = False # will revert to True if at least one component still running
for thisComponent in trial.components:
if hasattr(thisComponent, "status") and thisComponent.status != FINISHED:
continueRoutine = True
break # at least one component has not yet finished
# refresh the screen
if continueRoutine: # don't flip if this routine is over or we'll get a blank screen
win.flip()
# --- Ending Routine "trial" ---
for thisComponent in trial.components:
if hasattr(thisComponent, "setAutoDraw"):
thisComponent.setAutoDraw(False)
# store stop times for trial
trial.tStop = globalClock.getTime(format='float')
trial.tStopRefresh = tThisFlipGlobal
thisExp.addData('trial.stopped', trial.tStop)
# the Routine "trial" was not non-slip safe, so reset the non-slip timer
routineTimer.reset()
thisExp.nextEntry()
# completed 20.0 repeats of 'trials'
if thisSession is not None:
# if running in a Session with a Liaison client, send data up to now
thisSession.sendExperimentData()
# Close serialPort
if serialPort.is_open:
serialPort.close()
# mark experiment as finished
endExperiment(thisExp, win=win)
def saveData(thisExp):
"""
Save data from this experiment
Parameters
==========
thisExp : psychopy.data.ExperimentHandler
Handler object for this experiment, contains the data to save and information about
where to save it to.
"""
filename = thisExp.dataFileName
# these shouldn't be strictly necessary (should auto-save)
thisExp.saveAsWideText(filename + '.csv', delim='auto')
thisExp.saveAsPickle(filename)
def endExperiment(thisExp, win=None):
"""
End this experiment, performing final shut down operations.
This function does NOT close the window or end the Python process - use `quit` for this.
Parameters
==========
thisExp : psychopy.data.ExperimentHandler
Handler object for this experiment, contains the data to save and information about
where to save it to.
win : psychopy.visual.Window
Window for this experiment.
"""
if win is not None:
# remove autodraw from all current components
win.clearAutoDraw()
# Flip one final time so any remaining win.callOnFlip()
# and win.timeOnFlip() tasks get executed
win.flip()
# return console logger level to WARNING
logging.console.setLevel(logging.WARNING)
# mark experiment handler as finished
thisExp.status = FINISHED
logging.flush()
def quit(thisExp, win=None, thisSession=None):
"""
Fully quit, closing the window and ending the Python process.
Parameters
==========
win : psychopy.visual.Window
Window to close.
thisSession : psychopy.session.Session or None
Handle of the Session object this experiment is being run from, if any.
"""
thisExp.abort() # or data files will save again on exit
# make sure everything is closed down
if win is not None:
# Flip one final time so any remaining win.callOnFlip()
# and win.timeOnFlip() tasks get executed before quitting
win.flip()
win.close()
logging.flush()
if thisSession is not None:
thisSession.stop()
# terminate Python process
core.quit()
# if running this experiment as a script...
if __name__ == '__main__':
# call all functions in order
expInfo = showExpInfoDlg(expInfo=expInfo)
thisExp = setupData(expInfo=expInfo)
logFile = setupLogging(filename=thisExp.dataFileName)
win = setupWindow(expInfo=expInfo)
setupDevices(expInfo=expInfo, thisExp=thisExp, win=win)
run(
expInfo=expInfo,
thisExp=thisExp,
win=win,
globalClock='float'
)
saveData(thisExp=thisExp)
quit(thisExp=thisExp, win=win)