Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

Supported by

[open] Eyelink, measuring saccade latencies with a script

edited November 2012 in OpenSesame

Dear Sebastiaan,

We are facing a problem concerning the real-time use of eyelink data in an experiment. What we would like to do is to monitor saccade latencies in one condition and then use that information in another condition. Actually pretty similar to one of your examples, the curvature demo;)

However, the latency information we acquire that way does not exactly seem to match with the data we get out of the edf-file. Also, we sometimes get strange latencies of 0ms, while there was nothing wrong with the saccade as it turned out in the edf-file.

So we were wondering whether monitoring eyelink samples within a loop (or for instance using the wait_for_fixation_end function) just gives slightly different (shorter) latencies than those you would get watching individual trials with eyelink's data-viewer. Or do you think we could have done something wrong?

And out of mere interest, could you perhaps explain a little bit more about how the wait_for_fixation_end function works?

Thanks a lot!
Jeroen

(OpenSesame is great though!)

Comments

  • edited November 2012
    So we were wondering whether monitoring eyelink samples within a loop (or for instance using the wait_for_fixation_end function) just gives slightly different (shorter) latencies than those you would get watching individual trials with eyelink's data-viewer.

    Basically, yes. There are many ways to determine the onset of a saccade, and the choice for a particular one is fairly arbitrary. If you have implemented your own algorithm, for example by looking at individual samples and seeing when the eyes cross a particular border (as a for example, I don't know what you did, of course), this is bound to give different results from what you what get in the data-viewer, which is based on the Eyelink's built-in detection algorithm, Not necessarily bad, though, just different. However, if you get a lot of 0ms saccade latencies, the algorithm is probably not optimal.

    And out of mere interest, could you perhaps explain a little bit more about how the wait_for_fixation_end function works?

    The Eyelink sends a stream of data to the experimenter PC, and this function simply waits until a fixation end event is received. So this function (when used correctly) should give latencies that are approximately like those in the data-viewer. Just approximately, though, because you're still dealing with slight processing and network delays.

    Cheers!

    There's much bigger issues in the world, I know. But I first have to take care of the world I know.
    cogsci.nl/smathot

  • edited December 2012

    Hi Sebastiaan,

    I've spoken to Jeroen, and actually this last part doesn't seem to be completely true (and may even be considered a bug):

    The Eyelink sends a stream of data to the experimenter PC, and this function simply waits until a fixation end event is received

    I happened to have a similar issue to Jeroen at the moment (bottom line is: we had issues with the wait_for_[saccade|fixation]_[start|end] functions; they didn't always seem to wait properly)

    What I spotted is that these functions all call wait_for_event(), which in turn does basically this:


    while d != event:
    d = pylink.getEYELINK().getNextData()

    The problem lies in that getNextData() doesn't start getting data from the moment in time it is called: it starts from the first datapoint in the buffer. And this buffer isn't automatically cleared during a trial.

    So calling wait_for_saccade_end() actually doesn't necessarily block and wait for a saccade; it reads the buffer up to the first ENDSACC event. If this event already occured BEFORE the call to wait_for_saccade_end(), the function actually doesn't wait at all!

    The (quick and dirty?) solution I've found is to call

    pylink.getEYELINK().reset()
    before we do our wait_for_X calls
    This clears the link buffer (but maybe also does other things?). A quick test indicates that this indeed ignores events that occured before the wait_for_X call.

    Do you know whether this is a complete solution? If so, should the wait_for_event() function actually have this call to reset() in their implementation? Because now they don't do what they describe...

    Curious to hear your thoughts!
    Wouter

  • edited 10:08PM
    The problem lies in that getNextData() doesn't start getting data from the moment in time it is called: it starts from the first datapoint in the buffer. And this buffer isn't automatically cleared during a trial.

    Thanks for pointing that out. I actually didn't know that.

    Do you know whether this is a complete solution? If so, should the wait_for_event() function actually have this call to reset() in their implementation? Because now they don't do what they describe...

    I would be inclined to think so, but best to be sure. Perhaps you guys could post this on the SR Research forum, and see what they say. If they confirm this, we should indeed add a buffer reset to the wait_for_event() function, perhaps in the form of a keyword:

    def wait_for_event(self, event, clear_buffer=True):
        ...
    

    As an aside, do you know whether the same applies to the libeyelink.sample() function. Probably not, right?

    There's much bigger issues in the world, I know. But I first have to take care of the world I know.
    cogsci.nl/smathot

  • edited December 2012

    Hey Sebastiaan,

    Yeah I was hoping you know what resetData() excactly does: its description states:


    3.3.65 resetData
    resetData()
    Prepares link buffers to receive new data.
    Parameters
    < clear >: If nonzero, removes old data from buffer.
    Return Value
    Always 0.
    This function is equivalent to the C API INT16 eyelink_reset_data(INT16 clear);

    But it only works when you give it no arguments (and indeed this then seems to clear the buffer);

    ...But the buffer DOES get cleared before the start of a new trial... maybe the implementation of 'drift_correct' accidentally does that?

    libeyelink.sample() is not affected; this uses pylink.getEYELINK().getNewestSample()


    3.3.31 getNewestSample getNewestSample()
    Check if a new sample has arrived from the link. This is the latest sample, not the oldest sample that is
    read by getNextData(), and is intended to drive gaze cursors and gaze-contingent displays.
    Parameters
    None
    Return Value
    None if there is no sample, instance of Sample type otherwise.

    A quick search through the SR-boards gives me these relatively interesting threads:
    https://www.sr-support.com/showthread.php?2853-DCORR-FAILED-makes-pylink-crash&highlight=resetDATA
    https://www.sr-support.com/showthread.php?159-getNextData-return-value-63-(python)&amp;highlight=resetDATA

    BTW: the API ref manual also notes the function:


    waitForData
    waitForData(maxwait, samples, events)
    Waits for data to be received from the eye tracker. Can wait for an event, a sample, or either. Typically used after record start to check if data is being sent.
    Parameters
    <maxwait>: time in milliseconds to wait for data.
    <samples>: if 1, return when first sample available.
    <events>: if 1, return when first event available.
    Return Value
    1 if data is available; 0 if timed out.
    This function is equivalent to the C API INT16 eyelink_wait_for_data (UINT32 maxwait, INT16 samples, INT16 events);

    ... I couldn't get it to work at first, but maybe that is caused by this function also counting from the the first sample point, not the latest. It might pcovide an interesting alternative to the while d!=eventGDN loop

    I'm making a post on the SR-research page later today!

    Wouter

  • edited 10:08PM
    ...But the buffer DOES get cleared before the start of a new trial... maybe the implementation of 'drift_correct' accidentally does that?

    The buffer probably starts empty with each startRecording() call, which is called by the eyelink_start_recording plug-in.

    I'm making a post on the SR-research page later today!

    Great! If they confirm your suspicion about the buffer (I think they will), libeyelink needs to be patched. But the changes would be trivial.

    • Add a clear_buffer=True keyword to all the wait_* methods.
    • And (optionally) use waitForData(), as you suggest in your last post. There will still be a loop necessary though, because the function needs to wait for a specific event, rather than just any event.

    Would it be possible for you guys to test this and (ideally) submit a patch through GitHub? I will then push out a new version of the plug-ins.

    There's much bigger issues in the world, I know. But I first have to take care of the world I know.
    cogsci.nl/smathot

  • edited 10:08PM

    Here's the thread. As I feared, SR-research seems to really be against using resetData()...

    https://www.sr-support.com/showthread.php?3198-link-buffer-clearing-it-during-a-trial&p=11812&posted=1#post11812

    It seems that their solution for gaze-contingent or event-contingent designs really is to 'call getNextData() as often as possible', i.e. always work in a script which runs within a while-loop and calls getNextData() or getFloatData() every iteration. This doesn't seem to be a solution for the libeyelink.wait_for_event() function, because the buffer might already be filling up before it is called..

    I've further tried to clarify the issue; and proposed another possible solution.. I'm really hoping we won't have to redefine the way these functions can be used!

  • edited December 2012

    Ok, it seems there's a solution see the SR-research thread:

    The following seems best:
    in start_recording():

    • call trackerTime(), compare that self.time() and store the difference. (in self.t_sync_tracker or something)

    in wait_for_event():

    • in the while loop, ignore events that occurred before the call to wait_for_event()

    • I could still add clear_buffer=True to all argument lists in all functions, but I'm not sure if this is still appropriate: we're not really clearing a buffer. Also, what I understood, NOT clearing the buffer and looking for events is actually bad practice, since after a certain delay we don't know what's in the buffer anymore (e.g. I don't know the buffer size and I don't know when this buffer would overflow and start to become unreliable). What do you think?

    I'll write a patched version of libeyelink.py tonight, and will test it with Daniel tomorrow. He's testing his functions to send a backdrop to the eyelink PC tomorrow as well, and if all is well, we can submit all these changes at once.

    I'll also attempt to update the documentation for these functions where/if necessary

  • edited 10:08PM

    Yes, I agree with pretty much all of it. Perhaps instead of a clear_buffer keyword, there could be an ignore_old keyword (True by default). Otherwise there is no way for the user to retrieve old events through libeyelink.

    def wait_for_event(self, event, ignore_old=True):
       (...)

    Regarding the potential buffer overflow: Perhaps there should be an explicit resetBuffer() call in start_recording(). As you pointed out, it appears that the buffer is cleared at recording start anyway. But maybe not always (?), so adding a resetBuffer() to be sure won't hurt.

    Cheers!

    There's much bigger issues in the world, I know. But I first have to take care of the world I know.
    cogsci.nl/smathot

  • edited 10:08PM

    The fix worked! Daniel and I are putting this together and I hope we can push the changes later today.

    Perhaps there should be an explicit resetBuffer() call in start_recording(). As you pointed out, it appears that the buffer is cleared at recording start anyway. But maybe not always (?), so adding a resetBuffer() to be sure won't hurt.

    you mean the pylink resetData() function I spoke of earlier? I'm not sure.. I can't seem to find what this function does exactly -- and the SR-people's responses are somewhat off-putting ;). Furthermore, start_recording() already makes calls to

    • startRecording() , which I'm 90% sure creates a new link buffer
    • waitForBlockStart(), which states in its description "Reads and discards events in data queue until in a recording block. Waits for up to <timeout> milliseconds for a block containing samples, events, or both to be opened. Items in the queue are discarded until the block start events are found and processed"

    ...so it would seem buffer clearing is already resolved. As for an ignore_old keyword; I'll put it in, (i.e. if set to false, don't bother to check the timing of the event) but I'm not sure how reliable this will be; if you look at this thread, the SR-folks state:

    In fact, I can replicate the lost event messages if I add a 200 msec delay immediately before the getEYELINK().getNextData() call (...)

    This suggests that already after 200ms, buffer data will become unreliable... I'm thinking that if you want to provide users the option to inspect datapoints 'from the past', functions like startPlayBack() should be the way to go.
    Agree?

    One final issue; the wait_for_event() functions currently also return a timestamp, defined by d.getTime() ; This is, however, the timestamp of the event on the Eyelink Host. Now that libeyelink knows the time difference between the two, we could change these functions to return:

    • the timestamp converted to local time, so that it matches with exp.time()
    • the time that the 'wait' took (which would provide an immediate measure for latencies (only if EFIX is used though, not ESACC))
    • keep it the way it is, the Eyelink event timestamp

    which would you prefer? I'd say the first: its easy to implement and interpret, and it makes the return value compatible with other timing functions used in OS.

  • edited 10:08PM
    ...so it would seem buffer clearing is already resolved.

    Good.

    ...so it would seem buffer clearing is already resolved. As for an ignore_old keyword; I'll put it in, (i.e. if set to false, don't bother to check the timing of the event) but I'm not sure how reliable this will be;

    Well, in that case maybe leave it. I guess this option will hardly be used anyway, and people can always refer directly to the pylink API if they need some functionality that is not supported by libeyelink.

    I'm thinking that if you want to provide users the option to inspect datapoints 'from the past', functions like startPlayBack() should be the way to go. Agree?

    I don't understand the reference to startPlayBack() to be honest: Is that an existing pylink function, or do you suggest adding a new function by that name to libeyelink?

    which would you prefer? I'd say the first: its easy to implement and interpret, and it makes the return value compatible with other timing functions used in OS.

    Yes, I agree.

    There's much bigger issues in the world, I know. But I first have to take care of the world I know.
    cogsci.nl/smathot

  • edited 10:08PM

    Alright, it seems we're completely on the same page then, and I'll implement these changes and send them to Daniel:

    I don't understand the reference to startPlayBack() to be honest: Is that an existing pylink function, or do you suggest adding a new function by that name to libeyelink?

    Oh sorry, I spotted that function in the Pylink API ref man (that's also where I got my function descriptions from). There are a couple of functions listed there, intended for 'playback' mode, and what I get from it, its a way to 'rewind' the datafile and fetch the samples again.

    I meant it in a "if we ever want to give users the option of looking at past sample points and events, it seems we would in the future have to write wrappers around these playback functions; using those seems to be the 'clean' way, contrasted to using buffer data".

    Here's the ref manual.. a bit dated but it's worked for me so far.

  • edited 10:08PM

    Thanks (for the manual and the work on the Eyelink plug-in)! Daniel submitted the changes, and I just pushed out an update: https://github.com/smathot/opensesame_eyelink_plugins/tags

    There's much bigger issues in the world, I know. But I first have to take care of the world I know.
    cogsci.nl/smathot

  • edited 10:08PM

    Ok this is getting interesting (and annoying):

    I tested my fix with wait_for_saccade_end(), which was the function I personally needed, which worked perfectly. The behavior of the other wait_for_ functions was fine: they waited and didn't break on past events.

    However, today I conducted some more tests with the other wait_for functions; a simple experiment where all 4 wait_for_functions were called in sequence (looped 5x) and this was the output.

    The output was disturbing; all timing data seemed fine, but gaze data was -32768.0 for all events other then ESACC.

    Some research on this number had me land on an eye_data C header file, with the code fragment:


    /*********** EYE DATA FORMATS **********/ /* ALL fields use MISSING_DATA when value was not read, */
    (...) #define MISSING_DATA -32768 /* data is missing (integer) */
    #define MISSING -32768
    #define INaN -32768

    Looking for MISSING_DATA in the pylink API again, brough up the following segment:

    If certain property information not sent for this sample, the value MISSING_DATA (or 0, depending on the field) will be returned, and the corresponding bit in the flags field will be zero (see eye_data.h for a list of bits). Data may be missing because of the tracker configuration (set by commands sent at the start of the experiment, from the Set Options screen of the EyeLink II tracker, or from the default configuration set by the DATA.INI file for the EyeLink I tracker). Eye position data may also be set to MISSING_VALUE during a blink.

    But also (in a section on parsing event data):

    (...) due to the tracker configuration, some of the property information returned may be a missing value MISSING_DATA (or 0, depending on the field). So make sure you check for the validity of the data before trying to use them. To do the tracker configuration, the user can use the setLinkEventFilter() and setLinkEventData() methods of the EyeLink class to send commands at the start of the experiment or modify the DATA.INI file on the tracker PC.

    These functions are not in set in libeyelink.py , so I'm guessing the eyelink configuration file is set to actually not send this data across, except for ESACC data!

    This is very possible, as it would correspond to the flags FIXAVG and NOSTART:
    (pylink API:)


    3.4.4.3 setLinkEventData
    setLinkEventData(list)
    Sets data in events sent through link. See tracker file “DATA.INI” for types.
    Parameters
    <list>: list of data types, separated by spaces or commas.
    GAZE screen xy (gaze) position
    GAZERES units-per-degree angular resolution
    HREF HREF gaze position
    AREA pupil area or diameter
    VELOCITY velocity of eye motion (avg, peak)
    STATUS warning and error flags for event
    FIXAVGinclude ONLY average data in ENDFIX events
    NOSTART start events have no data, just time stamp
    Return Value
    None;
    Remark:
    This function is equivalent to EYELINK.sendCommand("link_event_data = %s"%list); 3.4.4.4 setLinkSampleFilter
    setLinkSampleFilter(list)
    Sets data in samples sent through link. See tracker file “DATA.INI” for types.
    Parameters
    <list>: list of data types, separated by spaces or commas.
    GAZE screen xy (gaze) position
    GAZERES units-per-degree screen resolution
    HREF head-referenced gaze
    PUPIL raw eye camera pupil coordinates
    AREA pupil area
    STATUS warning and error flags
    BUTTON button state and change flags
    INPUT input port data lines
    Return Value
    None;
    Remark:
    This function is equivalent to EYELINK.sendCommand("link_sample_data = s"%list)

    ...my .asc don't seem to have a 'link_event_data' entry (result from simple grep) so I'm guessing there's a data.ini file bothering us.

    Proposed quick test/fix: I'll quickly patch libeyelink that it sends an appropriate command upon initialization.

    if this works,

    Proposed final fix: I don't know what the downside of sending 'everything' is, but one of the following might be a solution:

    • send these commands either way to ensure control over these samples, but don't offer the user to return fields that do not comply with this command.
    • extend the eyelink_calibrate plugin to allow the user to determine per experiment themselves what sample/event data is needed and sent.

    What do you think?
    Wouter

  • edited 10:08PM

    Oh wait, I just gave libeyelink.py a proper look, and spotted (~line 138):


    # Set link data. This specifies which data is sent through the link and thus can
    # be used in gaze contingent displays
    self.send_command("link_event_filter = LEFT,RIGHT,FIXATION,SACCADE,BLINK,BUTTON")
    1. I guess self.send_command('link_event_data = ...') should also be sent here.
    2. Do you remember whether you have thoroughly tested whether changes in these lines actually have effect? The description of pylink's setEventFilter() gives me the impression that these filter commands should be sent as types, not strings. If that's true, this also affects file_sample_data, defined in the commands following .
    3. not sure about this one, but are you sure these commands are actually not sent too early? They don't appear in the edf2asc - converted file. -- then again maybe they also shouldn't.
  • edited 10:08PM

    Thanks for trying to sort this mess out! And, indeed, from what you say it appears that the main problem is that link_event_data has not been properly set.

    Do you remember whether you have thoroughly tested whether changes in these lines actually have effect?

    No, I have not tested this in any systematic way. Basically, I have grabbed these lines (and a lot of other stuff as well) from an example that came with PyLink.

    The description of pylink's setEventFilter() gives me the impression that these filter commands should be sent as types, not strings. If that's true, this also affects file_sample_data, defined in the commands following .

    From the PyLink docs, it appears that many of these commands can be set in two ways. Either by calling setEventFilter() (or similar functions), or by sending a link_event_filer = (...) command through sendCommand(). I think the function API is cleaner though, and less error-prone, so feel free to convert these string commands to the corresponding function calls.

    not sure about this one, but are you sure these commands are actually not sent too early? They don't appear in the edf2asc - converted file. -- then again maybe they also shouldn't.

    I couldn't say for sure, although I do believe I kept the general structure of the PyLink example, which I assume was correct. What makes you think there is a problem here?

    I don't know what the downside of sending 'everything' is

    That's a good question. The fact that the default is to not send everything suggests that there is a downside, but you would have to ask the Eyelink guys.

    extend the eyelink_calibrate plugin to allow the user to determine per experiment themselves what sample/event data is needed and sent.

    That would be ideal, yes. The default should then be that everything is sent, to protect the user from missing data.

    The bottom line ...

    ... is that there should be a setLinkEventData() / link_event_data = (...) call to make sure that sufficient data is sent across the link. Ideally, we would make this and the link event filter configurable through the GUI. Perhaps this last part is only necessary if the Eyelink guys tell us if there is any reason to not send everything through the link.

    There's much bigger issues in the world, I know. But I first have to take care of the world I know.
    cogsci.nl/smathot

  • edited 10:08PM

    Ok, progress! I sent a 'link_event_data' command to the tracker, and immediately the return values made more sense. :)
    So you can ignore my questions regarding whether the commands would have been sent too early, or whether it should be strings or possible event_data constants: it seems sending a simple command string on initialization of the tracker is indeed the way to solve this.

    BUT! off course things didn't completely work right away. All events seem to behave except for the saccade_start event. I detailed the behaviour in another thread on the SR-support forum, as it just screams 'bug!'

    Ideally, we would make this and the link event filter configurable through the GUI. Perhaps this last part is only necessary if the Eyelink guys tell us if there is any reason to not send everything through the link.

    In the same thread I also asked this question; I'm curious what they say.

    I truly feel this should be the LAST issue with waiting for events! -knock on wood-

    Wouter

  • edited December 2012

    Alright, here it is:
    it seems the pylink library does something weird with the SSACC event link data, and my SR-guy seems reluctant to admit it. I just ran a test after my latest post in that thread, printing every field in SSACC float_data and comparing it to the accompanying EDF output: (1 trial w/ 2 saccades)...(bold text is markdown's translation of double underscore):


    EDF:
    (...)
    SSACC L 17672657
    MSG 17672690 S_START at 17672657.0
    ESACC L 17672657 17672706 50 711.3 503.9 980.8 501.6 5.64 296
    SFIX L 17672707
    EFIX L 17672707 17673678 972 990.2 503.8 5063
    SSACC L 17673679
    MSG 17673714 S_START at 17673679.0
    ESACC L 17673679 17673747 69 981.6 504.0 435.7 482.9 11.33 338
    SFIX L 17673748
    (..) contents of float_data:
    read 17672657.0 # should be integer bitstring, is timestamp
    startGaze (283.0, 711.29998779296875) # (???, start_x)
    startHref (503.89999389648437, 47.799999237060547) # (start_y, ????)
    startUpd (32768.0, 4.5999999046325684)
    startVelocity 39.2000007629
    status 0
    sttime -705.0
    time 17672657.0 # timestamp again
    type 5 ---- # same goes for the second saccade. startGaze[1] and startHref[0] seem to contain (x,y)
    S_Start
    read 17673679.0
    startGaze (284.0, 981.5999755859375)
    startHref (504.0, 47.799999237060547)
    startUpd (32768.0, 3.5)
    startVelocity 39.2000007629
    status 0
    sttime 776.0
    time 17673679.0
    type 5

    This is inconsistent with what 'my SR-research guy' says. The start gaze data definitely seems to be incorporated in the event, just set wrong.

    What do you want to do? I suggest:

    • send everything by default; we're not experiencing buffer overflow problems (see SR-support thread). Maybe we could ( at a later stage ) simply include a checkbox for 'sparse link transfer' that users can set themselves if they do miss events.
    • either Ignore SSACC gaze data, or hack our way to the relevant data using getstartHref()[0]?

    I'm more than willing to write this fix for libeyelink.py btw.

    Wouter

  • edited December 2012

    Hi Wouter,

    First off, let me say that this is great work.

    This is inconsistent with what 'my SR-research guy' says. The start gaze data definitely seems to be incorporated in the event, just set wrong.

    From the thread I reckon he means that there is a bug (which is obvious I'd say). But the nature of the bug is that there is any position data in the SSACC event at all. Apparently the intended behaviour is for the SSACC event to come without position data. This makes sense given that the EDF file also works that way (i.e. the SSACC line contains only a timestamp, and no position data). My guess is that, as you also suggest in the post, the fact that the SFIX (which also doesn't have position info in the EDF file) does give reliable data position is an accident, and not something that we should rely on for libeyelink (for 'your guy' ;-) to confirm, of course).

    What do you want to do? I suggest: - send everything by default; we're not experiencing buffer overflow problems (see SR-support thread). Maybe we could ( at a later stage ) simply include a checkbox for 'sparse link transfer' that users can set themselves if they do miss events. - either Ignore SSACC gaze data, or hack our way to the relevant data using getstartHref()[0]?

    Agreed.

    I'm more than willing to write this fix for libeyelink.py btw.

    Yes, please :)

    There's much bigger issues in the world, I know. But I first have to take care of the world I know.
    cogsci.nl/smathot

  • edited December 2012

    First off, let me say that this is great work.

    Thanks, and no problem! It's definitely one of those issues that I got determined to get to the root of and get it fixed!

    Anyway, new progress! It turns out actually we should be able to get gaze data from SSACC, and that this IS garbled up in the pylink event data:
    thread
    This will be fixed in the upcoming pylink release, but I don't know when that will come out. They are now working on a 'temporary fix'.

    As for Opensesame; I don't know yet what their 'temporary fix' could be, but as of yet I envision 3 possible workarounds for wait_for_saccade_start()

    1. We make it so that it doesn't return gaze data.
    2. We return the 'garbled' fields that do reflect startGaze(), i.e. (startGaze()[1] , startHref()[0]) (95% sure this will be consistent ;), I just tested it in all our 3 eyelink labs )
    3. We fetch the 1st next sample from the link buffer following the SSACC event with getNextData(), read it out, and fill the return gaze data with that sample

    Either way, these methods should be updated once the new pylink release is out, and checks should ensure that users will not combine the wrong implementation with the wrong pylink version, right?

    Should I implement a check somewhere (like Daniel's "Forward compatibility" lines in the set_backdrop function)? Do you like the idea to define the function conditionally? Some way of overloading, e.g. in __init__():


    if pylink.vernum == (1,0,0,37):
    self.wait_for_saccade_end = self.__wait_for_saccade_end_10037
    else:
    self.wait_for_saccade_end = self.__wait_for_saccade_end_future

    ...maybe I am getting ahead of things and should I wait for their 'temporary fix', but I have doubts that it'll be something general that is applicable to Opensesame.

    Wouter

    Edit: Just got a message from them:

    (...) My colleague, Nabil, updated a version of Windows 32-bit Pylink for you. You can get the updated version the following link: (...)

  • edited 10:08PM

    Hey Sebastiaan,

    I've suggested some changes to libeyelink.py which are now being committed by Daniel. I went for the conditional function definition I outlined above, simply because it seemed easiest.

    In short, the changes are:

    ->in __init__():

    • 'link_event_data = ...' command sent (i.e. send 'everything', maybe I'll get to that checkbox we discussed above one day; or you might feel up to it)

    • tracker_display_delay renamed to exp_eyelink_delay, cf. "the delay that exp has on eyelink". (seemed more transparant: its definition is trackerTime - exp.time)

    • pylink version check, catching SSACC event bug
      if pylink version is before 1.0.0.28, wait_for_saccade_start is redefined

    ->a function get_exp_eyelink_delay() was added. This might prove useful if people want to retrieve this difference (I myself have already used it)

    ->In all wait_for_-event- functions:

    • updated DOCs

    • increased transparency

    ->and finally:
    __wait_for_saccade_start_pre_10028() was added. To be used as internal function, added to correct for pylink SSACC bug. I've left the docstring for this function: is that sufficient to make it not appear in the documentation?

    ...Unfortunately, the eyelink guys have a messed up their version system: Daniel has a pylink 1,0,0,43 version, and in this version the SSACC data is still scrambled. I'll confront them with this, and see whether they know a better way to check whether SSACC data will be parsed good or scrambled.

    If you agree on all of this, I'd say case closed! :D But of course I'm very open to suggestions or other changes before you put the new version up for download

    Wouter

  • edited 10:08PM

    Ok, great! I merged the pull request.

    Good detective work. I was wondering, though: Why the magic version 1.0.0.28? Do the Eyelink guys say that all later versions do no longer suffer from this bug, and that all earlier versions do? (Maybe I missed that part of the discussion.)

    I have some thoughts:

    ->and finally: __wait_for_saccade_start_pre_10028() was added. To be used as internal function, added to correct for pylink SSACC bug. (...) ...Unfortunately, the eyelink guys have a messed up their version system: Daniel has a pylink 1,0,0,43 version, and in this version the SSACC data is still scrambled. I'll confront them with this, and see whether they know a better way to check whether SSACC data will be parsed good or scrambled.

    In general, I think this is a good approach, but it should be full proof. Since you mention that it's not, we might be better off just adding a warning to the docstring. I'm worried that we might confuse things even more by sometimes catching this bug. And: are we sure that all versions before 1.0.0.43 also suffer from this bug? It might be a regression that was introduced at some point, in which case this fix actually causes a bug for some users.

    I've left the docstring for this function: is that sufficient to make it not appear in the documentation?

    Yes, or more specifically: You did add a docstring (which is good, of course), but no <DOC> tags. And indeed, these tags are used to determine which functions should mentioned in the documentation. This is not something that's built-in to Python the way that docstrings are, it's just how my documentation script parses the source code.

    ->a function get_exp_eyelink_delay() was added. This might prove useful if people want to retrieve this difference (I myself have already used it)

    Yes, that's indeed a very useful function. The naming is a bit odd, though: To me it suggests that it returns the roundtrip time to the tracker or something along those lines. I.e. that it tells you how much delay there is between the tracker registering an event and your experiment knowing about it, if that makes sense. I'm not sure what an intuitive name would be. Perhaps something like get_eyelink_clock_async?

    But these are all basically small things. The main thing is that all data is now sent through the link, and that by itself is worth an update. So let me know what you think of the above. Then we may or may not change some minor things and tag the 0.21 release.

    Cheers!

    There's much bigger issues in the world, I know. But I first have to take care of the world I know.
    cogsci.nl/smathot

  • edited December 2012

    Hi Sebastiaan,

    Why the magic version 1.0.0.28?
    (...)
    I'm worried that we might confuse things even more by sometimes catching this bug. And: are we sure that all versions before 1.0.0.43 also suffer from this bug?

    The simple reason was that the current version has pylink.version.vernum = (1,0,0,27) and the fixed versions I got yield (1,0,0,28) or higher. I concluded version numbers must go up, and a simple check for vernum == 1,0,0,28 or higher should do ... I concluded Daniel's version would have to be a mere exception, and I had not yet considered the possibility of a regression

    I'll wait for SR's reply on this issue to see whether a different check should / could be implemented. If there is no way to catch all possibilities, we'll have to settle for a warning in the docstring, that some pylink versions suffer from this bug.

    The naming is a bit odd, though: (...) Perhaps something like get_eyelink_clock_async?

    Yeah I see your issue. The reason for delay was that I thought the name could indicate in which direction the asynchrony was, but I guess the documentation would be the place to clarify this. I'd be fine with async, or tdiff ... or something with offset? I'm horrible with transparent namings

    Wouter

  • edited 10:08PM

    Hey Sebastiaan,

    First off, happy new year!

    I saw today that you committed the suggested changes, aside from the potential version issue. Well, It seems the SR guys are back to work, and I just got a reply from Nabil, the SR-guy who helped Daniel (and got him hooked on 1,0,0,43) in my SR-thread.

    https://www.sr-support.com/showthread.php?3208-Event-data-from-the-link-buffer&p=12006#post12006

    It seems, that to prevent any version issues, we are now allowed to simply bundle opensesame with a version of pylink that works ... In that case we can simply ignore my dynamic 'hack'

    Wouter

  • edited 10:08PM

    First off, happy new year!

    Same to you!

    I saw today that you committed the suggested changes, aside from the potential version issue.

    Yep, I tested it a (little) bit as well, and I think it's ready to be tagged as a new release. I also added some minor tidbits myself, such as the possibility for eyelink_log to automatically detect and log variables.

    It seems, that to prevent any version issues, we are now allowed to simply bundle opensesame with a version of pylink that works ...

    That would be great, but see my comment on the SR Research forum thread. The question is whether their permission to distribute PyLink along with OpenSesame is sufficient, because they appear to explicitly forbid further redistribution (and that wouldn't be compatible with the conditions under which OpenSeame is distributed). So I guess we'll first see what their take is on this.

    Cheers!

    There's much bigger issues in the world, I know. But I first have to take care of the world I know.
    cogsci.nl/smathot

  • edited January 2013

    Hey Sebastiaan,

    Yeah, it almost seems too good to be true. Let's hear what they say.

    In the meantime, I've run into yet ANOTHER issue when analyzing my gaze-contingent experiment that got me involved in this. I think this needs to be addressed before a new release is tagged.

    Between sending a msg to tthe tracker and having it appear in the edf, there's a delay (~20ms). Therefore, to get accurate saccade latencies, I use the following code:

        # show target canvas
        t = exp.t1_c.show();
        # send srt_onset msg with onset time (on tracker):
        el.log('SRT_ONSET_MSG_1 ' + str(t + el.eyelink_clock_async))
    

    Then, when analyzing the edf, I use the tstamp in the message, not the tstamp of the message.
    However, when looking at the latencies of an experiment >1h, I see an almost linear increase of latencies over trial index, up to ~1500ms.

    My explanation would be that, unfortunately, the clocks not only differ in offset, but also in speed: 1ms in OS is not exactly 1ms on the tracker. In this case, eyelink_clock_async becomes more and more unreliable during the experiment. (...and therefore also wait_for_X would become unreliable)

    I see 2 possible solutions:

    1. eyelink_clock_async is updated every trial, upon start_recording. Although this does not work for 'long trials', such as when watching a video.

    2. get_eyelink_clock_async() is used, which would not simply be a getter, but computes trackertime - ostime everytime it is called. This -maybe- has the risk of becoming a somewhat costly operation when it is called frequently (i.e. in wait_for_X)

    I'd say we give option 2 a try; or do you see other possibilities?

  • edited 10:08PM

    Option 2 is probably the better, one, yes. Particularly because there is no rule that says you cannot make a single trial very long — long enough for some divergence to build up. The cost of syncing with every call is probably not that much. Still, we could also consider a third option, which is to keep track of the last time that the clocks have been synced, and resync if this more than, say, 5 seconds ago (not sure if this is worth the trouble, though).

    It's very strange that the clocks differ so significantly in their ticking speed. I doubt that this is an issue with OpenSesame or PyLink per se, as they both (I assume) rely on the system clock. Perhaps one of the computers is a bit off. At any rate, whatever the reason for this problem, it's clearly a possibility that needs to be taken into account.

    There's much bigger issues in the world, I know. But I first have to take care of the world I know.
    cogsci.nl/smathot

  • edited January 2013

    Option 2 is probably the better, one, yes.

    Quick intermediate result: I just implemented it, and I don't notice any lag when get_clock_async() calls getTrackerTime() instead of returning a variable. (I also called get_clock_async() from within wait_for_event(), instead of using said variable. )

    Still, we could also consider a third option, which is to keep track of the last time that the clocks have been synced, and resync if this more than, say, 5 seconds ago (not sure if this is worth the trouble, though).

    The short answer seems to be that it wouldn't be worth the trouble. I also can't yet envision where this should be done..? In a separate background thread?

    It's very strange that the clocks differ so significantly in their ticking speed. I doubt that this is an issue with OpenSesame or PyLink per se, as they both (I assume) rely on the system clock. Perhaps one of the computers is a bit off.

    I honestly wouldn't know; if I use the timestamp of the message everything is fine, and I don't see another option than that the two clocks are not synchronized. Perhaps the Eyelink itself doesn't (fully) use the system clock but simply counts its samples (assuming 1000Hz sampling) or something. In that case, a 1.5s mismatch over the course of an hour isn't too bad.

    Either way; I'll change the function and remove the variable again, and make sure the changes get to you.

Sign In or Register to comment.