2

I have a while True loop which sends variables to an external function, and then uses the returned values. This send/receive process has a user-configurable frequency, which is saved and read from an external .ini configuration file.

I've tried time.sleep(1 / Frequency), but am not satisfied with the accuracy, given the number of threads being used elsewhere. E.g. a frequency of 60Hz (period of 0.0166667) is giving an 'actual' time.sleep() period of ~0.0311.

My preference would be to use an additional while loop, which compares the current time to the start time plus the period, as follows:

EndTime = time.time() + (1 / Frequency)
while time.time() - EndTime < 0:
    sleep(0)

This would fit into the end of my while True function as follows:

while True:
    A = random.randint(0, 5)
    B = random.randint(0, 10)
    C = random.randint(0, 20)

    Values = ExternalFunction.main(Variable_A = A, Variable_B = B, Variable_C = C)

    Return_A = Values['A_Out']
    Return_B = Values['B_Out']
    Return_C = Values['C_Out']

    #Updated other functions with Return_A, Return_B and Return_C

    EndTime = time.time() + (1 / Frequency)
    while time.time() - EndTime < 0:
        time.sleep(0)

I'm missing something, as the addition of the while loop causes the function to execute once only. How can I get the above to function correctly? Is this the best approach to 'accurate' frequency control on a non-real time operating system? Should I be using threading for this particular component? I'm testing this function on both Windows 7 (64-bit) and Ubuntu (64-bit).

jars121
  • 1,127
  • 2
  • 20
  • 35
  • Do you want to execute the `ExternalFunction.main` at a given frequency? – hugos May 04 '17 at 00:18
  • Yes that's correct. I've used threading.timer() and apscheduler in the past; perhaps these options would be better? EDIT: apscheduler only accepts second integers, so is not suitable for millisecond precision. – jars121 May 04 '17 at 00:23
  • I think you're on the right track, what my answer basically suggests is storing the time before the loop and "fill" the remaining period in the end. Very similar to what you are doing. – hugos May 04 '17 at 00:33

3 Answers3

1

If I understood your question correctly, you want to execute ExternalFunction.main at a given frequency. The problem is that the execution of ExternalFunction.main itself takes some time. If you don't need very fine precision -- it seems that you don't -- my suggestion is doing something like this.

import time

frequency = 1  # Hz
period = 1.0/frequency

while True:
    time_before = time.time()
    [...]
    ExternalFunction.main([...])
    [...]
    while (time.time() - time_before) < period:
        time.sleep(0.001)  # precision here

You may tune the precision to your needs. Greater precision (smaller number) will make the inner while loop execute more often.

This achieves decent results when not using threads. However, when using Python threads, the GIL (Global Interpreter Lock) makes sure only one thread runs at a time. If you have a huge number of threads it may be that it is taking way too much time for the program to go back to your main thread. Increasing the frequency Python changes between threads may give you more accurate delays.

Add this to the beginning of your code to increase the thread switching frequency.

import sys
sys.setcheckinterval(1)

1 is the number of instructions executed on each thread before switching (the default is 100), a larger number improves performance but will increase the threading switching time.

hugos
  • 1,313
  • 1
  • 10
  • 19
  • Thanks Hugo. I've just implemented your approach, and am experiencing the same issue. A period of 1/60 seconds (i.e. 60Hz frequency) is resulting in an actual delay of 0.0311 seconds, which is roughly twice the expected delay of 0.016667. – jars121 May 04 '17 at 00:34
  • Uhm, is there any chance the `ExternalFunction.main` is taking more than the period you want? – hugos May 04 '17 at 00:37
  • The 0.0311 second delay is measured across the while (time.time() - time_before) < period loop only, once the ExternalFunction.main function has returned. – jars121 May 04 '17 at 00:38
  • You seem to suggest you're using threads. Do you know approximately how many threads are running in your program? – hugos May 04 '17 at 00:46
  • Also can you tell me if adding `import sys; sys.setcheckinterval(1)` to the beginning of the code solves the problem? – hugos May 04 '17 at 00:54
  • I've added sys.setcheckinterval(1), which doesn't appear to have had any impact. I'm reviewing the number of threads at the moment. – jars121 May 04 '17 at 01:23
  • I've added a threading.activeCount() print which shows 2 active threads. – jars121 May 04 '17 at 01:27
  • Only 2? So that's likely not the problem – hugos May 04 '17 at 01:33
  • I'm somewhat at a loss. I've just written this: http://ideone.com/SAUOzI, which gives ~60Hz as expected, and copied it exactly to a new .py module on my computer, and am getting the same 32.05Hz (0.0311s period) as in my software. – jars121 May 04 '17 at 01:49
  • That's interesting, so it's definitely not thread related. Last questions: What is your OS and what python are you using? – hugos May 04 '17 at 01:58
  • Windows 7 Enterprise 64-bit and Python 3.4.4. I'll do the same testing on my Linux machine this afternoon. – jars121 May 04 '17 at 02:00
  • Yeah it's very strange. I''ll see how my Linux machine goes, and continue to play around. Thank you for all your input! – jars121 May 04 '17 at 02:15
  • Please let me know if you figure this out. I'm curious :) – hugos May 04 '17 at 02:16
  • I will do! By the way, I've just noticed that a Frequency set at 64Hz results in the 32.05Hz output frequency, but a Frequency set at 65Hz gives me ~64.1Hz output. Further to this point, I can increase the input Frequency up to 200Hz+ and still have the same ~64.1Hz output frequency. There's something very strange going on here! – jars121 May 04 '17 at 02:19
  • 1
    Hugo, this issue appears to have been solved, as per my post here: http://stackoverflow.com/questions/43772960/inaccurate-time-sleep-with-python-3-x-and-windows-7?noredirect=1#comment74587904_43772960. – jars121 May 04 '17 at 03:57
  • Thank you for telling me. So it is in fact a problem with sleep precision on Windows. Nice to know! – hugos May 04 '17 at 11:50
0

You may want to try python-pause

Pause until a unix time, with millisecond precision:

import pause
pause.until(1370640569.7747359)

Pause using datetime:

import pause, datetime
dt = datetime.datetime(2013, 6, 2, 14, 36, 34, 383752)
pause.until(dt)

You may use it like:

freqHz=60.0
td=datetime.timedelta(seconds=1/freqHz)
dt=datetime.now()

while true:
  #Your code here
  dt+=td
  pause.until(dt)
xvan
  • 4,554
  • 1
  • 22
  • 37
  • Thanks xvan. I've just tried the pause module, including pause.until, pause.seconds and pause.milliseconds, but find the 'actual' delay is always 0.0311, as per all other suggested and attempted approaches. – jars121 May 04 '17 at 00:43
0

Another solution for an accurate delay is to use the perf_counter() function from module time. Especially useful in windows as time.sleep is not accurate in milliseconds. See below example where function accurate_delay creates a delay in milliseconds.

import time


def accurate_delay(delay):
    ''' Function to provide accurate time delay in millisecond
    '''
    _ = time.perf_counter() + delay/1000
    while time.perf_counter() < _:
        pass


delay = 10
t_start = time.perf_counter()
print('Wait for {:.0f} ms. Start: {:.5f}'.format(delay, t_start))

accurate_delay(delay)

t_end = time.perf_counter()
print('End time: {:.5f}. Delay is {:.5f} ms'.
      format(t_end, 1000*(t_end - t_start)))

sum = 0
ntests = 1000
for _ in range(ntests):
    t_start = time.perf_counter()
    accurate_delay(delay)
    t_end = time.perf_counter()
    print('Test completed: {:.2f}%'.format(_/ntests * 100), end='\r', flush=True)
    sum = sum + 1000*(t_end - t_start) - delay

print('Average difference in time delay is {:.5f} ms.'.format(sum/ntests))`
Bruno Vermeulen
  • 2,970
  • 2
  • 15
  • 29