Timing things accurately

Dear Igoristas,

I'm currently using background functions for waiting a specific time during two data acquisition cycles. I'm using a background function as I want Igor to be responsive and not locked up.

Now I'm faced with the task of getting this waiting to be more accurate. From reading the documentation it does not seem to be possible to get the background functions execute with a finer granularity as 1 tick.

I would have thought that using Sleep could help here, e.g. I could do that in the background function if only a couple of ticks are left. But that seems really not up to the precision I need according to my investigation.

So should I do busy waiting with stopmstimer(-2)?

The first graph show sthe granularity of each timer. The second the sleep accuracy.

Function CompareTiming()

    variable i, numRuns = 1000
    variable reltimeDateTime, reltimemstimer, reltimeticks

    make/O/D/N=(numruns, 3) data

    reltimeDateTime = DateTime
    reltimeTicks    = ticks
    reltimeMstimer  = stopmstimer(-2)

    for(i = 0; i < numRuns; i += 1)
        data[i][0] = datetime
        data[i][1] = ticks
        data[i][2] = stopmstimer(-2)

//      printf "i: %d, DateTime: %g, ticks: %g, mstimer: %g\r", i, data[i][0], data[i][1], data[i][2]
    endfor

    data[][0] = data[p][0]- reltimedatetime
    data[][1] = data[p][1]/60 - reltimeticks/60
    data[][2] = data[p][2]/1e6 - reltimemstimer/1e6
End

Function CheckSleepTiming()

    variable i, numruns = 100, reftime, interval = 0.1

    Make/O/n=(numruns)/d dataSleep

    reftime = stopmstimer(-2)

    for(i = 0; i < numRuns; i += 1)
        sleep/s interval
        datasleep[i] = stopmstimer(-2)
    endfor
   
    datasleep[] = (datasleep[p]  - reftime)/ 1e6 - p * interval
End

Window Graph1() : Graph
    PauseUpdate; Silent 1       // building window...
    Display /W=(127.5,305.75,824.25,789.5) data[*][0],data[*][1],data[*][2]
    ModifyGraph mode=4
    ModifyGraph rgb(data)=(0,0,0),rgb(data#1)=(65535,16385,16385),rgb(data#2)=(2,39321,1)
    Legend/C/N=text0/J/A=MC/X=-17.73/Y=18.47 "\\s(data) DateTime\r\\s(data#1) ticks\r\\s(data#2) stopmstimer(-2)"
EndMacro

Window Graph3() : Graph
    PauseUpdate; Silent 1       // building window...
    Display /W=(138.75,822.5,727.5,1221.5) dataSleep
    ModifyGraph mode=4
    Legend/C/N=text0/J/X=53.03/Y=5.11 "\\s(dataSleep) dataSleep"
EndMacro
Igor's sleep operation converts the user-supplied timeSpec parameter into ticks, so you can't get granularity finer than one tick.

As for background tasks, Igor's outer loop fires approximately every 20 ms (50 Hz). So setting the period of a background task to 1 tick won't result in consistent inter-execution intervals.

If you need very precise timing, you shouldn't use background tasks (or sleep). Instead, sit in a tight loop that uses StopMSTimer(-2) to determine when to break out of the loop. And if you do this, your tight loop should be a do...while loop, not a for...endfor loop, because the later sometimes allows events to be processed to keep Igor responsive (and if you need tight timing, you don't want events to be processed).
It looks like stopmstimer is threadsafe; Is it possible to start a timer in the main thread, then run the loop Adam suggested in its own thread?
Although StopMSTimer is marked as being threadsafe, from looking at our code, it looks like this is actually only true if you pass -1 or -2 for timerRefNum. Timers 0-9 are not protected by a mutex, so if they were accessed from different threads at the same time you could run into problems. It is likely, though not guaranteed, that you would get what you expect if you started a timer using StartMSTimer from the main thread and then called StopMSTimer from only a single Igor thread.

I will look into making StopMSTimer (and also StartMSTimer) fully threadsafe for Igor 8.
You say this is all related to data acquisition. I would say that using software to time data acquisition is not a great idea if accuracy and reliability are important. If you use tools like NIDAQ Tools MX, you should be able to time the actual acquisition using hardware on your DAQ device. Use the background task only to check if new data is available and to process it when it is available.

Even if you get Igor to time something with sufficient accuracy, I think you will find that software timing of any kind is prone to erratic behavior. Sometimes the erratic behavior will only affect you once in a few hours, so that during development you think you have the solution, only to find over a period of months (or years!) that the solution is almost but not quite good enough.

John Weeks
WaveMetrics, Inc.
support@wavemetrics.com
I second John Weeks' response. In addition to the timing uncertainty arising in event-driven operating systems, consider that the master timing clock in your computer is probably not close enough to your acquisition hardware timing clock to guarantee long-term precision alignment. In fact, I have encountered a data acquisition situation where hardware timing clocks on different cards/devices had to be externally locked.
I did a test on StopMSTimer and Sleep, result is that StopMSTimer is far more accurate compared to Sleep, but suffered to the cost of a high CPU load (about 30%)
If you dont want to block Igor's main running loop, you can put the timming function in a preemptive thread. However, the CPU load is still very high if you just using a loop to check the value StopMSTimer returned.
Also, Igor 6.37 seems to be more accurate compared to Igor 7 when N becomes bigger in the same condition...
At last, I quite agree that it is really difficult to get very accurate timming just using software.

function f1()   //use StopMSTimer
    variable t0=stopmstimer(-2)
    do
        if(stopmstimer(-2)-t0-10000>0) //interval = 0.01 s
            return round(stopmstimer(-2)-t0)
            break
        endif
    while(1)
end

function f2()  //use Sleep
    variable t0=stopmstimer(-2)
    Sleep/S 0.01
    return round(stopmstimer(-2)-t0)
end

function test(N)
    variable N
    make/N=(N)/O ddd1,ddd2
    variable i
    for(i=0;i<N;i+=1)
        ddd1[i]=f1()
        ddd2[i]=f2()
    endfor
    checkdisplayed  ddd1
    if(!V_Flag)
        display ddd1
        display ddd1,ddd2
        ModifyGraph lsize=2,rgb(ddd2)=(0,0,65280)
        Legend/C/N=text0/J/F=0/A=MC "\\s(ddd1) StopMSTimer\r\\s(ddd2) Sleep"
    endif
end


//use multi-thread
threadsafe function f3()
    variable t0=stopmstimer(-2)
    do
        if(stopmstimer(-2)-t0-10000>0) //interval = 0.01 s
            return round(stopmstimer(-2)-t0)
            break
        endif
    while(1)
end

function test2(N)
    variable N
    variable/G tgID,i,index
    make/O/N=(N) ddd1
    tgID=ThreadGroupCreate(1)
    for(i=0;i<N;i+=1)
        threadstart tgID,0,f3()
        index=threadgroupwait(tgID,inf)
        ddd1[i]=threadreturnvalue(tgID,index)
    endfor
    checkdisplayed  ddd1
    if(!V_Flag)
        display ddd1
    endif
end
@wings: As I mentioned above, the Sleep operation converts your time specification into an integer number of ticks. This conversion is done using regular C/C++ casting, not by rounding. So your f2() function, which specifies a sleep time of 0.01 seconds, executes with a sleep duration of 0 ticks (effectively (int)(0.01*60)). So you're actually measuring the overhead of the sleep operation itself. On my Windows machine, the actual duration of the call to the Sleep operation is smaller and more reliable using Igor 7.06 (4.4 ms average, 0.5 ms standard deviation, n=1000) versus Igor 6.37 (7.4 ms average, 4.0 ms standard deviation, n=1000). You might have interpreted this as Igor 6.37 being more accurate since 7.4 ms is closer to the 10 ms you specified to the Sleep operation, but that's just an artifact of the particular sleep duration you selected.

We should probably add a note to the Sleep operation that makes it clear that the operation is not intended to be accurate for very small sleep durations.
Thanks. Your explanation for Sleep really helps. I had a wrong understanding of that operation before...Even yesterday I was so curious how can Sleep accuracy be that unacceptable...

However, when I say the timing accuracy behavior in different Igor version I mean StopMSTimer but not Sleep. follows may make what I want to say more clear:
Function f1()
    Variable t0=StopMSTimer(-2),dt
    do
        dt=StopMSTimer(-2)-t0
        if(dt>10000)
            return dt
        endif
    while(1)
End


Function Test(N)
    Variable N
    Make/O/N=(N) ddd1
    Variable i
    for(i=0;i<N;i+=1)
        ddd1[i]=f1()
    endfor
    CheckDisplayed ddd1
    if(!V_Flag)
        WaveStats/Q ddd1
        Display ddd1
        TextBox/N=tb1 "The Standard Deviation is " + num2str(V_sdev) + " us"
        ModifyGraph highTrip(left)=100000
        ModifyGraph mode=3,marker=8
    else
        WaveStats/Q ddd1
        TextBox/C/N=tb1 "The Standard Deviation is " + num2str(V_sdev) + " us"
    endif
End

Result in my case:
1 Igor 6.37,win
with N=1000, the standard deviation is typically less than 0.1 micro seconds

2 Igor 7.02, win
with N=1000, the standard deviation is typically about several tens of micro seconds (20~40)

FYI
I'm not seeing such a large difference in the standard deviation of the timing using your last test. Generally, the number is a bit lower using IP 6.37, but I typically see more like a 2-3 fold difference in the standard deviation, rather than an order of magnitude. I notice that in Igor 6.37, the cursor isn't animated like it is in Igor 7. It's possible that the animation has something to do with this. I don't think the underlying code of StopMSTimer has changed between Igor 6 and 7.

In any case, if you need to do what this test does and you need high accuracy, you are using the wrong program on the wrong operating system.
Thanks all for you answers.

I do know that trying to time things accurately in the low ms range on a general purpose operating system is a difficult task. But I can't do the timing in hardware as we currently don't use NI hardware but ITC/Heka hardware. Additionally I can't do everything in hardware as I want to execute Igor code at some defined moments and I did not find a PLC supporting Igor ;).

@adam: I'll look into the do/while loop thing.
aclight wrote:
In any case, if you need to do what this test does and you need high accuracy, you are using the wrong program on the wrong operating system.


Does that mean it is more accurate on MacOSX?
thomas_braun wrote:
I do know that trying to time things accurately in the low ms range on a general purpose operating system is a difficult task. But I can't do the timing in hardware as we currently don't use NI hardware but ITC/Heka hardware. Additionally I can't do everything in hardware as I want to execute Igor code at some defined moments and I did not find a PLC supporting Igor ;).

Do you mean to say that ITC doesn't have a programmable pulse/trigger/clock generator? Or perhaps that need is delegated to the stimulator? Of course, for any of that to be useful, you need to have something that calls Igor, like NIDAQ Tools MX hooks (shameless plug :).
Quote:
Does that mean it is more accurate on MacOSX?

I think that means you need to find a program that runs on RTOS :)

John Weeks
WaveMetrics, Inc.
support@wavemetrics.com
johnweeks wrote:
thomas_braun wrote:

Does that mean it is more accurate on MacOSX?

I think that means you need to find a program that runs on RTOS :)

Yes, exactly.
johnweeks wrote:

Do you mean to say that ITC doesn't have a programmable pulse/trigger/clock generator? Or perhaps that need is delegated to the stimulator? Of course, for any of that to be useful, you need to have something that calls Igor, like NIDAQ Tools MX hooks (shameless plug :).


With the ITC hardware I have to manually monitor the fifo and stop when I've collected enough data. And of course we are already planning to support NI hardware.

johnweeks wrote:
I think that means you need to find a program that runs on RTOS :)


Yeep that would be the best solution. But for that I would also like to have a "IgorProToC" XOP.