'How to send a code to the parallel port in exact sync with a visual stimulus in Psychopy

I am new to python and psychopy, however I have vast experience in programming and in designing experiments (using Matlab and EPrime). I am running an RSVP (rapid visual serial presentation) experiment with displays a different visual stimuli every X ms (X is an experimental variable, can be from 100 ms to 1000 ms). As this is a physiological experiment, I need to send triggers over the parallel port exactly on stimulus onset. I test the sync between triggers and visual onset using an oscilloscope and photosensor. However, when I send my trigger before or after the win.flip(), even with the window waitBlanking=False parameter then I still get a difference between the onset of the stimuli and the onset of the code.

Attached is my code:


    im=[]
    for pic in picnames:               
        im.append(visual.ImageStim(myWin,image=pic,pos=[0,0],autoLog=True))

    myWin.flip() # to get to the next vertical blank
    while tm < and t &lt len(codes):                
        im[tm].draw()                                             
        parallel.setData(codes[t]) # before
        myWin.flip()                
        #parallel.setData(codes[t]) # after
        ttime.append(myClock.getTime())
        core.wait(0.01)
        parallel.setData(0)                
        dur=(myClock.getTime()-ttime[t])*1000                
        while dur < stimDur-frameDurAvg+1:
           dur=(myClock.getTime()-ttime[t])*1000
        t=t+1
        tm=tm+1            
        myWin.flip()

How can I sync my stimulus onset to the trigger? I'm not sure if this is a graphics card issue (I'm using a LCD ACER screen with the onboard Intel graphics card). Many thanks,
Shani



Solution 1:[1]

win.flip() waits for next monitor update. This means that the next line after win.flip() is executed almost exactly when the monitor begins drawing the frame. That's where you want to send your trigger. The line just before win.flip() is potentially almost one frame earlier, e.g. 16.7 ms on a 60Hz monitor so your trigger would arrive too early.

There are two almost identical ways to do it. Let's start with the most explicit:

for i in range(10):
    win.flip()

    # On the first flip
    if i == 0:
        parallel.setData(255)
        core.wait(0.01)
        parallel.setData(0)

... so the signal is sent just after the image has been pushed to the monitor.

The slightly more timing-accurate way to do it will save you like 0.01 ms (plus minus an order of magnitude). Somewhere early in the script define

def sendTrigger(code): 
    parallel.setData(code)
    core.wait(0.01)
    parallel.setData(0)

Then do

win.callOnFlip(sendTrigger, code=255)

for i in range(10):
    win.flip()

This will call the function just after the first flip, before psychopy does a bit of housecleaning. So the function could have been called win.callOnNextFlip since it's only executed on the first following flip.

Again, this difference in timing is so miniscule compared to other factors that this is not really a question of a performance but rather of style preferences.

Solution 2:[2]

There is a hidden timing variable that is usually ignored - the monitor input lag, and I think this is the reason for the delay. Put simply, the monitor needs some time to display the image even after getting the input from the graphics card. This delay has nothing to do with the refresh rate (how many times the screen switches buffer), or the response time of the monitor.

In my monitor, I find a delay of 23ms when I send a trigger with callOnFlip(). How I correct it is: floor(23/16.667) = 1, and 23%16.667 = 6.333. So I call the callOnFlip on the second frame, wait 6.3 ms and trigger the port. This works. I haven't tried with WaitBlanking=True, which waits for the blanking start from the graphics card, as that gives me some more time to prepare the next buffer already. However, I think that even with WaitBlanking=True the effect will be there. (More after testing!)

Best, Suddha

Solution 3:[3]

There is at least one routine that you can use to normalized the trigger delay to your screen refreshing rate. I just tested it with a photosensor cell and I went from a mean delay of 13 milliseconds (sd = 3.5 ms) between the trigger and the stimulus display, to a mean delay of 4.8 milliseconds (sd = 3.1 ms).

The procedure is the following :

  1. Compute the mean duration between two displays. Say your screen has a refreshing rate of 85.05 (this is my case). This means that there is mean duration of 1000/85.05 = 11.76 milliseconds between two refreshes.
  2. Just after you called win.flip(), wait for this averaged delay before you send your trigger : core.wait(0.01176).

This will not ensure that all your delays now equal zero, since you cannot master the synchronization between the win.flip() command and the current state of your screen, but it will center the delay around zero. At least, it did for me.

So the code could be updated as following :

    refr_rate = 85.05
    mean_delay_ms = (1000 / refr_rate)
    mean_delay_sec = mean_delay_ms / 1000  # Psychopy needs timing values in seconds

    def send_trigger(port, value):
        core.wait(mean_delay_sec)
        parallel.setData(value)
        core.wait(0.001)
        parallel.setData(0)

    [...]

    stimulus.draw()
    win.flip()
    send_trigger(port, value)

    [...]

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 sus
Solution 3 Valéry Benoit