4

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

Shani Shalgi
  • 617
  • 1
  • 6
  • 19
  • Did you set the photosensor in the top of the screen and what kind of delay are you seeing? A photosensor on the middle of the screen would often yield a monitor-side latency of 5-8 ms. – Jonas Lindeløv Mar 11 '15 at 17:42
  • By the way, since timing matters, do not control time using seconds and while. Use win.flip()s since visual time is discretely locked to frames anyway. So for 0.5 sec break on a 60 Hz monitor, loop 0.5*60 times over win.flip(). – Jonas Lindeløv Mar 11 '15 at 18:48
  • Hi Jonas, I checked the photo sensor at the top of the screen but also at the center, this yields similar delays. The delays I'm talking about are large- more than the screen refresh -20-40 ms. Regarding the timing using flips, since I have code in between displays (sending triggers, checking responses) I found that using only flips usually results in one extra frame. The timing I get if measuring inside psychopy (not using the oscilloscope) when programming like I did above (ttime[t]-ttime[t-1] is perfect, because I wait one frame less, then perform my code, then wait for the next flip. – Shani Shalgi Mar 16 '15 at 08:15
  • Have you solved this problem? – styrofoam fly Jun 22 '17 at 21:28
  • No, never... went back to using EPrime – Shani Shalgi Jun 25 '17 at 11:45

3 Answers3

6

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.

Jonas Lindeløv
  • 5,442
  • 6
  • 31
  • 54
  • Thanks. I modified my code like you suggested, however this does not change the situation - the delays are still the same. It appears that the flip() command first execute the code then waits for the screen vertical blank and does not execute on the vertical blank. I wonder if this can be modified. I have no idea why the long delay, when using EPrime I get no such delays so it is not the result of the monitor or graphics card. – Shani Shalgi Mar 16 '15 at 08:18
  • PsychoPy is usually perfect at this, so I still suspect that something in your code causes this. Have you tried the minimal example where you simply present a white stimulus on a black background and then do a ``stim.draw()`` and then send the trigger like above? Without all the timing stuff. Just to test whether it's something deeper in the system/PsychoPy or if it's something about how you put your script together. – Jonas Lindeløv Mar 16 '15 at 19:11
  • Yes I used a simple test code and also used the benchmark code described in https://gist.github.com/jeremygray/9062586, modifying it to send a code. When you say psychopy is perfect at this, do you mean when tested using an oscilloscope? Because testing within the code (getTime[t]-getTime[t-1]) is perfect, but the screen says different. – Shani Shalgi Mar 17 '15 at 20:51
  • Ok. Yes, good timing of the actual change in luminance on the monitor. Then it's probably something about your drivers/graphics card. PsychoPy uses OpenGL for rendering. Not sure what Eprime does but this may be the source of the problem. Try installing the latest driver for your graphics card and run again. Older drivers do not support OpenGL properly but newer sometimes does. – Jonas Lindeløv Mar 18 '15 at 08:26
  • The graphics card is new, NVidia, all the drivers are up to date. I tested using multiple monitors. There is definitely something going on but I have no idea how to solve it. – Shani Shalgi Mar 19 '15 at 11:51
  • Me neither then. Try posting at the psychopy-users list, which is better for debugging. https://groups.google.com/forum/#!forum/psychopy-users – Jonas Lindeløv Mar 19 '15 at 12:07
  • If I time my stimulus by frames (say stimulus shown for 30 frames) and I use the function you defined `win.callOnFlip(sendTrigger, code=255)`, will PsychoPy actually set the code/pins at 255 and then back to 0 a total of 30 times when it's in the loop? Since win.flip() will be called 30 times in the loop. Thanks! – hsl Jul 20 '15 at 23:51
  • @hsl, yes, if ``win.callOnFlip`` is run before each ``win.flip()``. But a nice thing of doing it this way is that you can just run it before entering the flip-loop. Then it will send the trigger on the first iteration (first call to ``win.flip()`` but not on subsequent. Good for timing stimulus onset. To get offset, then just do a ``win.callOnFlip`` and ``win.flip()`` after the flip-loop. – Jonas Lindeløv Jul 25 '15 at 22:26
  • Thanks a lot, Jonas! I've been trying to send TTL signals for stimulus and responses and I think I've already got it to work. Going to properly test it next week on my EEG system. Someone on the PsychoPy's Google forum has also helped me a lot! https://groups.google.com/forum/#!topic/psychopy-users/6svdW47TDwE – hsl Jul 26 '15 at 01:33
  • Hi @JonasLindeløv, sorry for answering to a very old post but this answer is still very relevant to me. I'm implementing the triggering for the first time and I have a doubt. With your approach, I'm calling the `setData()` method, waiting for 10ms and finally resetting the port. Does this mean that If I run the method before the a flip-loop (for stimulus presentation) there will be a 10ms delay between the first flip and the second? – Filippo Gambarota May 11 '22 at 17:33
  • @FilippoGambarota There will only be delays in the visual flips if you end up skipping a flip. Assuming a 60Hz monitor, this means that you have up to 1/60 = 0.01666 seconds = 16.7 ms to do whatever you want. Since you only spend 10ms on sending a port signal, you should be fine, assuming that other code between the first and the second flip do not take more than ~6ms. – Jonas Lindeløv May 12 '22 at 08:16
  • @JonasLindeløv thank you! so If I get correctly, assuming that I have a simple `for` that draw and flip for let's say 10 frames, if just before I do the `win.callOnFlip` with the trigger and the core.wait() is less than the frame duration, that should be fine right? – Filippo Gambarota May 12 '22 at 08:58
  • @FilippoGambarota Yes. But you can always verify by logging the durations. I used to do something like `win.callOnFlip(clock.reset)` before the flip-loop and then after the loop do `trial["actual_duration"] = clock.getTime()`. Then you know whether it the flip-loop had the desired duration. – Jonas Lindeløv May 13 '22 at 19:48
0

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

sus
  • 103
  • 1
  • 4
  • Thank you very much, I will try this. However, isn't this something that is supposed to be taken care of by Psychopy? When using EPrime there is no such lag. – Shani Shalgi Jun 01 '15 at 11:26
  • The monitor input lag depends on the monitor only, and if you don't get it with EPrime I think something else is happening here... – sus Jun 02 '15 at 13:30
0

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)

    [...]